第 6 章 · 执行隔离与沙箱
Agent 能调用代码执行工具的那一刻,"模型生成的内容"就变成了"即将在你的基础设施上运行的代码"。执行隔离研究的问题是:如何让不可信代码在可控的牢笼里运行。
6.1 威胁模型
先明确要防什么——不是所有威胁都值得同等投入:
| 威胁 | 场景 | 防护优先级 |
|---|---|---|
| 模型写错代码误删数据 | LLM 生成的 rm -rf 有路径错误 |
高(概率高、损失大) |
| Prompt injection 驱动的恶意执行 | 网页内容里藏"执行 curl evil.sh" | 高(Agent 特有攻击面,第 11 章) |
| 资源耗尽 | 死循环吃满 CPU/内存、fork 炸弹 | 高(必然发生,不是概率问题) |
| 数据外泄 | 沙箱内代码把 workspace 文件 POST 到外部 | 中高(多租户平台必须防) |
| 沙箱逃逸 | 攻击者利用内核/容器运行时漏洞逃到宿主机 | 低概率高代价(多租户强隔离场景才值得 Firecracker 级投入) |
原则:误删与资源耗尽是必然事件,按工程问题解决;恶意逃逸按威胁建模分级投入。
6.2 隔离技术谱系
从弱到强,成本与启动时间同步上升:
进程级 + 语言运行时限制
Python 里用 resource 模块 / subprocess + ulimit 限制内存、CPU、进程数、输出量。
import subprocess, resource, shlex
def run_limited(code: str, timeout=10, mem_mb=256):
limit = f"ulimit -v {mem_mb * 1024} -u 64 -t {timeout}; "
return subprocess.run(
["bash", "-c", limit + shlex.quote(code)],
capture_output=True, timeout=timeout + 2, text=True)
优点:启动毫秒级。缺点:与宿主共享文件系统、网络、内核——只能防误删与资源耗尽,防不了任何有意攻击。适用:完全可信代码的单机工具、开发环境快速迭代。
容器(Docker / K8s Job)
每任务一个容器:文件系统隔离(镜像层 + 挂载点白名单)、网络策略(egress 白名单)、资源配额(cgroup)、用户降权。
docker_cmd = [
"docker", "run", "--rm",
"--memory=512m", "--cpus=1",
"--network=nothing", # 或 egress 白名单
"--read-only", # 根文件系统只读
"--cap-drop=ALL", # 丢弃全部 Linux capabilities
"--security-opt=no-new-privileges",
"-v", f"{workspace}:/work:rw", # 只挂 workspace
"agent-sandbox:latest", "python", "-c", code,
]
优点:隔离/成本/启动(秒级)的平衡点,K8s Job 化之后获得调度、回收、配额的全部生态。缺点:与宿主共享内核——容器逃逸漏洞(runc CVE 类)存在,恶意攻击者+内核漏洞的组合可以越过隔离。这是多数业务 Agent 的默认选择。
用户态内核(gVisor / Kata)
gVisor 在应用与内核之间实现一个拦截系统调用的"用户态内核",攻击面缩小到 gVisor 实现的 syscall 子集;Kata 用轻量 VM 隔离。代价:兼容性损耗(部分 syscall 慢或缺失)与运维复杂度。适用:多租户、运行不可信三方代码的平台。
microVM(Firecracker 等)
KVM 级硬件虚拟化的极简 VM(<125ms 启动、约 5MB 内存开销),每个任务一个独立内核。优点:硬件级隔离 + 快照(snapshot)能力——把 VM 内存态存盘、之后恢复,实现:
- 确定性回放:快照 + 固定输入 = 可复现的执行(评测与 RL 轨迹的基础设施,第 9/13 章依赖);
- 预热池:从"预加载了运行时"的快照恢复任务,冷启动从秒级降到毫秒级。
代价:需要 KVM 宿主与专职的 worker 管理面(网络命名空间、jailer、mTLS 通道)。适用:强隔离需求 + 快照/回放需求的平台。
选型决策树
代码来源是否完全可信? → 是:进程级限制即可
↓ 否
多租户/面向外部用户? → 否:容器(K8s Job)
↓ 是
是否需要快照/确定性回放或最强隔离? → 否:gVisor/Kata
↓ 是
Firecracker microVM
6.3 Workspace:数据面的隔离
进程隔离之外,数据面的隔离同样重要——Agent 的文件操作应该被约束在一个显式的 workspace 内:
- 路径解析在服务端:工具收到相对路径,服务端拼 workspace 前缀。绝不让客户端传绝对路径(
../逃逸、任意文件读都从这里来); - workspace 是唯一的可写区:容器根文件系统只读,唯一 rw 挂载是 workspace;
- 回写与检查点:workspace 内容要有版本化快照(每次任务结束或定时 checkpoint),误删可恢复——Agent 删文件不是 if 而是 when;
- 生命周期归属:workspace 按用户/Agent 派生与清理(挂到明确的 owner 上,不能成为无主存储的垃圾场)。
6.4 快照与确定性回放(进阶)
快照是高级沙箱的核心能力,三个工程要点:
- 快照一致性:执行后把 workspace 回写到事实源(对象存储/DB),必须确认写入成功后才能推进快照代次(epoch)——回写未确认就推进,崩溃恢复时会出现"VM 内存态指向不存在的文件"的幽灵状态;
- ** fencing(止步证明)**:旧实例恢复后必须证明自己已失效(epoch 过期)才允许继续操作,否则同一 workspace 出现两个"活着的"执行者;
- 确定性是有代价的:快照回放要求固定输入、关闭随机性、网络行为可 mock——这是评测基础设施(第 9 章)与 RL 环境(第 13 章)专用的高投入形态,普通业务 Agent 用不到。
6.5 失败语义
沙箱系统的失败矩阵必须显式设计("分配失败时怎么办"):
| 场景 | 正确行为 |
|---|---|
| 分配超时/失败 | fail-secure:任务失败并明示原因,绝不静默回退到无隔离执行 |
| 执行超时 | 杀进程/回收容器,返回超时错误给模型(可自救:换方法) |
| OOM | 同上;若频繁发生,注入"输出太大,请分步处理"的提示 |
| 沙箱空闲 | 延迟回收(连续 N 秒无执行才销毁),平衡复用与成本 |
| 平台故障 | 归还给编排层(Job TTL、回收策略),不依赖业务代码清理 |
fail-secure 值得单独强调:隔离是安全边界,安全边界的降级必须是显式决策而不是异常处理的副作用。"k8s 分配失败所以先在本机跑一下"这类代码是生产事故的经典起点。
实现作业
- 给第 2 章的 Agent 加代码执行工具:先用进程级限制(6.2)实现,跑通"让 Agent 写一段代码并执行它";
- 改造成 Docker 版本(上面的 docker run 参数模板),对比两者的逃逸面差异,写一份 200 字的对比笔记;
- 构造三个攻击用例验证你的沙箱:读
/etc/passwd(应失败:根文件系统只读+无挂载)、curl外部地址(应失败:无网络)、fork 炸弹(应失败:进程数 ulimit); - 思考题:如果你的平台要给 1 万个外部用户跑"AI 数据分析"(上传 CSV,Agent 写 pandas 代码),你选哪级隔离?逐条列出你的依据。
深入材料
- Firecracker 官方文档(docs.firecracker-microvm.org,快照与 jailer 机制)
- gVisor 架构文档(gvisor.dev,syscall 拦截的设计)
- OWASPGenAI 安全清单(与第 11 章衔接)
读者留言
COMMENTS 暂无还没有留言,来说第一句?