seccomp: require child memory peek in the availability probe
With Yama ptrace_scope=2 and no CAP_SYS_PTRACE, process_vm_readv and /proc/<pid>/mem both fail, so sc_decide cannot read path strings and every workspace write gets EACCES. The old probe only checked the USER_NOTIF round trip, so CST_SANDBOX=seccomp and the forced-seccomp test pass looked available and then failed on the first write. sc_can_peek() forks a child, places a marker on its stack, and retries the same sc_peek path. sc_probe() now needs that too, so those hosts fall back to none (or keep landlock) instead of a silent deny-all sandbox.
This commit is contained in:
@@ -279,6 +279,7 @@ TUI 中 `/tools off NAME`、`/tools on NAME` 可临时开关单个工具。
|
|||||||
(界面显示 `auto · seccomp`)。子进程的写类系统调用(带写标志的 open、创建 / 删除 / 改名 / 链接 / chmod 等)先交给
|
(界面显示 `auto · seccomp`)。子进程的写类系统调用(带写标志的 open、创建 / 删除 / 改名 / 链接 / chmod 等)先交给
|
||||||
agent 里的监督线程,按真实路径判断是否在工作区、临时目录或 `/dev/null` 等之内,不在就返回 `EACCES`;IPv4/IPv6 socket 一律拒绝。
|
agent 里的监督线程,按真实路径判断是否在工作区、临时目录或 `/dev/null` 等之内,不在就返回 `EACCES`;IPv4/IPv6 socket 一律拒绝。
|
||||||
读、执行和已打开文件的写入不检查。它防的是误操作而不是恶意程序:检查之后内核会再读一次路径,多线程程序可以趁机换掉。
|
读、执行和已打开文件的写入不检查。它防的是误操作而不是恶意程序:检查之后内核会再读一次路径,多线程程序可以趁机换掉。
|
||||||
|
探测还会确认能读直接子进程的内存;`ptrace_scope=2` 且无 `CAP_SYS_PTRACE` 时 seccomp 不会启用。
|
||||||
比 shell 活得久的后台进程:桌面发行版开着 Yama(`ptrace_scope` ≥ 1)时它们的受检写入会被拒;命令结束后一律失败。
|
比 shell 活得久的后台进程:桌面发行版开着 Yama(`ptrace_scope` ≥ 1)时它们的受检写入会被拒;命令结束后一律失败。
|
||||||
- macOS:`sandbox_init` profile,同样只放行工作区和临时目录写入,禁网。
|
- macOS:`sandbox_init` profile,同样只放行工作区和临时目录写入,禁网。
|
||||||
- Windows 10 1903+:Low 完整性级别的受限令牌 + Job Object(界面显示 `auto · lowil`)。第一次在工作区运行时
|
- Windows 10 1903+:Low 完整性级别的受限令牌 + Job Object(界面显示 `auto · lowil`)。第一次在工作区运行时
|
||||||
|
|||||||
+13
-10
@@ -66,16 +66,19 @@ every create / remove / rename / link / chmod / chown / utimes / xattr call
|
|||||||
stop until the supervisor has read the path from the child's memory,
|
stop until the supervisor has read the path from the child's memory,
|
||||||
resolved it (cwd, dirfd, `..`, symlinks, `/proc/self`, `/dev/stdout`) and
|
resolved it (cwd, dirfd, `..`, symlinks, `/proc/self`, `/dev/stdout`) and
|
||||||
answered: continue when it lands in the allowed places, `EACCES` otherwise.
|
answered: continue when it lands in the allowed places, `EACCES` otherwise.
|
||||||
Reads, exec and writes to already-open files are not checked. Background
|
Reads, exec and writes to already-open files are not checked. The probe also
|
||||||
jobs that outlive their shell are no longer the agent's descendants: with
|
checks that the parent can peek a direct child's memory (`process_vm_readv`
|
||||||
Yama `ptrace_scope` ≥ 1 (common on desktop distros, absent on Android) the
|
or `/proc/<pid>/mem`); with Yama `ptrace_scope=2` and no `CAP_SYS_PTRACE`
|
||||||
supervisor cannot read their memory and their checked writes fail with
|
that fails, so seccomp is not selected (forced `CST_SANDBOX=seccomp` then
|
||||||
`EACCES`; once the run has finished they get `ENOSYS`. This keeps
|
falls back to unavailable). Background jobs that outlive their shell are no
|
||||||
mistakes out of the rest of the disk but is not a security boundary: the
|
longer the agent's descendants: with Yama `ptrace_scope` ≥ 1 (common on
|
||||||
kernel reads the path again after the check, so a hostile multi-threaded
|
desktop distros, absent on Android) the supervisor cannot read their memory
|
||||||
program can swap it in between. The availability check runs in a
|
and their checked writes fail with `EACCES`; once the run has finished they
|
||||||
throwaway child, so a platform seccomp policy that kills the caller cannot
|
get `ENOSYS`. This keeps mistakes out of the rest of the disk but is not a
|
||||||
take the agent down.
|
security boundary: the kernel reads the path again after the check, so a
|
||||||
|
hostile multi-threaded program can swap it in between. The availability
|
||||||
|
check runs in a throwaway child, so a platform seccomp policy that kills the
|
||||||
|
caller cannot take the agent down.
|
||||||
|
|
||||||
## Terminal notes
|
## Terminal notes
|
||||||
|
|
||||||
|
|||||||
+42
-1
@@ -391,6 +391,47 @@ static ssize_t sc_peek(int pid, uint64_t addr, void *out, size_t n) {
|
|||||||
return k;
|
return k;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Path decisions need sc_peek on the sandboxed child. Yama
|
||||||
|
* kernel.yama.ptrace_scope=2 (and no CAP_SYS_PTRACE) denies that, so a
|
||||||
|
* listener-only probe would report seccomp available while every write
|
||||||
|
* fails with EACCES. */
|
||||||
|
static int sc_can_peek(void) {
|
||||||
|
static const char want[] = "cst-peek-ok";
|
||||||
|
int sp[2];
|
||||||
|
if (pipe2(sp, O_CLOEXEC) != 0)
|
||||||
|
return 0;
|
||||||
|
pid_t pid = fork();
|
||||||
|
if (pid == 0) {
|
||||||
|
char marker[] = "cst-peek-ok";
|
||||||
|
uint64_t addr = (uint64_t)(uintptr_t)marker;
|
||||||
|
close(sp[0]);
|
||||||
|
if (write(sp[1], &addr, sizeof(addr)) != (ssize_t)sizeof(addr))
|
||||||
|
_exit(1);
|
||||||
|
close(sp[1]);
|
||||||
|
pause();
|
||||||
|
_exit(0);
|
||||||
|
}
|
||||||
|
close(sp[1]);
|
||||||
|
if (pid < 0) {
|
||||||
|
close(sp[0]);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
uint64_t addr = 0;
|
||||||
|
int ok = 0;
|
||||||
|
if (read(sp[0], &addr, sizeof(addr)) == (ssize_t)sizeof(addr) && addr) {
|
||||||
|
char got[sizeof(want)];
|
||||||
|
if (sc_peek((int)pid, addr, got, sizeof(got)) == (ssize_t)sizeof(got) &&
|
||||||
|
memcmp(got, want, sizeof(got)) == 0)
|
||||||
|
ok = 1;
|
||||||
|
}
|
||||||
|
close(sp[0]);
|
||||||
|
kill(pid, SIGKILL);
|
||||||
|
int st = 0;
|
||||||
|
while (waitpid(pid, &st, 0) < 0 && errno == EINTR)
|
||||||
|
;
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
static int sc_read_mem(int pid, uint64_t addr, void *out, size_t n) {
|
static int sc_read_mem(int pid, uint64_t addr, void *out, size_t n) {
|
||||||
size_t got = 0;
|
size_t got = 0;
|
||||||
while (got < n) {
|
while (got < n) {
|
||||||
@@ -862,7 +903,7 @@ static int sc_probe(void) {
|
|||||||
int st = 0;
|
int st = 0;
|
||||||
while (waitpid(pid, &st, 0) < 0 && errno == EINTR)
|
while (waitpid(pid, &st, 0) < 0 && errno == EINTR)
|
||||||
;
|
;
|
||||||
return ok && WIFEXITED(st) && WEXITSTATUS(st) == 0;
|
return ok && WIFEXITED(st) && WEXITSTATUS(st) == 0 && sc_can_peek();
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif /* CST_HAVE_SECCOMP */
|
#endif /* CST_HAVE_SECCOMP */
|
||||||
|
|||||||
+13
-1
@@ -367,7 +367,19 @@ int main(void) {
|
|||||||
printf("test_sandbox: forced seccomp\n");
|
printf("test_sandbox: forced seccomp\n");
|
||||||
test_confined(CST_SANDBOX_SECCOMP);
|
test_confined(CST_SANDBOX_SECCOMP);
|
||||||
} else {
|
} else {
|
||||||
printf("test_sandbox: seccomp not available here\n");
|
FILE *yf = fopen("/proc/sys/kernel/yama/ptrace_scope", "r");
|
||||||
|
int yama = -1;
|
||||||
|
if (yf) {
|
||||||
|
if (fscanf(yf, "%d", &yama) != 1)
|
||||||
|
yama = -1;
|
||||||
|
fclose(yf);
|
||||||
|
}
|
||||||
|
if (yama >= 2)
|
||||||
|
printf("test_sandbox: seccomp not available here "
|
||||||
|
"(yama.ptrace_scope=%d; cannot peek child memory)\n",
|
||||||
|
yama);
|
||||||
|
else
|
||||||
|
printf("test_sandbox: seccomp not available here\n");
|
||||||
}
|
}
|
||||||
if (was)
|
if (was)
|
||||||
setenv("CST_SANDBOX", saved, 1);
|
setenv("CST_SANDBOX", saved, 1);
|
||||||
|
|||||||
Reference in New Issue
Block a user