seccomp-bpf 是 linux 下最轻量可控的系统调用过滤方案,需在 fork 后、execve 前按序完成 unshare、关闭无关 fd、加载 bpf 过滤器,并配合命名空间隔离方可安全生效。

用 seccomp-bpf 过滤系统调用最直接
Linux 下限制进程能执行哪些系统调用,seccomp-bpf 是目前最轻量、最可控的方案。它在内核态拦截非法 syscall,被拒调用会直接返回 -EPERM 或直接杀进程,不依赖用户态 wrapper 或 ptrace 开销。
关键点在于:必须在 fork() 后、execve() 前调用 prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, ...),且 filter 需用 bpf_prog_load() 加载;否则要么没生效,要么被子进程继承后误杀。
- 只允许基础调用:比如
read、write、exit_group、rt_sigreturn—— 缺少rt_sigreturn会导致信号处理崩溃 - 禁止
openat、execve、socket、clone等高风险调用,尤其注意openat是现代 glibc 打开文件的默认路径 - 用
libseccomp(而非裸 BPF)写规则更安全:它自动处理 ABI 差异(如 x86_64 vs arm64 的 syscall 编号不同)
clone() + unshare(CLONE_NEWPID | CLONE_NEWNS) 隔离进程和挂载视图
仅靠 seccomp 不防路径遍历或读取宿主文件。必须配合命名空间隔离,否则沙箱进程仍能看到 /proc、/sys,甚至通过 /proc/self/fd/ 访问父进程打开的文件。
重点不是“启动新 namespace”,而是确保子进程在调用 execve() 前已完成所有 unshare(),且父进程不保留对新 namespace 的引用(避免逃逸)。
立即学习“C++免费学习笔记(深入)”;
-
unshare(CLONE_NEWPID)后必须立刻fork(),因为 PID namespace 的 init 进程(PID 1)必须是第一个进程,否则execve()会失败 -
unshare(CLONE_NEWNS)后需调用mount(..., MS_REC | MS_SLAVE)把当前挂载点设为从属,否则子 namespace 可能传播挂载事件回宿主 - 不要依赖
chroot():它不防openat(AT_FDCWD, "..", ...)和 symlink race,且与 namespace 混用易出错
子进程 execve() 前关闭所有无关 fd,防止句柄泄露
沙箱子进程若继承父进程打开的文件描述符(比如日志文件、socket、甚至 /dev/kvm),可能绕过路径限制做侧信道攻击或资源耗尽。
标准做法是在 fork() 后、execve() 前遍历 /proc/self/fd/ 关闭非 0/1/2 的 fd,但更可靠的是用 close_range(3, INT_MAX, CLOSE_RANGE_CLOEXEC)(Linux 5.9+),或 fallback 到循环 fcntl(fd, F_GETFD) 检测。
- 务必在
unshare()和seccomp设置之后再关 fd,否则可能关掉 seccomp bpf program 所需的 fd(极少见但存在) - 如果父进程用了
FD_CLOEXEC,也要确认子进程未显式清除它(fcntl(fd, F_SETFD, 0)) - 注意 stdio 的缓冲区:
fflush(stdout)再fork(),否则子进程可能重复输出或丢日志
常见错误:EPERM 不一定来自 seccomp,先查 strace -f -e trace=network,file
运行时出现 Operation not permitted,90% 情况下不是 seccomp 拦截,而是 namespace 权限不足(如没 cap_sys_chroot 却尝试 chroot)、挂载选项错误(如 noexec 目录下执行 ELF)、或 execve() 路径含 symlink 且跨 mount point。
别急着改 seccomp 规则——先用 strace 看最后几个系统调用,重点盯 openat、access、statx 返回值;seccomp 拦截会在 strace 输出里明确标出 --- SIGSYS {si_signo=SIGSYS, si_code=SYS_SECCOMP, ...} ---。
- 测试时用静态链接的二进制(如
busybox),避免动态链接器触发一堆openat查找ld-linux.so - 如果
strace自身被 seccomp 拦截(比如禁了ptrace),就换用perf trace -e 'syscalls:sys_enter_*' - 容器环境(如 Docker)中,
seccompprofile 可能已预加载,和你代码叠加导致双重拦截,需检查/proc/$(pid)/status的Seccomp:字段
真正难的不是拼出这几个 API,而是让它们按严格顺序执行:fork → unshare → close fd → seccomp → execve。中间任意一步失败(比如 unshare 返回 -1 但没检查),沙箱就形同虚设。










