OpenAI Codex · 代码模式

Linux:先建视图,再上 seccomp,最后 exec

同一条读取,Mac 上回 Operation not permitted,Linux 上回 No such file or directory。差别在于 Linux 先换进程能看见的文件树,再收紧它能调用的系统接口。

课程目标读完能说清三件事。Linux 默认路径为什么分两拍:先用 bubblewrap 建文件系统视图,再上 seccomp。为什么要单独起一个 helper。为什么类型名还叫 LandlockCommand,默认却走 bubblewrap。
先玩一遍 · 一个请求穿过两层
换探针、拨开关,看请求在哪一层停下
探针
开关
先看默认路径上读 /etc/shadow 停在视图。再换成 connect,同一条路会走到过滤器。
这次请求
open /etc/shadow
当前路径
默认两阶段
第一层 · 预检待命
transform 先问 helper 在不在、是不是 WSL1、能不能建 user namespace。进不去就在这里停。
第二层 · 视图待命
bubblewrap 先换文件树。默认只读,可写根再叠上去。不在挂载里的路径,后面会报文件不存在。
第三层 · seccomp待命
视图建好之后,才打开 no_new_privs、装网络过滤器。命中规则返回 EPERM。
点播放,看这个请求依次穿过两层。
逻辑轨迹 · 动画每一步对应源码里的哪一段
  1. 读入权限档案并做 WSL1 预检manager.rs L413
  2. 拼出 helper argv,加上权限档案 JSONlandlock.rs L23
  3. 外层 run_main 决定走 bwrap 还是捷径linux_run_main.rs L152
  4. 拼内层命令,带上 --apply-seccomp-then-execlinux_run_main.rs L1520
  5. bwrap 按顺序 ro-bind、bind、unsharebwrap.rs L306
  6. exec bwrap 时插入 --as-pid-1launcher.rs L39
  7. 内层 capget,确认能力已清零linux_run_main.rs L216
  8. 打开 PR_SET_NO_NEW_PRIVS,然后装 seccomplandlock.rs L61
  9. fork 用户命令,父进程 waitpid 收孤儿linux_run_main.rs L243
  10. execvp 接管进程镜像linux_run_main.rs L1565
点播放,看这个请求依次穿过两层。
视图挡的是看见路径不在挂载里,内核回 No such file or directory。这一层还没轮到系统调用表。
seccomp 挡的是调用过滤器默认放行,命中 connect、bind、ptrace 这类规则才回 EPERM。已经看不见的文件,它不管。
进不去就明说WSL1 或缺 user namespace 时,命令停在 transform。默认路径失败不会悄悄换成 Landlock。
教学示意:路径与探针为课程化设定。逻辑轨迹右侧行号对应 openai/codex 仓库 commit 4f39251a01。
思路一 · 先换它能看见的世界,再收紧调用
它解决什么问题

同事在 Mac 上跑 Codex,agent 去读 ~/.ssh/id_rsa,Seatbelt 立刻回 Operation not permitted。同一份仓库放到 Linux 笔记本上,常见文案变成 No such file or directory。路径常常根本不在挂载里。

如果先打开 PR_SET_NO_NEW_PRIVS,再去调系统自带的 bwrap,不少发行版上的 setuid 二进制会直接起不来。沙箱在装得最全的机器上反而失败。seccomp 是一套内核过滤器,用来规定进程还能调用哪些系统接口;no_new_privs 是它的前置条件,也会挡住 setuid 提权。

思路是什么

默认路径两次进入同一份 helper。外层只拼 bubblewrap,把文件系统换成默认只读,再叠可写根,.git.agents.codex 即使落在可写根里也保持只读。内层才打开 no_new_privs、装网络 seccomp,然后 fork、把进程镜像让给用户命令。

函数头把顺序写成三步:

codex-rs/linux-sandbox/src/linux_run_main.rs第 152 至 158 行
/// Entry point for the Linux sandbox helper.
///
/// The sequence is:
/// 1. When needed, wrap the command with bubblewrap to construct the
///    filesystem view.
/// 2. Apply in-process restrictions (no_new_privs + seccomp).
/// 3. `execvp` into the final command.
源码快照说明:依据本地仓库 openai/codex,核对文件 codex-rs/linux-sandbox/src/linux_run_main.rs,commit 4f39251a01,核对日期 2026-08-22。代码块保留源码原文,这三步就是默认路径的合同。

字段注释把原因写在旗标上:bubblewrap 可能依赖 setuid,必须先建好视图,再收紧。出处:codex-rs/linux-sandbox/src/linux_run_main.rs 第 117 至 121 行;codex-rs/linux-sandbox/src/landlock.rs 第 57 至 65 行

外层 helper 拼 bubblewrap 旗标 --ro-bind 再 --bind 先换文件树 还不碰 no_new_privs 内层 helper capget 确认能力为零 no_new_privs 加 seccomp 再收紧系统调用 带 --apply-seccomp-then-exec execvp 用户命令 接手进程
教学化结构图:同一份二进制进两次,中间隔着 bubblewrap 建好的世界。
为什么长期成立

能看见什么,和能调用哪些 syscall,是两道墙。先换世界,再收紧调用。换成别的语言重写,发行版上只要还有 setuid 包装器,这个顺序就还成立。

思路二 · 单独起 helper,留下一个收孤儿的人
它解决什么问题

bubblewrap 和 seccomp 必须发生在即将变成用户命令的那个进程里。主 CLI 自己 unshare 再 exec,失败会把整次会话带走,也没法在 PID namespace 里留下一个收孤儿的 1 号进程。超时和取消会留下睡眠进程。

思路是什么

Linux 看 argv[0] 的 basename。文件名已经是 codex-linux-sandbox 就保留,否则改成这个别名,把同一份二进制拐进 helper。外层 exec 的不是 ls,是 helper 自己,带着 --apply-seccomp-then-exec。launcher 还会插进 --as-pid-1,让 bubblewrap 自己当命名空间里的 1 号进程。

内层先 capget。effective 或 permitted 任一非零,立刻 panic,命令不会跑。过了这道检查,才 fork。子进程 execvp,父进程 waitpid(-1) 把子孙都收掉,退出码原样传出。出处:codex-rs/linux-sandbox/src/linux_run_main.rs 第 216 至 221 行、第 242 至 243 行;codex-rs/linux-sandbox/src/launcher.rs 第 38 至 39 行

SandboxManager 外层 helper 拼内层 argv bubblewrap --as-pid-1 内层 fork 出命令 子进程 execvp 父进程留下 waitpid 收孤儿 取消和超时靠还在的那一个 1 号进程
教学化时序图:进程让出去之后,收尸的人还在。
为什么长期成立

独立辅助进程加先包装、再收紧,是跨平台同一类自我调用。Windows 看隐藏参数,Linux 看 arg0。包装器失败时,固定退出码加固定 stderr 前缀更好自动归因。

思路三 · 默认不是 Landlock,失败也不回退
它解决什么问题

类型名还叫 LandlockCommand,crate 目录也叫 linux-sandbox,网上不少资料会写成 Codex 在 Linux 上用 Landlock。当前默认文件系统沙箱已经是 bubblewrap。Landlock 是显式打开的 legacy 回退,特性开关标成 Deprecated,默认关。legacy 吃不下受限读,也吃不下拆分出来的嵌套只读。

如果 bwrap 失败后静默换 Landlock,模型会按「我被关在视图里」规划,实际却还看得见主机文件树。下一步就会分叉。

思路是什么

外层写明 never falls back。bubblewrap 失败就失败。WSL1 在需要 bubblewrap 时,transform 返回 Wsl1UnsupportedForBubblewrap,命令不会进 helper。全盘可写且不走代理时,根本不会进 bwrap,WSL1 可以过。user namespace 建不出来给启动警告,不会改走更弱的后端。出处:codex-rs/linux-sandbox/src/linux_run_main.rs 第 290 至 294 行;codex-rs/sandboxing/src/manager.rs 第 413 至 420 行、第 696 至 710 行;codex-rs/features/src/lib.rs 第 1064 至 1069 行

本机发行版 有 user namespace 系统 bwrap 或打包二进制 默认两阶段 CI 容器 禁了 user namespace 不会静默改走 Landlock 启动警告,硬失败 WSL1 建不出 user namespace 需要 bwrap 的命令进不去 transform 阶段拒绝
教学化结构图:都叫 Linux 沙箱,三台机器启用的那一层并不一样。
看见和调用是两道墙。进不去就明说,不要合成一句已经沙箱了。
为什么长期成立

跨平台能力不对等时,最容易犯的错是用「我们有 Linux 沙箱」覆盖所有 Linux。本机有新 bwrap、CI 容器禁了 user namespace、同事还在 WSL1,三台机器不是同一道墙。产品文案和模型说明必须按实际启用的那一层来写。

横向对比 · 三拍拆开之后,各自漏什么

DeepSeek Harness:探得过就换档,合成一拍

DSH 的 Linux 链写死为 bwraplandlock。bwrap 档是视图加 exec,没有 seccomp。探不过才走 landlock-run:在自己身上装规则,再 exec 目标命令。文件系统限制和 exec 合在同一份 main 里。这条路径没有 setuid bwrap,所以不必把过滤器再拆到下一拍。

两边都走独立辅助进程。DSH 启动器失败是 125,还要同时看到 landlock-run: 致命行,更好自动归因。Codex helper 失败是 panic 或原样 wait status。两条链都探失败,DSH 拒绝裸跑。它可以换探测过的第二档,换完仍 fail closed。Codex 默认路径失败不回退 Landlock。出处:packages/sandbox/sandbox-local/src/index.ts 第 159 至 166 行;native/landlock-run/packages/entry/src/main.c 第 254 至 261 行

已核对 DSH PLATFORM_CHAINS 与 landlock-run · 2026-08-22 · DSH · 沙箱

Grok:三拍打在 agent 上,失败可以回退

Grok 的顺序和 Codex 同一条工程理由:先让可能依赖 setuid 的 bwrap 跑完,再在已经进命名空间的进程里收紧。差别在主体。Grok 把三拍打在 agent 进程上,命令只继承结果。视图是补丁式的 --bind / / 再盖 deny 路径,主机树还在。子进程 spawn 时,若需要限网,才在 pre_exec 里装 seccomp。

bwrap exec 失败且不要求 read-deny 时,它警告一句,回退到 Landlock。内置 Profile 的 apply 失败记警告并继续,is_active() 才是真状态。Codex 外层写明 never falls back。出处:crates/codegen/xai-grok-shell/src/config/mod.rs 第 1293 至 1320 行;crates/codegen/xai-grok-sandbox/src/lib.rs 第 181 至 192 行

已核对 Grok bwrap_reexec_for_profile 与 apply · 2026-08-22 · Grok · 五种沙箱 Profile
课堂练习
01

同一条读取,换开关之后卡在哪

把探针留在读 /etc/shadow,先打开 WSL1。写下预测:停在哪一层,文案是什么。再关掉 WSL1,打开 legacy Landlock,看受限读会不会变成 UnsupportedOperation。

进阶一问:探针换成 connect 外网。为什么默认路径会穿过视图、停在 seccomp,文案从 No such file 变成 EPERM。

Takeaway:Linux 默认路径是三拍:先用 bubblewrap 换文件系统视图,再在已经进命名空间的 helper 里上 seccomp,最后才 exec。顺序反了,setuid 的系统 bwrap 会起不来。类型名还叫 Landlock,那是兼容,不是现行默认。进不去就在 spawn 之前拒绝,失败不换更弱的后端。