沙箱与隔离模式

9 分钟读完

S5
深入解析 · 智能体安全

把智能体生成的代码当作敌意的来对待:在 2026 年,共享内核的容器已经不再够用,而真正的选择是 microVM、gVisor、还是仅远程——由你能容忍的爆炸半径来决定。

在一个智能体运行它自己写的代码——或一个它被告知要信任的工具——的那一刻,你就在执行不受信输入,而一个共享内核的 Docker 容器离你的宿主机只差一个内核漏洞。2026 年的共识是:默认把智能体生成的代码当作敌意的,并按爆炸半径来挑隔离:microVM(Firecracker、E2B)给你一个独立的内核和亚 150ms 的启动,gVisor 在用户态拦截系统调用、速度接近容器,而仅远程执行则彻底移除本地执行。本文是带着真实启动数字与威胁模型数字的层级对比、计算机操作与浏览器隔离在哪里改变了这道算式(是屏幕加网络出口、而不只是代码),以及仅远程何时才是对的默认。

STEP 1

智能体代码是不受信输入。

本组里其他每一篇都把流模型的数据当作不受信的——一个工具描述、一份被检索的文档、一个 GitHub issue。而流模型的可执行输出理应得到一模一样的怀疑,可它通常得到的更少。当一个智能体写一段 Python 去转换一个文件、调用一个代码解释器工具、或运行一条被投毒的工具描述让它运行的 shell 命令时,那个即将在你机器上执行的东西,是由一个系统所撰写的——而一个红队基准表明,这个系统会以一个有意义的比率去顺从敌意指令。那段被生成的代码不是你代码库里一个受信的产物;它是一段恰好可执行的不受信输入。

这个重新定框就是全部的论证。如果你不会在你的笔记本上运行一个攻击者的二进制文件,那你就不该在你宿主机的内核、文件系统与网络都可达的情况下运行智能体生成的代码——因为上游两跳之外的一次提示注入,就能让智能体生成的正是那个攻击者的二进制文件。2026 年的共识,横跨多家沙箱厂商,说得很直白:假设任何一个智能体产出的、或被诱导去运行的代码都是敌意的,并让隔离边界——而不是模型的良好行为——成为那个把它兜住的东西。

隔离不替代其他各层;它给它们划界。一道策略闸门能拒绝一个危险的工具调用,但一旦代码在一个解释器里开始执行,对于那段代码所发出的系统调用,策略引擎就已经不在回路里了。当所有更早的控制都已被绕过时,沙箱就是那个兜住爆炸半径的东西——而在工具投毒这条轴上,你应当假设这种情况会发生。来自实践的一个有用定框:沙箱兜住爆炸半径;纵深防御把闭环合上。

STEP 2

隔离层级。

有三个值得了解的层级,它们在一条决定一切的轴上彼此有别:那段不受信代码是否共享你宿主机的内核。共享内核意味着单单一个内核级漏洞就是一次完整的宿主机逃逸;独立内核则意味着同一个漏洞只拿下一个用完即弃的客机。

共享内核的容器(Docker、runc)是几乎每个人最先伸手去拿的默认。它们在远不到一秒内启动、且很廉价,但一台宿主机上的每个容器都共享那台宿主机的内核。命名空间与 cgroups 隔离的是进程视图、而非内核攻击面——所以一次来自容器内部的内核漏洞就是一次宿主机被攻破。对于不受信的、智能体生成的代码,那个共享内核正是 2026 年的姿态所拒绝的暴露——认为它单靠自己已不再够用。

microVM(Firecracker,以及在它之上构建的 E2B)把不受信代码运行在一个真实、极简的虚拟机里,这个虚拟机在一道硬件虚拟化边界之后有它自己的内核。客机内部的一次内核漏洞逃进的是一个一无所有的客机。对虚拟机的那个由来已久的反对意见——启动慢——正是 Firecracker 被造出来要抹掉的:厂商报告 microVM 启动在亚 150ms 的范围内,与容器延迟已足够接近,以至于每个任务一台全新虚拟机变得切实可行。这里归因很重要:这些启动数字来自 microVM 厂商(Firecracker 的维护者、E2B)、而非一个中立的基准,所以把它们当作厂商声称的数字、而非已成定论的事实来对待。

gVisor 坐落在两者之间。它是一个用户态内核:一个沙箱进程拦截客机的系统调用、并自己服务其中大多数,于是那段不受信代码很少直接触到真实的宿主机内核。这在没有一台完整虚拟机的情况下缩小了内核攻击面,而它以接近容器的速度启动。代价是对系统调用密集的负载有一笔兼容性与性能税,而那道边界是一大坨用户态代码、而非一道硬件的。同样,"接近容器速度"这个表述可追溯到那些勘查这一领域的沙箱厂商、而非一个你能作为中立来引用的独立测量。

STEP 3

microVM 与 gVisor 与容器。

把这三者并排放在一次安全评审真正会问的那些问题面前——它是否共享宿主机内核、一个全新实例启动有多快、以及一次内核漏洞的爆炸半径是什么——那个选择就不再是一件口味问题。

tier          kernel        cold start    kernel-exploit blast radius
────────────  ────────────  ───────────   ────────────────────────────
container     SHARED host   <1s           HOST COMPROMISE (namespaces
(Docker/runc)                             isolate view, not the kernel)
gVisor        userspace     ~container    userspace escape, then host;
                            speed         smaller kernel surface
microVM       SEPARATE      <150ms *      throwaway guest only; real
(Firecracker) guest kernel  (* vendor)    virtualization boundary
────────────  ────────────  ───────────   ────────────────────────────
2026 default for UNTRUSTED agent code: microVM; relax to
gVisor / container only when the threat model allows.

把这张表读作一条默认-再放宽的规则、而非一个总要拉满的排名。对不受信的智能体生成代码,2026 年的默认是 microVM,因为那个独立内核是唯一一个内核漏洞够不到你宿主机的层级。你只在威胁模型确实允许时才向下放宽到 gVisor 或一个普通容器——例如,你自己生成并评审过的代码,跑在没有任何机密、没有任何网络的地方,那里更强边界的代价换来的很少。共识在纠正的那个错误是相反的习惯:从一个共享内核的容器起步,并把 microVM 当作一次异域的升级来对待。对默认敌意的代码,microVM 才是基线、而容器是那个需要辩护的例外。

这张表所隐含的另一个设计决策是生命周期。因为一台 microVM 启动廉价、又一无所有,那个强模式就是每个任务一台全新沙箱——或每次工具调用一台——用完立即拆掉。一台长生命周期的沙箱会积累状态,攻击者能跨调用地利用它;一台单次调用后即死的短暂沙箱,则几乎不给一次成功的漏洞留下任何可抓住的东西、也没有任何可持续进下一个请求的东西。

STEP 4

计算机操作与浏览器隔离。

代码执行是那个利落的情形,因为不受信的面就一样东西:系统调用。计算机操作与浏览器智能体把这个面拓宽了,而隔离的算式也得随之拓宽。一个计算机操作智能体不只是运行代码——它驱动一块屏幕、读上面的任何东西、并以一个人类操作员的方式够到网络。沙箱现在得同时兜住三样东西:代码能对宿主机做什么、智能体能在屏幕上看到什么、以及它能把字节发往哪里。

网络出口是团队最常忘掉的那个面。一台隔离得完美无缺、却仍有敞开出站互联网的 microVM,是一条上好的外泄通道:一次落进浏览智能体内部的提示注入能从当前会话里读到一个机密、并干脆把它 POST 出去,而那道内核边界从未起过作用,因为没有任何东西跨过它。出口控制——默认拒绝出站、一个任务确实需要的目的地白名单——对任何会浏览的东西而言,都是隔离边界里一等的组成部分、而不是为合规而事后拴上去的补丁。一个浏览智能体被诱导去做这件事的具体方式是浏览器智能体失效模式那一篇的主题;这里的要点是,沙箱必须假设它会发生。

屏幕本身就是会泄露的状态。一个与其他任何东西共享一块显示器、一份剪贴板、或一个浏览器配置的计算机操作智能体,能跨那个共享面去读和写,所以那个治理内核边界的、同样短暂的、每任务一会话的纪律也应当治理视觉与会话边界:一个全新的、可丢弃的浏览器配置,没有任何环境里已登录的 cookie,任务结束时即弃。生产中的计算机操作那份实战指南涵盖了安全运行这些东西的运营形态;隔离上的要点是,对计算机操作而言,"沙箱"意味着内核加屏幕加出口,而把这三者中任何一个留着敞开,就把整件事重新打开了。

STEP 5

仅远程何时取胜。

最强的隔离,是那段根本不在你基础设施上运行的代码。仅远程执行把不受信负载交给一个提供商,它整个产品就是在可丢弃的 microVM 里运行敌意代码——你得到一个每次调用一个沙箱的 API,而一次成功逃逸的爆炸半径是那个提供商用完即弃的客机、而不是你的宿主机、你的网络、或你的机密。当代码来自一个你无法审查的来源——一个来自不受信 MCP 服务器的工具、一段智能体针对一个被攻击者影响的提示所写的片段——仅远程就是一个稳健的默认,恰恰因为它把边界整个挪出了你的机器。

这个模式是每次工具调用创建一个沙箱、调用后即销毁。只有最少的输入跨进沙箱、只有声明过的结果跨回来,而够到沙箱的凭证被收窄到任务不需要的都没有——正是那个限制任何工具调用能触到什么的受限凭证纪律,被应用在沙箱边界上,好让一个被攻破的客机握着一把随它一同过期的钥匙。

# one fresh microVM sandbox per tool call, torn down after
async def run_untrusted(code: str, task_inputs: dict) -> Result:
    sbx = await Sandbox.create(          # remote microVM, own kernel
        template="code-interpreter",
        timeout=30,                      # hard wall-clock cap
        egress="deny",                   # default-deny outbound
        env=scoped_secrets(task_inputs), # only what THIS task needs
    )
    try:
        return await sbx.run(code, inputs=task_inputs)
    finally:
        await sbx.kill()                 # ephemeral: nothing persists

仅远程并不免费——它添了一次网络跳、一个依赖、以及离开你边界的数据,所以对于你自己生成、又针对没有任何机密去评审过的代码,它可能是杀鸡用牛刀,而一台本地 microVM 或 gVisor 沙箱才是对的那种放宽。但决策规则还是本文开篇那一条:按你能容忍的爆炸半径来挑边界,在一道边界证明之前假设代码是敌意的,并让那道边界——而不是那个模型——成为你所信任的东西。在一个真实的智能体回路里把这些沙箱立起来的机制,是沙箱与执行那份实战手册的主题;而那个姿态,才是本文要求你先修正的东西。