Codex 源码解析 · 第 17 讲
Codex 源码解析系列 第 17 讲:Shell 命令执行与进程管理——拦截每一次 execve 基于 OpenAI Codex 源码 · 2026-10-11 💡 本讲一句话: Codex 给 zsh 打了一个小补丁,把 shell 里每一次 execve(2) 都变成一次"必须过审批门"的请求——沙箱内放行(Run)、带 stdio 文件描述符转移的沙箱外提权执行(Escalate)、或直接拒绝(Deny)。读完你会明白:AI agent 能跑任意命令,但每一条命令的生死都被卡在了一个 Unix socket 上。 一、为什么要在 execve 层拦截? 第 16 讲我们看了 Unified Exec 怎么把进程当"资源"管理。但有一个更根本的问题还没回答: 模型让 shell 跑一条命令,这条命令在沙箱里执行时,谁来决定它能不能跑、要不要出沙箱? 最直觉的做法是解析命令字符串——Codex 确实有 shell-command/src/parse_command.rs (2766 行)把命令归类成 Read/ListFiles/Search/Unknown。但那是给 摘要和审批提示 用的,不是安全边界:子 shell、 eval 、别名、管道里的嵌套命令都能绕过字符串解析。真正所有路径都绕不过去的咽喉点只有一个: execve(2) 系统调用 ——任何"执行另一个程序"的动作最终都要经过它。 Codex 的选择很激进:不改内核、不用 seccomp,而是 直接给 zsh 打补丁 。补丁只有十几行 C 代码( shell-escalation/patches/zsh-exec-wrapper.patch ),改的是 zsh 的 zexecve() : 📄 shell-escalation/patches/zsh-exec-wrapper.patch(Src/exec.c,第 526-534 行附近) exec_argv = argv; // 默认保持原样:没有 EXEC_WRAPPER 时,zsh 行为和原版完全一致 if ((exec_wrapper = getenv("EXEC_WRAPPER")) && // 读 Codex 注入的 EXEC_WRAPPER 环境变量——拦截开关就藏在这里 *exec_wrapper && !inblank(*exec_wrapper)) { // 必须非空且首字符不是空白:防止空字符串/空格把 exec 劫持成"执行空路径" exec_argv = argv - 2; // 指针回退两个槽位(zsh 内部缓冲区预留了空间),腾出位置构造新 argv exec_argv[0] = exec_wrapper; // 新 argv[0] = wrapper 程序路径,即 codex-execve-wrapper exec_argv[1] = orig_pth; // 原本要执行的目标路径,降级成 wrapper 的第一个参数 pth = exec_wrapper; // execve 的目标从"原命令"换成"wrapper"——劫持完成 } winch_unblock(); // 恢复 SIGWINCH 处理(与拦截无关的原有逻辑,保持不动) execve(pth, exec_argv, newenvp); // 真正执行:设了 EXEC_WRAPPER 时跑的是 wrapper,否则和原版 zsh 一模一样 为什么这样设计: 这个补丁的巧妙之处在于"零配置、可关闭、无感"。没有 EXEC_WRAPPER 时 zsh 行为与原版完全一致( exec_argv = argv 直接短路);有它时,shell 里 每一次 execve——包括 bash -c "rm -rf /" 这种嵌套、子 shell、别名展开后的最终执行——都会先变成对 wrapper 的调用。字符串解析看到的是"外层命令",而 execve 拦截看到的是"最终要执行的程序",后者才是安全语义上正确的粒度。代价是必须用 Codex 自己构建的打过补丁的 zsh( shell-escalation/README.md 里记录了完整的重编译流程,CI 按 tag 发布二进制)。 拦截方案 能看到什么 弱点 命令字符串解析(parse…
阅读全文:Codex 源码解析 第 17 讲