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 等)先交给
|
||||
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`)。第一次在工作区运行时
|
||||
|
||||
+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,
|
||||
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/<pid>/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
|
||||
|
||||
|
||||
+42
-1
@@ -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 */
|
||||
|
||||
@@ -367,6 +367,18 @@ int main(void) {
|
||||
printf("test_sandbox: forced seccomp\n");
|
||||
test_confined(CST_SANDBOX_SECCOMP);
|
||||
} else {
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user