本集对应课程章节:OpenAI Codex · 代码模式(Linux 沙箱分层)
内容来源:xueai.miyang.cn(小山学堂 · 洛小山《学 AI 产品,从入门到精通》),本页为二次演绎的解读与音频稿件版本。
同一条读取:Mac 上回 Operation not permitted,Linux 上回 No such file or directory。
差别不在强度,而在顺序:Linux 先换掉进程能看见的文件树(视图),再收紧它能调用的系统接口(seccomp)。路径常常根本不在挂载里,所以还没轮到系统调用表,内核就已经回了「文件不存在」。
本集回答三件事:
LandlockCommand,默认却走 bubblewrap一句话骨架:能看见什么,和能调用哪些 syscall,是两道墙。先换世界,再收紧调用。
| 维度 | 读完本集你能做到 | 对应证据 |
|---|---|---|
| 两层分工 | 区分「视图挡看见」与「seccomp 挡调用」 | 视图 → No such file;过滤器 → EPERM |
| 顺序理由 | 解释 no_new_privs 前置会如何打挂 setuid bwrap | linux_run_main.rs L117–121、landlock.rs L57–65 |
| 三拍合同 | 复述入口函数头的三步注释 | linux_run_main.rs L152–158 |
| helper 分身 | 说明 argv[0] basename 如何把同一份二进制拐进 helper | launcher.rs L38–39 |
| 内层自检 | 说明 capget 未清零即 panic 的意义 | linux_run_main.rs L216–221、L242–243 |
| 失败策略 | 说明 never falls back 与 WSL1 预检 | linux_run_main.rs L290–294、manager.rs L413–420 / L696–710 |
| 横向差异 | 对比 DSH 与 Grok 的三拍放法与失败策略 | DSH sandbox-local、landlock-run;Grok config/mod.rs L1293–1320 |
反直觉的失败:若先打开 PR_SET_NO_NEW_PRIVS 再调系统自带的 bubblewrap,不少发行版上的 setuid 二进制会直接起不来 —— 沙箱在装得最全的机器上反而失败。
原因:no_new_privs 会挡住 setuid 提权,而系统自带的 bwrap 常常正是 setuid 包装器。必须先让它靠提权把命名空间建好,再在自己身上收紧。
no_new_privs:seccomp 的前置条件,同时挡住 setuid 提权默认路径 = 两次进入同一份 helper
| 拍 | 动作 |
|---|---|
| 外层 | 只拼 bubblewrap,文件系统默认只读,再叠可写根;.git / .agents / .codex 即使落在可写根里也保持只读 |
| 内层 | 打开 no_new_privs、装网络 seccomp,然后 fork,把进程镜像让给用户命令 |
字段注释把原因直接写在旗标上:bubblewrap 可能依赖 setuid,必须先建好视图,再收紧 —— linux_run_main.rs L117–121、landlock.rs L57–65。
沙箱在「装得最全的机器」上失败时,先查收紧动作是否早于包装器执行。顺序反了的表现是:工具自身起不来,而不是目标命令被拦。
出处:codex-rs/linux-sandbox/src/linux_run_main.rs L152–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,commit 4f39251a01,核对日期 2026-08-22。这三步就是默认路径的合同。
两层各挡什么
| 层 | 挡的是 | 表现 |
|---|---|---|
| 视图 | 看见 | 路径不在挂载里 → No such file or directory,此时还没轮到系统调用表 |
| seccomp | 调用 | 默认放行,命中 connect / bind / ptrace 等规则才回 EPERM;已经看不见的文件它不管 |
同一探针换一下,停的位置就变
/etc/shadow → 停在视图层,文案 No such fileconnect → 穿过视图,停在过滤器,文案 EPERM判断一次拦截发生在哪一层,看错误文案就够了:No such file 是视图层,EPERM 是 seccomp 层。这比读日志快得多。
为什么不能让主 CLI 自己 unshare + exec:失败会把整次会话带走,且没法在 PID namespace 里留下一个收孤儿的 1 号进程 —— 超时和取消会留下睡眠进程。
做法:靠 argv[0] 的 basename 分身
| 机制 | 内容 |
|---|---|
| 别名拐入 | 文件名已是 codex-linux-sandbox 则保留,否则改为该别名,把同一份二进制拐进 helper |
| 外层 exec 目标 | 不是 ls,而是 helper 自己,带 --apply-seccomp-then-exec |
| 1 号进程 | launcher 插入 --as-pid-1,让 bubblewrap 自己当命名空间里的 1 号进程 |
出处:launcher.rs L38–39、linux_run_main.rs L1520
精髓:同一份二进制进了两次,中间隔着 bubblewrap 建好的世界 —— 第一次为建世界,第二次为在已建好的世界里把自己收死。
跨平台一致性:Windows 看隐藏参数,Linux 看 argv[0]。包装器失败时,固定退出码 + 固定 stderr 前缀更好自动归因。
| 步骤 | 内容 | 出处 |
|---|---|---|
| 1 | capget:effective 或 permitted 任一非零 → 立即 panic,命令不跑 | linux_run_main.rs L216–221 |
| 2 | 过检后 fork;子进程 execvp | linux_run_main.rs L1565 |
| 3 | 父进程 waitpid(-1) 收掉子孙,退出码原样传出 | linux_run_main.rs L242–243 |
父进程为何必须留下:命名空间里它是 1 号进程,孤儿会被交给它收养。没有这个人,超时与取消就会留下没人收的睡眠进程。
一句话概括:进程让出去之后,收尸的人还在。
常见误解:类型名还叫 LandlockCommand,crate 目录叫 linux-sandbox,于是不少资料写成「Codex 在 Linux 上用 Landlock」。当前默认的文件系统沙箱已经是 bubblewrap。
Deprecated,默认关失败策略:外层写明 never falls back —— bubblewrap 失败就是失败,不静默换 Landlock。
为什么这条关键:若静默换 Landlock,模型会按「我被关在视图里」规划下一步,实际却还看得见主机文件树 → 下一步行为分叉,且最难排查。
具体情况
| 场景 | 行为 | 出处 |
|---|---|---|
| WSL1 且需要 bwrap | transform 返回 Wsl1UnsupportedForBubblewrap,命令不进 helper | manager.rs L413–420 / L696–710 |
| 全盘可写且不走代理 | 根本不会进 bwrap,WSL1 可以过 | 同上 |
| user namespace 建不出 | 给启动警告,不改走更弱后端 | features/src/lib.rs L1064–1069 |
| bwrap 失败 | never falls back | linux_run_main.rs L290–294 |
三台机器不是同一道墙:本机有新 bwrap、CI 容器禁了 user namespace、同事还在 WSL1。产品文案与给模型的说明必须按实际启用的那一层来写,不能用「我们有 Linux 沙箱」一把覆盖。
| 项目 | 结构 | 失败策略 |
|---|---|---|
| DeepSeek Harness | Linux 链写死为 bwrap 再 landlock。bwrap 档 = 视图 + exec,没有 seccomp;探不过才走 landlock-run,在自己身上装规则再 exec。文件系统限制与 exec 合在同一份 main 里 | 启动器失败退出码 125 + landlock-run: 致命行,便于自动归因;两条链都探失败则拒绝裸跑,可换到探测过的第二档,换完仍 fail closed |
| Codex | 三拍:bwrap 视图 → 进程内 no_new_privs + seccomp → execvp。同一份 helper 进两次 | helper 失败为 panic 或原样 wait status;默认路径失败不回退 Landlock |
| Grok | 顺序理由与 Codex 相同,但三拍打在 agent 进程上,命令只继承结果。视图为补丁式 --bind / / 再盖 deny 路径,主机树还在;子进程 spawn 时若需限网才在 pre_exec 装 seccomp | bwrap exec 失败且不要求 read-deny 时警告并回退 Landlock;内置 Profile 的 apply 失败记警告并继续,is_active() 才是真状态 |
共同点:两边都走独立辅助进程;都承认「先让可能依赖 setuid 的 bwrap 跑完,再收紧」这条工程理由。
关键差异:失败之后谁负责降级 —— Codex never falls back,Grok 可以警告后回退,DSH 换档但 fail closed。
Linux 沙箱改动或排查时逐项自查:
.git / .agents / .codex 是否即使在可写根里也保持只读argv[0] basename;--as-pid-1 是否插入capget 未清零时是否 panic(而非继续跑)waitpid(-1) 收掉子孙,退出码是否原样传出No such file)与 seccomp 层(EPERM)把探针留在读 /etc/shadow:
UnsupportedOperationconnect 外网 —— 为什么默认路径会穿过视图停在 seccomp,文案从 No such file 变成 EPERMopenai/codex,核对文件 codex-rs/linux-sandbox/src/linux_run_main.rs,commit 4f39251a01,核对日期 2026-08-22;代码块保留源码原文。PLATFORM_CHAINS 与 landlock-run(2026-08-22);Grok 依据 bwrap_reexec_for_profile 与 apply(2026-08-22)。Linux 默认路径是三拍:先用 bubblewrap 换文件系统视图,再在已经进命名空间的 helper 里上 seccomp,最后才 exec。顺序反了,setuid 的系统 bwrap 会起不来。类型名还叫 Landlock,那是兼容,不是现行默认。进不去就在 spawn 之前拒绝,失败不换更弱的后端。
来源:xueai.miyang.cn(小山学堂 · 洛小山《学 AI 产品,从入门到精通》)
本页为二次演绎的解读与音频稿件版本,音频与本页配套发布。