WASM vs seccomp:为代码评测器测量沙箱启动开销
上周我们发布了 sandbox_exec——一个用 seccomp-bpf 隔离学生代码的 224 行 C 程序。当时的诚实答案是:"WASM 的安全模型更干净,但 Python 生态还没准备好。"
这周我们量化了"还没准备好"到底意味着多少毫秒。答案比预想的更有意思。
测试环境
- 运行时:Wasmtime v42.0.1
- 平台:macOS arm64
- 方法:每个场景 50 次运行,5 次预热,取均值
- 对比基准:sandbox_exec 包装 Python 3.x
阶段一:冷启动开销
最基础的问题:沙箱启动空载代码需要多久?
| 环境 | 均值 | P95 |
|---|---|---|
| WASM(JIT) | 9.79ms | 10.86ms |
| WASM(AOT 预编译) | 9.25ms | 10.14ms |
| Python(无沙箱) | 14.71ms | 15.29ms |
WASM 比 Python 本身启动更快。这是反直觉的结论——大家默认"虚拟机=慢",但 Wasmtime 的初始化比 CPython 解释器启动更紧凑。
这 10ms 的拆解:
0–2ms: fork() + exec(wasmtime)
2–7ms: Wasmtime 运行时初始化
├── 命令行解析
├── 配置加载
└── WASI 环境准备
7–9ms: WASM 模块处理
├── 文件读取
├── 类型校验
└── JIT 编译
9–10ms: 执行 + 清理绝大多数时间花在 Wasmtime 自身的初始化上,不是模块解析或 JIT。
阶段二:模块大小影响
| 模块大小 | 耗时 | 增量 |
|---|---|---|
| ~100B | 9.58ms | baseline |
| ~4KB | 9.68ms | +0.1ms |
| ~40KB | 10.97ms | +1.4ms |
模块大小增加 400 倍,只多花 1.4ms。初始化开销主导一切。
阶段三:计算性能
这是 WASM JIT 优势开始显现的地方:
| 计算量 | WASM | Python | 加速比 |
|---|---|---|---|
| fib(10) | 10.06ms | 15.12ms | 1.5x |
| fib(20) | 9.63ms | 16.80ms | 1.7x |
| fib(25) | 10.77ms | 25.94ms | 2.4x |
| fib(30) | 15.91ms | 128.97ms | 8.1x |
fib(30) 时,WASM 总耗时约 16ms(10ms 启动 + 6ms 计算),Python 需要 129ms。WASM 整体超越 Python 的临界点大约在 fib(20–25)——大概是计算开销超过启动开销的那个位置。
对于评测算法题的评测器来说,这个差距是真实的。
阶段四:I/O 开销
| 操作 | WASM | Python |
|---|---|---|
| 1 次 fd_write | 10.16ms | 15.15ms |
| 100 次 fd_write | 9.97ms | 15.23ms |
100 次写入和 1 次写入耗时几乎相同。启动开销完全掩盖了 I/O 本身的开销,WASI I/O 额外成本可以忽略不计。
阶段五:内存分配
| 内存大小 | 耗时 |
|---|---|
| 64KB | 9.62ms |
| 1MB | 9.86ms |
| 4MB | 10.04ms |
| 16MB | 9.74ms |
WASM 使用懒惰分配。声明 16MB 内存几乎不影响启动时间。
阶段六:安全特性的开销
| 配置 | 耗时 | 备注 |
|---|---|---|
| 无限制 | 9.58ms | baseline |
| +fuel(指令计数) | 7.94ms | 反而更快 |
| +内存限制 | 7.76ms | 反而更快 |
| +目录预授权 | 10.50ms | +0.9ms |
| 全部开启 | 7.91ms |
添加 fuel 和内存限制反而比不加更快——可能触发了某条优化路径。唯一有可测量开销的是目录预授权(+0.9ms 用于文件系统能力配置)。
安全特性的开销是负数。 这很罕见。
安全模型的本质差异
性能之外,安全模型的对比更鲜明:
| 维度 | sandbox_exec | WASM |
|---|---|---|
| 隔离级别 | 进程 | 虚拟机 |
| 内存隔离 | 共享地址空间 | 线性内存(硬边界) |
| syscall 控制 | seccomp 白名单 | 根本不存在 syscall |
| 文件系统 | 需外部清理 | 能力授权 |
| 网络 | seccomp 阻断 | 默认不存在 |
WASM 不过滤 syscall——它根本没有 syscall。WASI 下的 WASM 模块无法调用 socket()、ptrace() 或 io_uring_setup(),因为在沙箱内部没有调用这些东西的机制。
这比 seccomp 的白名单是本质上更强的保证。seccomp 的逻辑是"阻断这 62 个 syscall",WASM 的逻辑是"没有 syscall 这个概念"。两者之间的攻击面差距是性质上的,不是数量上的。
为什么我们现在还没用 WASM
安全模型更强。CPU 密集型代码性能更强。启动开销相当。
问题在 Python:
| Python WASM 运行时 | 大小 | C 扩展 | 结论 |
|---|---|---|---|
| MicroPython | 370KB | ❌ | 标准库受限 |
| RustPython | ~5MB | 部分 | 不完整 |
| Pyodide | ~15MB | ✅ | 浏览器专用,500ms+ 启动 |
评测器需要 numpy、scipy 和任意 C 扩展。Pyodide 支持这些,但依赖浏览器 JS 引擎——无法在 Wasmtime 下运行。MicroPython 和 RustPython 不支持完整的科学计算生态。
这不是性能问题,是生态问题。WASM Python 工具链进化很快,但"运行任意学生 numpy 代码"这个需求,它还没准备好。
路线图
现在: sandbox_exec(seccomp + rlimit)
└── 完整 Python + C 扩展
└── ~1.5ms 沙箱 + ~15ms Python 启动
└── 62 个阻断 syscall
1–2 年: WASM 用于非 Python 语言
└── JS/Rust/Go 学生代码 → WASM 直接运行
└── 更强安全性,相当的性能
2–3 年: WASM Python 生态成熟后迁移
└── Component Model + WASI Preview 2
└── 混合架构:Python → sandbox_exec,其他 → WASM混合架构大概率是最终形态:Python 继续用 seccomp(C 扩展支持不可妥协),其他语言用 WASM(生态已经成熟)。
数字总结
如果你在为类似场景评估 WASM:
- 启动开销:~10ms(与 Python 启动本身相当甚至更快)
- JIT 计算:CPU 密集型代码比 CPython 快 2–8 倍
- 安全特性开销:零(安全特性免费,甚至负成本)
- Python 兼容性:需要 numpy/scipy/C 扩展的场景暂不可用
- 其他所有语言:已经可用
10ms 的启动开销不是阻塞因素。Python 生态才是。
性能测试由明石(CTO)主导。所有数据:Wasmtime v42.0.1,macOS arm64,50 次运行均值。
