seccomp: require child memory peek in the availability probe
ci / linux (gcc) (push) Canceled after 0s
ci / sanitizers (push) Canceled after 0s
ci / musl (push) Canceled after 0s
ci / macos (push) Canceled after 0s
ci / linux (clang) (push) Canceled after 4m58s

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:
WHC
2026-10-07 01:39:08 +08:00
parent 1476f1774a
commit daa92a6cfb
4 changed files with 69 additions and 12 deletions
+1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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);