在执行不受信任代码的场景中,如在线代码评测平台或 AI 代理沙箱,分层隔离是关键策略。单纯依赖单一机制易被绕过,而 seccomp BPF 过滤器、用户命名空间和 cgroup v2 的组合提供多层防御:前者限制系统调用,后者隔离特权,后者控制资源。这种架构能有效防范特权提升、资源耗尽和内核逃逸攻击。
用户命名空间:特权隔离基础
用户命名空间允许进程在隔离环境中获得 “假根” 权限,而不影响宿主机。通过 unshare(CLONE_NEWUSER | CLONE_NEWNS | CLONE_NEWPID) 创建命名空间,然后配置 UID/GID 映射,将宿主机非特权用户(如 UID 1000)映射为容器内 UID 0。
关键步骤与参数:
- 调用
unshare(CLONE_NEWUSER | CLONE_NEWNS)前,确保内核启用CONFIG_USER_NS=y。 - 写入
/proc/self/setgroups为deny,防止组权限问题。 - UID 映射:
echo "0 1000 1" > /proc/self/uid_map(宿主机 UID 1000 映射为 0,并限制范围为 1)。 - GID 映射类似:
echo "0 1000 1" > /proc/self/gid_map。 - 挂载私有文件系统:
mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL),然后 bind-mount 最小 rootfs(如 tmpfs + busybox)。
证据显示,这种映射确保即使沙箱内进程有 CAP_SYS_ADMIN,也无法访问宿主机资源。[Kernel docs] 这种隔离是 cgroup 和 seccomp 的前提,避免了真实 root 权限需求。
落地清单:
- 专用沙箱用户:创建
sandbox用户(UID 10000+),所有 jailer 从此用户启动。 - 最小 rootfs:包含语言运行时(如 Python minimal),大小 < 50MB。
- 监控:
cat /proc/[pid]/status | grep Uid验证映射生效。
cgroup v2:资源 containment
cgroup v2 统一层次结构,便于精细限流。将 jailer 及其子进程附加到专用 cgroup 子树,防止 DoS 攻击。
核心参数配置(统一挂载 /sys/fs/cgroup):
- 创建
/sys/fs/cgroup/sandboxes/[sandbox_id]。 - 内存:
echo 134217728 > memory.max(128MB),echo 0 > memory.swap.max(禁用 swap)。 - PID:
echo 64 > pids.max。 - CPU:
echo "100000 1000000" > cpu.max(quota 100ms / period 1s,10% CPU)。 - IO:
echo "8:0 1048576 1" > io.max(针对 /dev/sda,限速 1MB/s)。 - 附加:
echo [jailer_pid] > cgroup.procs。
证据与优势: 在 fork bomb 测试中,pids.max 立即终止超限进程;memory.max 触发 OOM killer 而非宿主机崩溃。结合超时(jailer 后 10s kill -9),确保无挂起。
监控要点:
| 参数 | 阈值 | 告警触发 |
|---|---|---|
| memory.current | >90% max | 高优先 |
| cpu.stat | >quota 5min | 中优先 |
| pids.current | ==max | 立即 kill |
回滚:若 cgroup 失效,fallback 到 systemd 限流 slice。
seccomp BPF:syscall 表面最小化
Seccomp BPF 是最后防线,使用 Berkeley Packet Filter 过滤系统调用,默认拒绝(SECCOMP_RET_KILL)。
最小白名单(纯计算负载,如脚本执行):
read, write, close, exit, exit_group, brk, mmap, munmap, mprotect,
rt_sigaction, rt_sigprocmask, clock_gettime, getpid, gettid, futex,
getrandom, prlimit64 (仅 RLIMIT_AS/CPU_TIME)
禁止:socket*, mount*, clone/unshare (除预设), ptrace, bpf, perf_event_open。
加载时机与代码片段(libseccomp):
scmp_filter_ctx ctx = seccomp_init(SCMP_ACT_KILL);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(read), 0);
// ... 添加上述 syscall
seccomp_load(ctx); // 在 exec 前加载
参数调优:
- 语言特定:Python 加 openat/readlink(预开 FD 替代)。
- 参数过滤:mmap 仅 MAP_PRIVATE|PROT_READ|PROT_WRITE。
- 审计模式:初始 SCMP_ACT_LOG,strace 收集 syscall,迭代收紧。
证据:Docker 默认 profile 禁用 44 个危险 syscall,结合 namespaces 阻挡 99% 已知逃逸。[Docker seccomp docs]
集成实现:jailer 生命周期
- Supervisor fork jailer(sandbox 用户)。
- Jailer:unshare namespaces → uid_map → private mounts → mkdir cgroup → set limits → echo $$ > cgroup.procs → prctl(PR_SET_NO_NEW_PRIVS) → seccomp_load → execve(untrusted)。
- Supervisor:waitpid 或 timeout 10s kill cgroup 树,rm cgroup/mounts。
完整伪代码(bash 包装):
# cg-setup.sh
mkdir /sys/fs/cgroup/sandbox/$ID
# set limits...
echo $$ > /sys/fs/cgroup/sandbox/$ID/cgroup.procs
# ns-seccomp-exec /path/to/rootfs /bin/untrusted
unshare -U -m -p # namespaces
# maps & mounts...
./seccomp_loader # load filter
exec /bin/untrusted
风险与缓解:
- 逃逸:seccomp 后无 ptrace;测试 CVE 如 Dirty COW。
- 性能:BPF 过滤 <1% 开销。
- 兼容:内核 >=5.1 cgroup v2;userns 子继承需 CAP_SYS_ADMIN(用 rootless)。
这种分层方案在生产中证明有效,如 gVisor 或 Firecracker 的灵感来源。部署时从小负载测试,逐步扩展语言支持,确保隔离鲁棒性。
资料来源:
- Kernel.org seccomp docs
- Practical sandbox examples from GitHub & blogs
(正文字数:约 1250)