沙箱实现:策略转换、内核机制与执行边界
从 PermissionProfile 到最终进程,重点追踪 macOS 与 Linux 的实际限制机制及条件重试。
学完你能做到
- 能说明逻辑权限怎样转换成平台启动参数
- 能解释 与 / 的执行机制
- 能区分受限升级、控制通道保护和远程执行器边界
本课目录 · 7 节
1. 沙箱不是一个固定容器产品
公开代码中的 sandbox 是执行限制抽象,不是统一的虚拟机或 Docker 环境。它把 PermissionProfile 所描述的文件系统与网络权限,转换成目标执行环境可施加的机制。macOS 用 策略包住进程;Linux 主路径组合 与 ;Windows 有和其他后端。远程执行时,这些转换还可能发生在 exec-server。
| 层 | 主要输入/输出 | 负责什么 |
|---|---|---|
| 策略数据 | PermissionProfile、文件/网络策略、额外授权 | 描述允许访问哪些资源,不直接启动进程。 |
| 执行协调 | ToolOrchestrator、要求、环境 | 决定是否允许、第一次用什么边界、是否可以重试。 |
| 平台转换 | SandboxManager::transform | 把逻辑策略变成 argv、环境和有效 profile。 |
| 执行边界 | 本地/远程执行器、平台启动逻辑 | 真正创建受限进程并收集结果,不能只依赖遵守指令。 |
读取环境与权限 ↗codex-rs/core/src/tools/orchestrator.rs:146–167
到执行边界的请求类型 ↗codex-rs/sandboxing/src/manager.rs:95–135
2. 协调器先作授权判断,再准备执行快照
ToolOrchestrator 读取工具选择的环境、PermissionProfile 和执行器是否自行管理进程。后者为真时,符号化工作区根保留给执行器解析;否则协调器把它们物化为路径。工具可以给出 ExecApprovalRequirement,未给出则使用默认规则。
| 要求 | 协调器动作 | 容易误读的细节 |
|---|---|---|
Forbidden | 返回 Rejected | 不会靠换平台绕过拒绝。 |
NeedsApproval | 准备授权动作,等待 Session.request_approval | 批准仍要进入权限转换和实际执行。 |
Skip | 通常由配置允许 | strict_auto_review 打开时,Skip 也会请求一次审核;不是“永远不”。 |
三种授权要求和 strict 分支 ↗codex-rs/core/src/tools/orchestrator.rs:166–235
批准后也不只有“关闭”一种处理。EscalatedSandboxWithRestrictions 分支用 for_approved_command 计算扩展后的文件权限,同时保留其它限制;远程环境还要报告能安全保持 denied reads 的能力,否则拒绝。这体现了“扩大某项权限”和“移除全部边界”的区别。
受限升级与远程能力核对 ↗codex-rs/core/src/tools/orchestrator.rs:238–303
SandboxManager::should_sandbox 先处理 Forbid/Require/Auto,再由平台与网络要求选择具体实现。需要边界却本机不能提供时,协调器还有 ensure_native_sandbox 检查;不能把 select_initial 返回 None 单独解释为安全放行。命令执行目录 cwd 和策略解析目录 sandbox_policy_cwd 也分开保存,不能混用。
选择是否需要沙箱 ↗codex-rs/sandboxing/src/manager.rs:313–352
执行器管理边界与首次尝试快照 ↗codex-rs/core/src/tools/orchestrator.rs:330–385
3. macOS:生成 SBPL,用 sandbox-exec 启动命令
路径取出有效的读写与网络策略,生成 (macOS 策略语言)字符串。启动形式由源码构造为 /usr/bin/sandbox-exec -p <policy> -D<路径参数> -- <原命令>。使用固定系统路径,而不是从可被改写的 PATH 搜索包装程序。
Seatbelt 包装命令 ↗codex-rs/sandboxing/src/manager.rs:437–474
固定系统包装程序位置 ↗codex-rs/sandboxing/src/seatbelt.rs:58–62
基础模板从 deny default 开始,允许进程创建与必要的运行能力;子进程继承策略。动态部分根据可读根、可写根、只读子路径、不可读根和网络规则补 allow/deny。受限读取还需要平台默认路径和临时目录等引导资源,否则解释器和动态库本身可能无法启动。
(deny default)
; child processes inherit the policy of their parent
(allow process-exec)
(allow process-fork)
(allow signal (target same-sandbox))这是内核策略模板的起点。不是把“只能访问工作区”写进提示就算完成。
查看原文及相邻代码 ↗有效根与临时资源 ↗codex-rs/sandboxing/src/seatbelt.rs:895–914
允许父目录可写之后,仍须保住内部只读子路径。实现保护这些路径的祖先目录,拒绝相关 unlink/rename,防止通过移动父目录把受保护内容移出原来的路径规则。deny glob 会转换为带锚点的正则,并结合解析后的路径别名;嵌套可写根的符号链接也受到检查。
符号链接可写根的准备限制 ↗codex-rs/sandboxing/src/seatbelt.rs:414–454
不可读 glob 和祖先保护 ↗codex-rs/sandboxing/src/seatbelt.rs:623–677
只读子路径的祖先集合 ↗codex-rs/sandboxing/src/seatbelt.rs:915–937
网络策略不只是“联网开/关”。托管代理模式只允许相应 loopback 端口、可选本地绑定和 Unix socket 白名单。即使这些连接被允许,文件受限进程也不能碰到有更高权限的 socket;源码追加 daemon transport 保护。另外,代码显式拒绝会通过只读描述符改变文件的特定 fcntl 操作。
代理端口与本地连接 ↗codex-rs/sandboxing/src/seatbelt.rs:329–350
daemon socket 与描述符变更防护 ↗codex-rs/sandboxing/src/seatbelt.rs:1058–1084
控制通道保护测试 ↗codex-rs/sandboxing/src/seatbelt_daemon_socket_tests.rs:1–52
4. Linux:先重建挂载视图,再安装系统调用过滤
SandboxManager 将原命令包为 codex-linux-sandbox helper,并传入目录、有效权限和网络状态。helper 的常规路径先构建 参数,进入新的视图,再重新进入自己的 inner stage。只有这时才安装 /,然后 exec 原命令。
Linux helper 参数转换 ↗codex-rs/sandboxing/src/manager.rs:477–528
外层 bubblewrap 与内层命令 ↗codex-rs/linux-sandbox/src/linux_run_main.rs:294–345
| 顺序 | 文件系统视图的构造 | 原因 |
|---|---|---|
| 1 | 全盘可读通常 --ro-bind / /;局部可读从 --tmpfs / 开始,再加可读根 | 把“看得到”和“可以写”分开;不是一律只隐藏工作区外的文件。 |
| 2 | 最小 /dev 和必要平台资源 | 保留标准设备和运行依赖,避免整个宿主设备树裸露。 |
| 3 | 先遮罩不可读祖先,再恢复允许的子路径 | 使明确的细粒度授权在宽范围拒绝里仍能生效。 |
| 4 | 对明确可写根加 --bind root root | 重开写权限;这是原文件的挂载,不是工作区副本。 |
| 5 | 对受保护子路径重新 --ro-bind | 父目录可写不应顺带允许修改 .git/.agents/.codex 等受保护元数据。 |
| 6 | 可写根内部和无关区域的不可读路径最后遮罩 | 挂载顺序决定最终可见性与覆盖关系。 |
挂载层叠顺序的原始说明 ↗codex-rs/linux-sandbox/src/bwrap.rs:414–432
主路径还建立和 ;是否继承、是否隔离取决于模式。inner stage 检查描述符挂载和残余 ,按需要激活代理,再施加系统调用过滤。 防止后续 exec 获得额外权限; 则限制系统调用和 socket family。关闭直接 IP 联网的模式仍可保留部分本地 ;托管代理模式在隔离网络中按规则开放相应连接。
命名空间开关 ↗codex-rs/linux-sandbox/src/bwrap.rs:299–312
内层验证与施加限制 ↗codex-rs/linux-sandbox/src/linux_run_main.rs:196–242
权限状态到过滤模式 ↗codex-rs/linux-sandbox/src/seccomp.rs:36–100
受限网络系统调用规则 ↗codex-rs/linux-sandbox/src/seccomp.rs:181–232
必须纠正一个容易延续的旧结论:此提交的文件受限执行要求 。虽然还留有 legacy 的代码,helper 会拒绝把文件受限策略交给该旧路径;正常 失败也不会自动回落到 legacy 。函数和枚举里的 LinuxSeccomp 名称,并不表示文件边界只靠 完成。
fn ensure_legacy_landlock_mode_supports_policy(
use_legacy_landlock: bool,
file_system_sandbox_policy: &FileSystemSandboxPolicy,
) {
if use_legacy_landlock && !file_system_sandbox_policy.has_full_disk_write_access() {
panic!("filesystem-restricted execution requires bubblewrap to isolate app-server sockets");
}
}
兼容代码存在不代表它仍是受限文件系统的可选兜底。
查看原文及相邻代码 ↗5. Windows 与远程执行:同一策略,不同落点
WindowsRestrictedToken 分支把有效 profile 保留给后续平台启动器;直接 spawn 的路径要额外包上 Windows helper。令牌实现调用 CreateRestrictedToken,组合权限标志与限制 ;文件边界还结合 。配置为 MXC 时走另一个后端,并先检查能力是否可用。本文不把这些分支合并成“所有 Windows 安装都有同一种隔离”。
Windows 的平台启动边界 ↗codex-rs/sandboxing/src/manager.rs:530–542
直接启动需要额外包装 ↗codex-rs/sandboxing/src/manager.rs:568–600
受限令牌创建 ↗codex-rs/windows-sandbox-rs/src/token.rs:473–518
远程执行尤其要区分 controller 与 executor。协调阶段保留 PathUri 和执行器原生符号根,到真正的执行边界再验证和转换成本机路径。当前 Mac 是控制端,并不能证明远程任务用了 ;应根据目标执行器的 OS、 和最终启动请求确认。
PathUri 的执行边界约定 ↗codex-rs/sandboxing/src/manager.rs:95–116
运行时报告执行器管理边界 ↗codex-rs/core/src/tools/runtimes/unified_exec.rs:200–218
6. 权限失败后的重试:一个受条件约束的第二次尝试
只有 SandboxErr::Denied 进入相应升级路径;参数错误、普通程序错误、取消或其他 Codex 错误不在这个分支。即使是 Denied,还要检查网络拒绝是否能形成授权请求、工具是否允许升级、当前策略是否允许、权限是否能扩大。
拒绝分类及重试前置条件 ↗codex-rs/core/src/tools/orchestrator.rs:395–475
strict_auto_review 下,第一次批准只覆盖第一次受限尝试;去掉的重试需要新的审核。非 strict 模式才可能满足缓存批准的绕过条件。第二次尝试是否仍有,依赖 unsandboxed_allowed 与网络/权限要求;并不是“重试”这个词就代表完全解除限制。该函数明确执行一次第二尝试,然后把结果或错误返回,不在这里无限循环扩大权限。
// Strict auto-review approval covers the sandboxed attempt only;
// retrying without the sandbox requires a fresh guardian review.
let bypass_retry_approval = !strict_auto_review
&& tool.should_bypass_approval(approval_policy, already_approved)
&& network_approval_context.is_none();严格模式不复用受限尝试的批准来覆盖更大的执行权限。
查看原文及相邻代码 ↗第二次尝试的实际边界 ↗codex-rs/core/src/tools/orchestrator.rs:529–587
7. 一个可审查的执行例子
设命令要在 /workspace/app 写输出,.git 为只读子路径,网络只准通过代理。Linux 正常路径会先建立只读或局部可读视图,再给工作根可写挂载,最后把 .git 保护恢复;网络栈与 syscall 规则要求命令走已准备的代理通道。
若命令尝试写 .git/config,不能仅根据退出码断言是拒绝。应保留 argv、执行目录、策略目录、有效 profile、最终 wrapper 和拒绝分类。若它写允许的 build/output 文件,修改是实际发生在可写根上的;若它访问外部工具服务,还需确认该服务的凭据和自身权限,不能推断本机进程边界自动约束服务端操作。
沿着源码读
以下定位均锁定本课程版本。先读注释与分支,再深入类型定义。
- 授权、有效权限与第一次尝试
codex-rs/core/src/tools/orchestrator.rs:146–385 - 拒绝与第二次尝试
codex-rs/core/src/tools/orchestrator.rs:395–587 - 平台转换
codex-rs/sandboxing/src/manager.rs:355–542 - macOS 策略生成
codex-rs/sandboxing/src/seatbelt.rs:878–1105 - Linux 两阶段执行
codex-rs/linux-sandbox/src/linux_run_main.rs:196–345 - 挂载顺序
codex-rs/linux-sandbox/src/bwrap.rs:414–432 - 系统调用与权限约束
codex-rs/linux-sandbox/src/seccomp.rs:36–100
实现推演
给一个带工作区可写根、.git 只读子路径和代理网络要求的执行请求,分别画出 macOS 与 Linux 的转换步骤。然后设计三项验证:改写受保护文件、访问 daemon 控制 socket、启动会尝试直连网络的子进程。每项明确预期的限制层与需要收集的证据。
阅读练习无需模型账号。示例命令请先确认目录与环境。
自测与边界检查
为什么在可写工作根上恢复只读子路径还不够简单?
展开自测答案
父根挂载会覆盖之前的保护;重命名祖先又可能让内容离开路径 carveout;符号链接、别名和控制 socket 都可能构成另一条访问路径。因此实现还需要顺序、祖先保护、路径准备与控制通道限制。
此提交中, 启动失败会自动切回 吗?
展开自测答案
不会。常规路径明确不回落到 legacy ,helper 还拒绝用 legacy 路径执行文件受限策略。保留旧实现和兼容开关不代表可以安全降级。