OpenAI Codex · 执行边界

macOS:把安全策略拼成一个字符串

Seatbelt 吃的是一段文本。文本由静态基线加现拼的读写网段组成。路径不进这段文本,走旁边的参数表。

课程目标读完能说清三件事。macOS 上平台沙箱默认就在,没有 Windows 那种总开关。交给 sandbox-exec 的策略正文只留占位符,真实路径走 -D。挖空一条路径时,要同时挡住这个节点、它的子孙,以及把这个节点搬走。
先玩一遍 · 开关片段,看菜单怎么长
策略拼装台:左边加片段,右边看正文、配料表和放行范围
片段
点片段开关,看策略正文和右侧探针怎么变。
网络
实验
策略正文 · 只留代号
配料表与探针
逻辑轨迹 · 动画每一步对应源码里的哪一段
  1. 权限档案拆成读写网络manager.rs L375
  2. 固定使用 /usr/bin/sandbox-execseatbelt.rs L56
  3. 装入四份静态 sbplseatbelt.rs L21
  4. 按根生成 allow 与 require-notseatbelt.rs L476
  5. 可写根钉上 file-write-unlinkseatbelt.rs L508
  6. 排除子路径同时写 literal 与 subpathseatbelt.rs L551
  7. 拼网络段,managed 无端口走受限seatbelt.rs L309
  8. sections join 成 -p 文本seatbelt.rs L1012
  9. 路径写成 -DKEY=valueseatbelt.rs L1024
  10. 双短横线后接用户命令seatbelt.rs L1029
点播放,看一份策略怎样由片段拼出来。也可自己开关片段。
正文和路径分开菜单上只写 param 代号。真实目录名落在 -D。带引号的路径污染不了 SBPL 语法。
挖空少一条就漏只留 subpathmkdir .codex 会成功。两条 require-not 加上最后的 unlink,才把节点、子孙和搬家一起钉住。
网络先收窄要走代理却没有端口,就不要放行整网。网络基线还在,只是没有 blanket outbound。
教学示意:路径与探针为课程化设定,用来展示片段如何改变策略文本和放行范围。逻辑轨迹右侧行号对应 openai/codex 仓库 commit 4f39251a01。
思路一 · 先关大门,再按片段开门
它解决什么问题

同事把仓库设成可写根,让模型在里面改代码。模型执行 mkdir .codex,想在仓库里落一份自己的配置。命令立刻失败,stderr 写着 Operation not permitted。可写根听起来像整棵树,实际不是。

再试一条更绕的路。模型先在仓库里写一个普通文件,再 mv 整个工作区,想把里面的 .codex 一起挪到沙箱外面。这条也失败。错误还是 Operation not permitted。

思路是什么

macOS 上 get_platform_sandbox 直接返回 MacosSeatbelt。那个布尔开关只对 Windows 有意义。包装器不是 Codex 自己的 exe,是系统里固定路径的 /usr/bin/sandbox-exec。PATH 上放一个同名二进制没用。

出处:codex-rs/sandboxing/src/manager.rs 第 62 至 76 行 · codex-rs/sandboxing/src/seatbelt.rs 第 52 至 56 行

策略文本分两层。四份 .sbplinclude_str! 编进二进制,当静态基线。动态段按这一次的可读可写根现拼。拼接顺序固定:基线、读、写、网络在前。全盘可读才加 preferences。受限读才加平台默认路径。祖先的 file-write-unlink 放最后,避免前面更宽的 allow 把 rename 用的 unlink 重新打开。

出处:codex-rs/sandboxing/src/seatbelt.rs 第 21 至 27 行 · codex-rs/sandboxing/src/seatbelt.rs 第 985 至 1012 行

基线第一句业务规则是 (deny default)。没写明的事一律拒绝。子进程继承这份底稿,sandbox-exec 拉起的 bash 再 fork 出来的子孙,仍在同一套规则里。

权限档案 读根 · 写根 · 网络 四份静态 sbpl 动态读写规则 动态网络段 join -p 文本 -D 参数表 sandbox-exec 再 exec 用户命令
教学化结构图:一份权限档案拆成片段,拼成 argv 再交给系统里那个固定路径的包装器。
为什么长期成立

Seatbelt 的内核接口就是吃一份 SBPL 文本。能控的是文本怎么生成。默认拒绝,再按这一次的根往上开门,换语言重写也用得上。片段的顺序是合同:更窄的 deny 必须能压住前面更宽的 allow。

思路二 · 路径走配料表,不进菜单
它解决什么问题

有人把工程放在 ~/work/app (copy) 这种带括号和空格的目录里,或者路径里出现引号。SBPL 里括号、引号、分号都是语法。路径一旦插进策略正文,用户目录名就能变成语法。sandbox-exec 解析失败,模型看到的是包装器错误,不是沙箱拒绝。

思路是什么

动态读写规则只往正文里写 (param "WRITABLE_ROOT_0") 这类键名。真实路径写成 -DKEY=value,成为独立 argv。排除子路径同样走参数表。策略正文和 -D 必须同时出现,测试把这件事锁成合同。

出处:codex-rs/sandboxing/src/seatbelt.rs 第 1022 至 1028 行

路径写进正文 subpath "/tmp/app(copy)" 括号变成语法 包装器解析失败 路径走参数表 正文只写 param WRITABLE_ROOT_0 -D 单独一条 argv value 里可以有引号 语法不被污染
教学化对照:左边把目录名交给解析器,右边只把代号交给解析器。
为什么长期成立

安全策略如果最终是一段文本,用户可控的字符串不要进这段文本。模板只含占位符,真实路径、域名、端口走另一份参数表。和 Seatbelt 语法无关。Python 里同样是模板加 argv。

思路三 · 挖空要挡住节点、子孙和搬家
它解决什么问题

只写 subpath,第一次创建这个目录会漏,mkdir .codex 会成功。元数据保护变成空壳。不加 file-write-unlink,rename 能把只读子树挪出挖空区。下一次若仍按原路径授权,实际写的是链接对面。

思路是什么

排除子路径时,require-not 要写两条。literal 管目录自己,subpath 管下面的内容。两条一起放进 require-all,是并且关系。顺序不敏感,少写 literal 才是。

出处:codex-rs/sandboxing/src/seatbelt.rs 第 548 至 555 行

可写根默认保护三个顶层名字:.git.agents.codex。可写根目录本身再钉一条 deny file-write-unlink。可写根中间若有用户可控的符号链接,拼装直接报错,不会跟着链接走。

出处:codex-rs/protocol/src/permissions.rs 第 24 至 33 行 · codex-rs/sandboxing/src/seatbelt.rs 第 504 至 509 行 · codex-rs/sandboxing/src/seatbelt.rs 第 441 至 450 行

网络是另一段动态文本。要走代理却没有可用端口,返回受限网络段加网络基线,不给 (allow network-outbound) 这种整网放行。网络基线还在,只是没有 blanket outbound。

出处:codex-rs/sandboxing/src/seatbelt.rs 第 309 至 335 行

literal 挡住这个节点 mkdir .codex 目录自己还不存在时 subpath 不一定吃到 subpath 挡住它的子孙 建出来之后的文件 只留 literal 下面的写会漏 unlink 挡住把边界搬走 rm 再 ln -s 放在 sections 最后 避免被更宽的 allow 打开
教学化结构图:三条规则一起生成,少一条就会漏一种绕法。
路径走参数表。挖空要三条一起。
为什么长期成立

挖空一条路径,同时挡住节点本身和它的子孙。再挡住对这个节点的 rename。三条一起生成,用测试锁住形状。跨平台错误文案不同:macOS 常见 Operation not permitted,能被现有关键词表接住。不要假设另外两个平台也是这句话。

出处:codex-rs/sandboxing/src/denial.rs 第 50 至 58 行

横向对比 · 路径进不进这段文本

Claude Code:先放开读,路径 stringify 后进文本

Claude 从 (allow file-read*) 往下加 deny,再在 deny 里 re-allow。后写规则优先。路径用 JSON.stringify 包一层,嵌进策略文本。包装命令从 PATH 里找 sandbox-exec,不写死 /usr/bin。两边都要防 rename,Claude 同样生成 deny file-write-unlink

Codex 从 (deny default) 往上加 allow,路径留在 -D。付的是生成器复杂度,换来用户路径不进入 SBPL 语法。Claude 付的是后写顺序必须按对,换来配置面更接近 allowAllExcept。

两侧均已核对源码 · 2026-08-22

DSH 与 Grok:路径进文本,靠转义或直接失败

DSH 默认 (allow default),再 (deny file-write*),只把写权收回来。路径经 sbplString 转义反斜杠和双引号后,直接嵌进 -p。没有 -D 参数表。sandbox-exec 从 PATH 取。本机 macOS 上做不到「可写根里挖掉 .codex」这一粒度。

Grok 不包一层 sandbox-exec。它把 SBPL 片段交给当前进程。路径照样进文本,控制字符直接失败,避免沙箱报 active、那条路径却没被挡住。内核 apply 失败时继续跑。Codex 在 Seatbelt 准备失败时返回错误,命令不会裸跑。

两侧均已核对源码 · 2026-08-22
课堂练习
01

少一条 require-not,漏在哪里

演示里先打开写根和挖空 .codex,看 mkdir .codex 探针是红的。再打开「只留 subpath」,探针变绿。用自己的话解释:subpath 管的是目录下面,为什么挡不住第一次创建这个节点。对照 seatbelt.rs 第 548 至 555 行的注释即可,不要在真实仓库里执行破坏性命令。

Takeaway:macOS 上的沙箱是一段拼出来的 SBPL。路径走 -D,正文只留占位。挖空要同时挡住节点、子孙和搬家。Seatbelt 挡得住策略里拒绝的操作,挡不住被写宽的 allow,也挡不住模型在可写根里改业务代码。