沙箱运行器:拒绝清单与路径牢笼


文档摘要

沙箱运行器:拒绝清单与路径牢笼 本节摘要:验证门决定工具调用该不该跑,沙箱决定它跑时会发生什么。本节发一个子进程运行器:拒绝危险可执行文件、拒绝危险 argv 形状、把每个文件路径牢笼到项目根、截断过大输出、按墙钟超时杀失控进程。它是模型与操作系统之间两层防御的第二层。三类失败反复出现:危险可执行( / / )、argv 把戏(经解释器 偷渡攻击)、路径逃逸(读 )。本节用四条拒绝轴(名/argv/路径/结构)在 subprocess 启动前把它们挡掉,并强调这不是 OS 级沙箱,而是开发时护栏——让常见失败响亮,而非静默跑。 对应原课程:Phase 19 · Lesson 26 · (原英文 )。本节属「Agent Harness 深度构建赛道」第七节。

沙箱运行器:拒绝清单与路径牢笼

本节摘要:验证门决定工具调用该不该跑,沙箱决定它跑时会发生什么。本节发一个子进程运行器:拒绝危险可执行文件、拒绝危险 argv 形状、把每个文件路径牢笼到项目根、截断过大输出、按墙钟超时杀失控进程。它是模型与操作系统之间两层防御的第二层。三类失败反复出现:危险可执行(sudo/chmod -R 777/rm -rf)、argv 把戏(经解释器 -c 偷渡攻击)、路径逃逸(读 ../../etc/passwd)。本节用四条拒绝轴(名/argv/路径/结构)在 subprocess 启动前把它们挡掉,并强调这不是 OS 级沙箱,而是开发时护栏——让常见失败响亮,而非静默跑。

对应原课程:Phase 19 · Lesson 26 · sandbox-runner-denylist(原英文 phases/19-capstone-projects/26-sandbox-runner-denylist/docs/en.md)。本节属「Agent Harness 深度构建赛道」第七节。

学习目标

阅读完本节,你应当能够:

  1. 构建包 subprocess.run、带超时、捕获、截断的 Sandbox 类。
  2. 按名拒绝(拒绝清单)与按结构拒绝(argv 检查器)命令。
  3. 拒绝任何解析到声明项目根之外的路径参数。
  4. shell 模式关闭时拒绝 shell 元字符。
  5. 返回下游可观测性与评估套件可摄取的结构化 SandboxResult

一、问题与直觉

一个能 shell out 的编码 Agent 能在一轮里装后门、外泄密钥、砖开发者笔记本、刷爆云账单。最便宜的防御是不给它 shell;第二便宜的是一个对精确模式清单说「不」的沙箱。

三类失败在 Agent trace 里反复出现。第一,危险可执行:模型为修路径问题会试 sudochmod -R 777rm -rfmkfsdd——这些都不该出现在 Agent 运行里,拒绝清单按名与别名抓。第二,argv 把戏:被告知不能 shell 的模型会经解释器偷渡(python3 -c "import os; os.system('rm -rf /')"bash -c '...'node -e '...')——沙箱得知道任何带 -c 类标志的解释器调用就是多走几步的 shell 调用。第三,路径逃逸:被告知读 ./src/main.py 的模型去读 ../../etc/passwd——沙箱把每个路径参数经 os.path.realpath 解析并断言前缀。

沙箱不是操作系统意义上的安全边界。有代码执行权的攻击者仍能逃。沙箱是开发时护栏:让常见失败响亮,止住 Agent 因纯粹无能而造的破坏。

二、从零实现

四条拒绝轴,每条都是调用的纯函数,还没 subprocess。subprocess 只在每条轴都过后才起。SandboxResult 退出码用常规约定(0 成功、非零失败),加三个哨兵码:denied(-100)、timed_out(-101)、truncated(真退出码 + 标志位)。下游读结构化结果而非解析 stderr。

拒绝清单是可执行 basename 的 frozenset,别名(/bin/rm/usr/bin/rm)都解析到同 basename。argv 检查器认解释器形状:argv[0] 是解释器且后面任一参数以 -c-e 开头则拒。shell 元字符(;|&><、反引号、$())在调用没显式请求 shell 时致拒。

路径牢笼(最微妙):

def _check_path_jail(argv, project_root): root = os.path.realpath(project_root) for arg in argv: if _looks_like_path(arg): # 含 / 或匹配现存文件 target = os.path.realpath(arg) if not target.startswith(root): return deny("path_escape", arg) # 符号链接逃逸也被 realpath 抓 return allow()

沙箱构造时取 project_root。任何像路径的参数(含 / 或匹配现存文件)经 os.path.realpath 归一化,再查对 project_root 的 realpath。解析目标不在根下则拒。项目根里指向外部的符号链接逃逸也被 realpath(而非字面路径)抓。

三、可复用产物

main.py 加测试目录。SandboxResult(exit_code/stdout/stderr/truncated/timed_out/denied/reason/duration_ms)、SandboxConfig(project_root/max_output_bytes/timeout_seconds/denylist/interpreter_block)、Sandbox 类(run(argv, *, shell=False, cwd=None) 返回 SandboxResult)。内部拒绝助手:_check_executable_denylist/_check_argv_interpreter/_check_shell_metachars/_check_path_jail。输出截断带清晰 truncated 标志与捕获流里的标记行。demo 跑一串合法与对抗调用,各显结果。沙箱默认 shell=Falsecapture_output=True,墙钟超时用 timeout 参数,TimeoutExpired 时杀进程组并合成 SandboxResult

四、为何这不是真沙箱

本节沙箱不用 namespace、cgroup、seccomp、gVisor、Firecracker 或任何内核级隔离。子进程能做的,沙箱就能做。保护是结构性的:Agent 被拒最常见的危险调用,响亮的拒绝进可观测性而非静默跑。生产 Agent 要在其上叠层:跑在无特权 Docker 容器、跑在 microVM、drop capabilities、project_root 只读挂载加 scratch 目录读写、设内存与 CPU 的 ulimit、把环境清洗到已知安全白名单。第 29 节会做其中一些。操作系统级隔离超本节范围。

五、与赛道其余组合

第 25 节产出门链,第 26 节是门 ALLOW 后的执行器,第 27 节的评估套件把沙箱结果对照每任务期望退出码,第 28 节在每个 Sandbox.run 外发 gen_ai.tool.execution span,第 29 节的端到端 demo 把真编码 Agent 接过两层。

六、练习

  1. 路径符号链接逃逸:在 project_root 里放一个指向外部的符号链接,确认 realpath 抓住它而非字面路径。
  2. 解释器偷渡:试 python3 -c "import os; os.system('rm -rf /')",确认 argv 检查器拒绝。
  3. shell 元字符:shell=False 时试 cat x | grep y,确认拒绝;显式请求 shell 时确认放行。
  4. 超时杀进程组:跑一个死循环命令,确认墙钟超时杀进程组并返回 timed_out=True
  5. 截断标记:让命令输出超 max_output_bytes,确认 truncated=True 且捕获流有标记行。

本节要点回顾

  1. 沙箱是第二层:验证门决定该不该跑,沙箱决定跑时发生什么。
  2. 四条拒绝轴:名(拒绝清单)/argv(解释器 -c)/路径(realpath 牢笼)/结构(shell 元字符)。
  3. 路径牢笼最微妙:realpath 解析,符号链接逃逸也被抓,不只看字面路径。
  4. 哨兵退出码:denied -100、timed_out -101、truncated 真码 + 标志。
  5. 非 OS 级沙箱:开发时护栏,让常见失败响亮,止无能破坏。
  6. 生产叠层:无特权容器/microVM/drop capabilities/只读挂载/ulimit/env 白名单。

下一节,我们建「评估套件与夹具任务」——pass@1、pass@k、均值延迟与成本,把回归从重构里分出来。


发布者: 作者: Rohit Gupta 转发
评论区 (0)
U