diff --git a/README.md b/README.md index d004e79..644cd27 100644 --- a/README.md +++ b/README.md @@ -279,6 +279,7 @@ TUI 中 `/tools off NAME`、`/tools on NAME` 可临时开关单个工具。 (界面显示 `auto · seccomp`)。子进程的写类系统调用(带写标志的 open、创建 / 删除 / 改名 / 链接 / chmod 等)先交给 agent 里的监督线程,按真实路径判断是否在工作区、临时目录或 `/dev/null` 等之内,不在就返回 `EACCES`;IPv4/IPv6 socket 一律拒绝。 读、执行和已打开文件的写入不检查。它防的是误操作而不是恶意程序:检查之后内核会再读一次路径,多线程程序可以趁机换掉。 + 探测还会确认能读直接子进程的内存;`ptrace_scope=2` 且无 `CAP_SYS_PTRACE` 时 seccomp 不会启用。 比 shell 活得久的后台进程:桌面发行版开着 Yama(`ptrace_scope` ≥ 1)时它们的受检写入会被拒;命令结束后一律失败。 - macOS:`sandbox_init` profile,同样只放行工作区和临时目录写入,禁网。 - Windows 10 1903+:Low 完整性级别的受限令牌 + Job Object(界面显示 `auto · lowil`)。第一次在工作区运行时 diff --git a/docs/compat.md b/docs/compat.md index 052cf4a..bd75aca 100644 --- a/docs/compat.md +++ b/docs/compat.md @@ -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, resolved it (cwd, dirfd, `..`, symlinks, `/proc/self`, `/dev/stdout`) and answered: continue when it lands in the allowed places, `EACCES` otherwise. -Reads, exec and writes to already-open files are not checked. Background -jobs that outlive their shell are no longer the agent's descendants: with -Yama `ptrace_scope` ≥ 1 (common on desktop distros, absent on Android) the -supervisor cannot read their memory and their checked writes fail with -`EACCES`; once the run has finished they get `ENOSYS`. This keeps -mistakes out of the rest of the disk but is not a 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. +Reads, exec and writes to already-open files are not checked. The probe also +checks that the parent can peek a direct child's memory (`process_vm_readv` +or `/proc//mem`); with Yama `ptrace_scope=2` and no `CAP_SYS_PTRACE` +that fails, so seccomp is not selected (forced `CST_SANDBOX=seccomp` then +falls back to unavailable). Background jobs that outlive their shell are no +longer the agent's descendants: with Yama `ptrace_scope` ≥ 1 (common on +desktop distros, absent on Android) the supervisor cannot read their memory +and their checked writes fail with `EACCES`; once the run has finished they +get `ENOSYS`. This keeps mistakes out of the rest of the disk but is not a +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 diff --git a/src/sandbox_exec.c b/src/sandbox_exec.c index 16b04e7..9050c93 100644 --- a/src/sandbox_exec.c +++ b/src/sandbox_exec.c @@ -391,6 +391,47 @@ static ssize_t sc_peek(int pid, uint64_t addr, void *out, size_t n) { 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) { size_t got = 0; while (got < n) { @@ -862,7 +903,7 @@ static int sc_probe(void) { int st = 0; 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 */ diff --git a/tests/test_sandbox.c b/tests/test_sandbox.c index 3375982..fd577bf 100644 --- a/tests/test_sandbox.c +++ b/tests/test_sandbox.c @@ -367,7 +367,19 @@ int main(void) { printf("test_sandbox: forced seccomp\n"); test_confined(CST_SANDBOX_SECCOMP); } 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) setenv("CST_SANDBOX", saved, 1);