Sandlock:705 行 C 代码实现的无 root Linux 沙箱
经过三周为学生代码评测构建沙箱,我们把核心逻辑提取成了独立工具:Sandlock。
705 行 C 代码。无需 root。约 1.5ms 开销。
它是什么
Sandlock 是一个命令行包装器,把不可信代码运行在多层隔离环境中:
# 阻断网络、限制资源、清洗环境变量
sandlock --no-network --no-fork --clean-env \
--cpu 5 --mem 256 --timeout 30 \
-- python3 student_code.py
三层安全机制依次叠加:
┌──────────────────────────────────────┐
│ seccomp-bpf(syscall 过滤) │
│ • 60+ 危险 syscall 阻断 │
│ • 可选:阻断所有网络 │
│ • 可选:阻断 fork │
├──────────────────────────────────────┤
│ rlimits(资源限制) │
│ • CPU 时间、内存、文件大小 │
│ • 文件描述符数量 │
├──────────────────────────────────────┤
│ prctl(NO_NEW_PRIVS) │
│ • 永久阻断特权提升 │
└──────────────────────────────────────┘
在 kernel ≥ 5.13 上,还有第四层可用:Landlock——Linux 的无特权文件系统访问控制。下面详说。
与现有方案对比
| 工具 | 需要 root | 开销 | syscall 过滤 | 文件系统隔离 |
|---|---|---|---|---|
| sandlock | ❌ | ~1.5ms | ✅ | ⚠️(Landlock,需 kernel 5.13+) |
| Docker | ✅ | ~100ms | ✅ | ✅ |
| Firejail | ⚠️ SUID | ~50ms | ✅ | ✅ |
| bubblewrap | ⚠️ | ~10ms | ✅ | ✅ |
定位:单一二进制文件,无 root,快速启动,可组合使用。不需要管理容器,也不需要安装 SUID 二进制。
CI 难题:如何安全测试炸弹
Sandlock 会阻断 fork 炸弹、内存炸弹和 CPU 炸弹。但如何在 CI 里测试沙箱能正确阻断炸弹,同时不真的触发炸弹?
答案是三层防护:
timeout 10 ./sandlock --mem 64 --timeout 5 -- python3 -c "
import itertools; list(itertools.repeat(None)) # 内存炸弹
"
│ │ │
│ │ └── sandlock 5s 超时
│ └── sandlock 64MB 内存限制
└── 外部 10s 强制终止
GitHub Actions job 还有自己的 timeout-minutes: 10。
| 沙箱失效场景 | 防护机制 |
|---|---|
| 内存炸弹漏过 | GitHub runner 内存限制 + job 超时 |
| CPU 无限循环 | timeout 10 + job 10 分钟超时 |
| fork 炸弹漏过 | GitHub 每个 runner 的进程限制 |
| 磁盘写满 | GitHub 存储限制 |
最坏情况:job 在 10 分钟后被 GitHub 强制终止,账号不受影响。
CI 工作流只在 sandlock.c 或 Makefile 变更时触发,不是每次 push 都跑。炸弹测试是可选的:security-tests 工作流有 skip_bombs 输入,手动触发时自行决定是否跑。
Landlock:文件系统隔离层
Landlock(Linux 5.13+)允许无特权进程限制自己的文件系统访问。在 exec 前授权特定路径的读写能力:
# 只有 /tmp 可写;/usr 只读
sandlock --landlock --rw /tmp --ro /usr -- python3 script.py
我们在配置 CI 时踩了一个坑。失败的测试是:
./sandlock --landlock --rw /tmp -- touch /tmp/test
# sandlock: exec: Permission denied (exit 127)
报错不是关于 /tmp 的——而是 /usr/bin/touch。Landlock 启用后,execve() 本身就需要二进制文件可访问。而二进制文件需要加载它的共享库(/lib、/lib64)。所以正确的写法是:
sandlock --landlock --rw /tmp --ro /usr --ro /lib --ro /lib64 \
-- touch /tmp/test
Landlock 的能力模型真的很严格:没有显式授权的路径一律不可访问——包括启动被沙箱进程本身所需的路径。这是正确行为,但意味着你需要考虑沙箱启动需要什么,而不只是被沙箱代码运行需要什么。
未解决的问题
和 shimmy 研究阶段一样的几个缺口:
/proc可读:彻底封堵需要 mount namespace,而那需要 root。用--clean-env缓解(剥离AWS_*等敏感环境变量)。RLIMIT_NPROC按用户计算:fork 炸弹耗尽限制后会影响同用户下所有进程。可以用--no-fork完全禁止 fork 来规避。- Landlock 需要 kernel 5.13+:Sandlock 会在运行时检测,旧内核上自动跳过 Landlock。
仓库
github.com/bkmashiro/Sandlock,MIT 协议。
Sandlock/
├── sandlock.c # 705 行,单文件
├── Makefile
├── test.sh
├── README.md
└── .github/workflows/
├── ci.yml # 编译 + 快速测试,sandlock.c 变更时触发
└── security-tests.yml # 完整炸弹测试,手动触发
集成到 shimmy 只需一行:
cmd := exec.Command("sandlock",
"--no-network", "--no-fork", "--clean-env",
"--cpu", "5", "--mem", "256",
"--", "python3", studentCode)
由明石(CTO)构建,作为 Lambda Feedback MSc 毕业项目的一部分。