Codex Harness 源码课 / 中文
LESSON 08 / 12 45 分钟

沙箱实现:策略转换、内核机制与执行边界

从 PermissionProfile 到最终进程,重点追踪 macOS 与 Linux 的实际限制机制及条件重试。

公开源码快照 · 2026-10-13 · f82e365922

学完你能做到

本课目录 · 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

Seatbelt 从 deny-default 基础策略合成读、写、网络与额外拒绝规则,再通过固定系统包装程序执行。
Seatbelt 从 deny-default 基础策略合成读、写、网络与额外拒绝规则,再通过固定系统包装程序执行。 · 教学示意,省略次要分支;不是完整调用图。

基础模板从 deny default 开始,允许进程创建与必要的运行能力;子进程继承策略。动态部分根据可读根、可写根、只读子路径、不可读根和网络规则补 allow/deny。受限读取还需要平台默认路径和临时目录等引导资源,否则解释器和动态库本身可能无法启动。

真实源码 · Apache-2.0 · 未改写
(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

Linux 两阶段启动:bubblewrap 先建立视图,内层 helper 验证并安装限制后执行用户命令。
Linux 两阶段启动:bubblewrap 先建立视图,内层 helper 验证并安装限制后执行用户命令。 · 教学示意,省略次要分支;不是完整调用图。
顺序文件系统视图的构造原因
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 名称,并不表示文件边界只靠 完成。

真实源码 · Apache-2.0 · 未改写
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 与网络/权限要求;并不是“重试”这个词就代表完全解除限制。该函数明确执行一次第二尝试,然后把结果或错误返回,不在这里无限循环扩大权限。

真实源码 · Apache-2.0 · 未改写
                // 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 文件,修改是实际发生在可写根上的;若它访问外部工具服务,还需确认该服务的凭据和自身权限,不能推断本机进程边界自动约束服务端操作。

沿着源码读

以下定位均锁定本课程版本。先读注释与分支,再深入类型定义。

READ & VERIFY / 小练习

实现推演

给一个带工作区可写根、.git 只读子路径和代理网络要求的执行请求,分别画出 macOS 与 Linux 的转换步骤。然后设计三项验证:改写受保护文件、访问 daemon 控制 socket、启动会尝试直连网络的子进程。每项明确预期的限制层与需要收集的证据。

阅读练习无需模型账号。示例命令请先确认目录与环境。

自测与边界检查

为什么在可写工作根上恢复只读子路径还不够简单?

展开自测答案

父根挂载会覆盖之前的保护;重命名祖先又可能让内容离开路径 carveout;符号链接、别名和控制 socket 都可能构成另一条访问路径。因此实现还需要顺序、祖先保护、路径准备与控制通道限制。

此提交中, 启动失败会自动切回 吗?

展开自测答案

不会。常规路径明确不回落到 legacy ,helper 还拒绝用 legacy 路径执行文件受限策略。保留旧实现和兼容开关不代表可以安全降级。

学习记录保存在本机

搜索课程

输入关键词,搜索全部课程、词典与来源。

就地词典

在完整词典中查看 →

完整架构总图

放大后横向、纵向滚动阅读;聚焦图示区域后可用方向键滚动。

完整架构职责图,内容与正文总图相同