你写了一个 Agent,它替你在网上下载了一个压缩包,然后打算解压执行。你会让它直接在你的 Mac 上跑吗?除非你不要这台电脑了。于是你开始搜“Agent 沙盒”,搜出来 Docker、gVisor、Firecracker、Wasm,四个名字个个都像正确答案,但选哪个?这个坑我踩过,今天说说我的理解。

先搞清楚你要防什么
沙盒的本质是隔离,但“隔离”这个翻译骗了很多人。它不只是把代码关起来,而是规定代码能碰到什么、不能碰到什么。不同沙盒的隔离粒度完全不同:有的只是把目录挂载切换一下,有的则完全模拟了一台电脑。
我最早以为 Docker 就是沙盒。你跑个 docker run,进程不是照跑?但只要看过 Docker 的文档就知道,它只是用 namespace 和 cgroups 做了资源限制和命名空间隔离。容器和宿主机共享同一个内核。内核里的漏洞一旦被利用,隔离墙就塌了。
四种方案,各有什么看家本领
Docker:一个高效的进程包装器
Docker 不是安全设备,它是一个打包和分发工具。它解决的是“在我机器上能跑,为什么在你机器上跑不起来”。Docker 官方文档里说得明白,它利用 Linux 内核的隔离能力把进程划分开,但所有容器共用同一颗内核。任何一个能利用内核漏洞的恶意程序,都有可能逃逸到宿主机。历史上爆过一堆容器逃逸漏洞,比如 CVE-2019-5736,就是在 runc 里发现的,容器可以直接覆盖宿主机的 runc 二进制文件。
因此,如果你拿 Docker 当沙盒用,你的威胁模型只适合“代码是良性的,只是怕误操作”。恶意代码吗?Docker 挡不住。
gVisor:用 Go 写一个用户态内核
gVisor 的做法很特别:它把应用程序发出的系统调用全都拦截下来,转到自己在用户态实现的一个“Linux 内核”里去处理。你写代码时还是调用 open、read、write,但真正在内核里执行的,是一个 Go 语言写的模拟层。这个项目由 Google 开源,架构文档里说得很直白:它的目的是提供一个最小攻击面的隔离边界。
代价是性能。每一条系统调用都要经过拦截、模拟、返回,比直接调用真实内核多几个数量级的开销。但好处是,你不需要额外硬件虚拟化,只要内核支持某几个基础特性就能跑,所以一般只比原生慢一点,但在读文件、写文件这类密集调用下可能会慢到让你怀疑人生。gVisor 还有很多简化实现,比如不支持某些 Linux API,所以不是所有程序都能直接跑。
gVisor 还有一个实用价值:它可以接入 Docker 的 OCI runtime,你只需要把默认的 runc 换成 runsc,Docker 容器就自动获得了额外的隔离层。GitHub 仓库里有详细说明。这是我觉得最实用的特性之一。
Firecracker:为无服务器打造的微型虚拟机
Firecracker 是 AWS 为了 Lambda 和 Fargate 专门开发的。官网开头就写:它让你在毫秒级启动一个基于 KVM 的 microVM,每个 microVM 只占用最少的内存。跟传统虚拟机比,它去掉了所有不必要的硬件模拟,只保留一个最小的设备模型,但隔离边界依然是真正的硬件虚拟化:每个 microVM 里有独立的内核,宿主机和客户机之间隔着一层 hypervisor。
这意味着什么?就算客户机里的内核被攻破了,攻击者也只是拿到一个虚拟机的 root,他必须再找一个 hypervisor 的漏洞才能碰到你。这是目前生产环境里最硬的一层隔离之一。代价是每个 VM 有自己的内存开销(几百 MB 起步),而且需要支持 KVM 的环境。你本地开发机的 Docker Desktop 里跑 Firecracker,基本跑不了——需要完整的 Linux + KVM。
AWS 官方博客介绍过,Lambda 和 Fargate 正是靠它实现了大规模多租户隔离,冷启动时间被压到 150 毫秒以内。这个数据是真实可查的。
Wasm:在语言层面把权力锁死
Wasm 和上面三个完全不是一个维度的。它根本不是操作系统级的隔离,而是语言级的。你写一个 Rust / C++ 程序,编译成 .wasm,然后在 wasmtime、WasmEdge 这类运行时里执行。这个运行时只给程序一个接口列表,程序想做什么全由宿主决定。没有系统调用,没有文件描述符,没有网络 socket,除非运行时的“能力”主动注入。
这里的关键是 WASI(WebAssembly System Interface),它定义了 Wasm 程序访问操作系统资源的标准接口。WASI 采用能力模型:程序要打开哪个文件、连接哪个地址,必须由宿主在启动时显式授权。没有授权,函数根本不存在。这比任何 Linux 的权限控制都更细粒度。
所以 Wasm 的隔离粒度是最细的,性能损耗也最低——因为根本没有上下文切换。但它能做的事也最少。你不能在 Wasm 里随便 fork、exec、操作 GPU。如果你需要一个能跑任意系统调用的通用计算环境,Wasm 给不了你。
一张表看透四种方案
| 方案 | 隔离层级 | 安全边界 | 启动速度 | 性能损耗 | 适合场景 |
|---|---|---|---|---|---|
| Docker | 内核命名空间 | 共享内核,弱 | 毫秒级 | 接近零 | 打包分发、本地开发、垂直扩展 |
| gVisor | 用户态内核 | 强(隔离系统调用) | 毫秒级 | 中高(Syscall 开销) | 多租户容器、不可信代码隔离 |
| Firecracker | 硬件虚拟化 | 很强(完整VM) | 数十毫秒 | 低-中(VM 开销) | Serverless、强隔离多租户 |
| Wasm | 语言级能力 | 可设计(能力模型) | 微秒级 | 极低 | 插件系统、CDN 边缘、纯函数执行 |
性能与安全的取舍,不是一条直线
你可能会想,越安全就一定越慢,但 Wasm 打破了这个直觉。因为它的隔离不依赖操作系统,纯粹靠编译器把非法操作在编译阶段就变成了不可能。所以它可以在 1 微秒内启动一个执行环境,吞吐量接近原生。但它限制了你只能做“能被编译成 Wasm 的事情”。
Firecracker 和 gVisor 就不同了。它们要支持“完整”的 Linux API,所以不可避免地要引入性能开销。gVisor 在计算密集型任务上的损耗可能不大,但在系统调用密集的场景(比如文件复制、网络转发)会高出不少。Firecracker 的损耗主要来自虚拟化本身的 CPU 开销,和传统虚拟机相比已经压得很低,但仍需要固定内存和冷启动时间。
这里我想说一个我踩过的坑。最早我在项目里用 Docker 容器跑用户提交的代码,以为加了 --read-only 和 --cap-drop ALL 就安全了,结果被安全测试的人打脸——只要容器里有 setuid 二进制或者未挂载的 procfs,就能造成不少麻烦。后来我翻了很多资料,才发现容器隔离的安全模型和 VM 完全不是一回事。将 Docker 用作沙盒,只是在“资源配额”上做到了,在“安全边界”上远远没做到。
怎么选:按你的威胁模型来
- 你的 Agent 只跑你自己写的、被信任的脚本——Docker 就够了。它干净、顺手、生态成熟,哪怕出了问题,你重启一下容器就行。
- 你的 Agent 需要执行从互联网下载的、来源不明的代码——至少上 gVisor 或 Firecracker。gVisor 省内存、启动快,适合高并发小任务;Firecracker 更重但更安全,适合需要完整系统能力的任务。
- 你的 Agent 不需要操作系统 API,只是一些计算逻辑(比如写 JSON、做验证、解析数据)——用 Wasm。它比任何一种 OS 级沙盒都快,而且资源占用可以忽略不计。
- 最极端的情况:你的 Agent 要做的事情本身就是危险的(比如反编译、渗透测试),那最好用 Firecracker 再叠一层 gVisor,双重保险。
很多人的误区是:先选一个看着酷的沙盒,再往里塞自己的 Agent。正确做法是先把 Agent 要做的事列出来,看看它访问哪些系统资源,需要哪些 API。如果它只需要读写几个文件、执行几段代码,Wasm 足够了。如果它要 fork、exec、连 socket,那你只能选 gVisor 或 Firecracker。
实操:一个组合思路
我现在做 Agent 时,会先用一个“执行规划器”分析不可信代码的类型。如果它只是一个 Wasm 模块,直接丢进 wasmtime,按需注入 WASI 权限;如果它是一个原生 ELF,我会启动一个 Firecracker microVM,挂载只读根文件系统,网络通过 TAP 设备接到独立网段,所有 inbound 连接都记录日志。这样既拿到了近似原生的速度,又把风险限制在最小的圈里。
但这套方案需要不少工程投入,如果你只想快速上线,那就选个现成的:gVisor 的 runsc 可以直接替换 Docker 的 runtime,Firecracker 也有 firecracker-containerd 项目让你用容器镜像直接跑 microVM。选型时关注三个指标:你对延迟的容忍度、你的威胁模型、你愿意为隔离付多少成本。
我的结论与偏见
如果你做的是一个能执行任意代码的 Agent,永远把 Docker 排除在“安全方案”之外。它只是打包工具,不是沙盒。至少上 gVisor 或 Firecracker。而如果你能把 Agent 的能力设计成“纯函数”,那么 Wasm 可能是未来最主流的选择——因为它的隔离原理决定了它天然适合做权限最小化。
原创文章,作者:guanweilu,如若转载,请注明出处:https://guanweilu.cn/article/459.html