Codex 的沙箱机制:从内核原语到工程实现
约 7093 字大约 24 分钟
2026-10-12
概述
AI 编码 Agent 与传统工具最本质的区别是:它执行的是模型生成的、未经审查的任意代码。npm install 可能带 postinstall 脚本,curl | bash 可能是模型幻觉出的地址,一个"修复测试"的动作可能是 rm -rf 的变体。
权限确认弹窗是最直觉的防线,但它有两个问题:决策疲劳(用户点几十次"允许"后就不再看了)和不可靠(用户看不懂那条命令到底做了什么)。
沙箱的思路是换个方向——不判断命令是否危险,而是限制它即使危险也造不成伤害。这就是"内核级沙箱"要解决的问题:让被执行的命令在操作系统层面就够不到不该够的东西。
Codex 在这条路上走得比同类工具深。本文分两层讲:先讲它所依赖的内核机制(Landlock / seccomp / bubblewrap / Seatbelt),再讲 Codex 具体怎么把它们组装起来。
可信度说明:内核机制部分引自
docs.kernel.org、man 手册、Microsoft Learn 等官方文档;Codex 实现部分引自github.com/openai/codex的main分支源码快照(2026-10)与 issue tracker。源码路径与函数名均为实际核对结果。个别无法核实的点已显式标注「未确认」。
为什么要"内核级"
软件层面的沙箱(比如拦截 child_process 调用、正则匹配危险命令)天然不可靠——只要绕过那一层,就完全没有约束。内核级沙箱的约束点在系统调用边界:进程要读写文件、要建 socket,都必须经过内核,内核在那里做检查。
这带来两个性质:
- 不可绕过:除非能找到内核本身的漏洞(沙箱逃逸),否则用户态代码无法突破
- 零特权:现代 Linux 的非特权沙箱机制允许任意普通用户给自己上锁,不需要 root
代价是:内核漏洞即为逃逸。这也是为什么真正的强隔离场景最终要走向虚拟机(见后文对比)。
核心原理
文件系统控制:Landlock
Landlock 是 Linux 5.13 引入的 LSM(Linux Security Module),内核文档对它的定位是:
Landlock's goal is to create scoped access-control (i.e. sandboxing).
Any user can enforce Landlock rulesets on their processes.
它和传统访问控制的关键差异:
| DAC(uid/gid + mode 位) | MAC(SELinux / AppArmor) | Landlock | |
|---|---|---|---|
| 谁来定义 | 文件所有者 | 特权方(启动时/策略加载) | 进程自己 |
| 作用对象 | 所有进程 | 其他进程 | 自身及其后代 |
| 是否需要特权 | 否 | 需要 | 否 |
| 能否放松限制 | — | — | 不能,只能收紧 |
内核文档对"只能收紧"这条有明确表述:
A Landlock rule shall not interfere with other access-controls enforced on the system, only add more restrictions.
这个单调收紧(intersection)语义是它安全的基础——一个被沙箱的程序无法通过叠加新规则来放宽自己。
调用序列(来自 userspace-api/landlock.html):
// 1) 建一个空规则集,声明"我要管辖哪些权限位"
long landlock_create_ruleset(const struct landlock_ruleset_attr *attr,
size_t size, __u32 flags);
// 2) 往规则集里逐条加规则
long landlock_add_rule(int ruleset_fd, enum landlock_rule_type rule_type,
const void *rule_attr, __u32 flags);
// 3) 把规则集施加到当前线程及后代
long landlock_restrict_self(int ruleset_fd, __u32 flags);"只允许写 /tmp、其他一律只读"是这么表达的:
- 在
landlock_ruleset_attr.handled_access_fs里声明接管的权限位(READ_FILE | READ_DIR | WRITE_FILE | REMOVE_FILE | ...) - 对
/调landlock_add_rule,allowed_access只给读相关位 - 对
/tmp再调一次,这次给出写相关位 landlock_restrict_self
未被任何规则覆盖的路径,落在 handled_access_fs 里的权限一律拒绝——默认拒绝是这条路线的核心。
一个易踩的坑:LANDLOCK_ACCESS_FS_REFER(rename / link 控制)是唯一一个即使不在 handled_access_fs 里也默认被拒绝的权限,必须显式声明才能放行。
前置条件是 no_new_privs:
prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);内核文档说得明确:"For unprivileged processes, setting the no_new_privs attribute is required by Landlock." 不设置的话,setuid / setgid / file-capability 二进制可能成为 confused deputy,所以内核直接拒绝(EPERM)。
no_new_privs 本身的语义也值得记住:一旦设置,跨 fork / clone / execve 继承且无法取消,具体效果是 execve 不再提权(setuid 位失效、file capabilities 不进 permitted 集合)。
Landlock 的主要局限(全部来自官方文档):
- 不覆盖已打开的文件描述符:"Files or directories opened before the sandboxing are not subject to these restrictions."
- 部分 syscall 家族无法限制:官方点名
chdir、stat、flock、chmod、chown、setxattr、utime、fcntl、access - 网络覆盖很晚:ABI v4(约 6.7)才管 TCP bind/connect,v10 才管 UDP;而且需要
CONFIG_INET=y。早期版本完全不管网络 - 层数上限 16:超出返回
E2BIG - 线程作用域:只作用于施加规则的线程及其后代,不含兄弟线程(除非用
TSYNC)
ABI 与内核版本的对应关系:官方 ABI 表只列特性不列内核版本,常见记载的对应是 v1→5.13、v2→5.19、v3→6.2、v4→6.7、v5→6.10、v6→6.12。引用这张表时建议回 include/uapi/linux/landlock.h 复核。
系统调用过滤:seccomp-bpf
seccomp 的 filter 模式做的事情完全不同:它在 syscall 边界上按 BPF 程序做判断,看的是 seccomp_data——即 syscall 号、架构值、按值的参数。
struct sock_fprog prog = { .len = ..., .filter = bpf_insns };
prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog);返回值(action)按优先级从高到低:KILL_PROCESS → KILL_THREAD → TRAP(投递 SIGSYS)→ ERRNO → USER_NOTIF(交给用户态守护进程决定)→ TRACE → LOG → ALLOW。多层过滤器叠加时取优先级最高者。
最关键的一条限制:BPF 程序 "may not dereference pointers"。也就是说过滤器只能看到 openat 的路径字符串地址,看不到路径内容。官方把这个特性当作优点——天然避免了 interposition 框架常见的 TOCTOU 问题——但它直接决定了 seccomp 无法按文件路径做拦截。
官方对 seccomp 的定位说得非常克制:
(seccomp filter) provides a clearly defined mechanism for minimizing the exposed kernel surface
——即它不是沙箱,只是攻击面收缩工具。
还有一个必须注意的安全陷阱:只检查 syscall 号而不检查架构值是不安全的。不同调用约定下 syscall 号可能重叠(x86-64 与 x32 ABI 是经典例子),可以借此绕过过滤。
隔离的骨架:bubblewrap
bubblewrap(bwrap)不是内核机制,而是 Flatpak 生态里的一个用户态工具,把 namespace + bind mount + seccomp 组装成沙箱。它的自述很坦诚:
bubblewrap is not a complete, ready-made sandbox with a specific security policy.
——保护级别完全由调用方传入的参数决定,谁拼命令谁负责安全模型。
它组合的 namespace:
| namespace | 效果 |
|---|---|
| mount | 总是新建,根挂在一个宿主不可见的 tmpfs 上 |
| user | 隐藏除当前 uid/gid 外的所有身份,可重映射 |
| PID | 看不到沙箱外的任何进程,内部有一个 trivial pid1 负责 reap |
| network | 独立 netns,只有 loopback |
| IPC / UTS / cgroup | 各自独立副本 |
关于 no_new_privs,README 里有句话很关键:
bubblewrap uses
PR_SET_NO_NEW_PRIVSto turn off setuid binaries
这堵死了经典 chroot 逃逸路径。历史上 bwrap 有 setuid 模式,上游已移除——现在它依赖非特权 user namespace,因此没有 unprivileged userns 就没有 bwrap 的默认模式。
【社区/源码观察,非官方声明】bwrap 用
pivot_root而非chroot切换根。原因据第三方分析是:自 Linux 3.9 起,在 chroot 环境里调CLONE_NEWUSER会返回EPERM(caller 的 root 与 mount namespace 的 root 不一致),破坏 rootless 嵌套场景。此条以源码注释 + Flatpak wiki 为据,未抓到官方明确声明。
三者的分工
这是理解整套机制的关键,内核文档原话:
A Landlock rule shall be focused on access control on kernel objects instead of syscall filtering, which is the purpose of seccomp-bpf.
翻译成实践规则:
- 要限制"能对哪些内核对象(路径、端口)做什么操作"→ 用 Landlock
- 要限制"哪些整类 syscall 可以调"→ 用 seccomp
- 要造一个隔离的文件系统视图 → 用 namespace / bind mount
因为 seccomp 看不到路径,所以"只允许写 /tmp"这类需求必须靠 Landlock(或 bind mount);而"禁止 mount / ptrace / io_uring_setup 这些整类动作"用 seccomp 更直接。三者互补,不是替代关系。
跨平台实现
Linux:能力最强,也最容易被打断
Linux 是三平台里唯一具备"非特权、自施加、可叠加"全套原语的系统。反过来说,它的沙箱也最容易被环境配置打断。
Ubuntu 24.04 的 AppArmor 陷阱是最典型的例子。Ubuntu 24.04 release notes 原文:
the Ubuntu kernel now restricts the use of unprivileged user namespaces
对应的 sysctl 是 kernel.apparmor_restrict_unprivileged_userns——该特性在 23.10 引入但默认关闭,24.04 默认启用。它拦的是"既非特权又非受限"的程序创建 userns,而 bwrap 恰恰需要 CLONE_NEWUSER 并写 /proc/PID/uid_map、gid_map。
结果就是报错形如:
bwrap: setting up uid map: Permission denied
bwrap: loopback: Failed RTM_NEWADDR
bwrap: No permissions to create a new namespace三类绕法(按最小授权排序):
- 给 bwrap 单独建 AppArmor profile(推荐)然后
abi <abi/4.0>, include <tunables/global> profile bwrap /usr/bin/bwrap flags=(unconfined) { userns, include if exists <local/bwrap> }sudo apparmor_parser -r /etc/apparmor.d/bwrap - 关掉 sysctl(全局,安全性下降):
kernel.apparmor_restrict_unprivileged_userns=0 - setuid bwrap / 修 SUID sandbox 权限:bwrap 上游已移除 setuid 模式,但 Electron 系应用仍靠修
chrome-sandbox权限解决同类问题
注意发行说明明确不推荐的一种做法是给应用加 --no-sandbox——那等于关掉应用自身的安全特性。
受影响面很广:Docker Desktop、AOSP 构建(nsjail)、Yocto、Nautilus 缩略图、Claude Desktop、Codex CLI 都出现过同类报错。
macOS:Seatbelt
macOS 对应的是 Seatbelt(内部代号),通过 sandbox-exec 或已弃用的 sandbox_init() 接口驱动。Apple 的公开文档里只用 "App Sandbox" 这个名字,不用 "Seatbelt"——因为它始终是私有接口。
man sandbox-exec 的标题处直接标着 (DEPRECATED)。但工具链并没有因此迁移,原因是 App Sandbox 只面向"App"(需要 bundle、entitlements、签名),无法用于任意命令行进程。Nix、Chromium 系、各类 agent 工具因此只能继续依赖这个已弃用的接口。
策略语言是 SBPL(Sandbox Profile Language),一个 Scheme 风格的 s-expression DSL:
(version 1)
(deny default) ; 白名单模式
(allow file-read* (subpath "/usr"))
(allow file-write* (subpath (param "WRITABLE_ROOT_0")))
(allow process-exec)
(allow process-fork)两点值得注意:
- 动作只有
allow和deny,(deny default)是白名单、(allow default)是黑名单。引擎"首次显式匹配即停止",所以同一 operation + target 先 deny 再 allow 永远以 deny 生效 - 网络在 SBPL 里是一等公民(
network*分 inbound / outbound)。这是 macOS 相对早期 Landlock 的一个优势——sandbox-exec -n no-internet ping ...会直接报sendto: Operation not permitted
Apple 官方对这类沙箱的定性值得完整引用(man sandbox(7)):
It is not a replacement for other operating system access controls.
Restrictions are generally enforced upon acquisition of operating system resources only. For example, if file system writes are restricted, an application will not be able to open(2) a file for writing. However, if the application already has a file descriptor opened for writing, it may use that file descriptor regardless of restrictions.
"已打开的可写 fd 不受限制"——这条局限和 Linux Landlock 完全同构,是进程级沙箱的共性边界,不是某个平台的实现缺陷。
Windows:为什么特别难
Windows 有一堆安全机制,但没有一个是 Linux 那样的"非特权、自施加、可叠加"沙箱原语:
| 机制 | 能做到什么 | 为什么不够 |
|---|---|---|
| AppContainer | 真实边界:credential / device / file / network / process / window 六类隔离,靠唯一 AppContainer SID + capability SID 做 ACL 检查 | 创建 profile/token 通常需要管理员或安装包上下文,没有"一行调用给自己上锁"的对应物;网络能力是"授给什么就有什么",粒度粗 |
| Job Objects | 资源与生命周期管控(CPU、内存、进程数、UI 限制),KILL_ON_JOB_CLOSE 一键杀干净进程树 | 官方定性是"groups of processes to be managed as a unit"——资源管理,不是访问控制,对"能不能读某个文件"无约束力 |
| Restricted Token | 用 CreateRestrictedToken 降权:移除特权、SID 打 deny-only、给 restricting SIDs 列表;两次访问检查都通过才放行 | 普通应用即可用(不需要 SE_ASSIGNPRIMARYTOKEN_NAME),但只降权不做路径级策略 |
| Mandatory Integrity Control | 完整性级别(low / medium / high / system),"低完整性无法写中等完整性对象,即使 DACL 允许" | 管的是标签层级的"写不向上",不是路径级访问 |
| minifilter | 内核态拦截文件 I/O,是实现文件系统重定向 / 应用虚拟化的底层手段 | 需要已签名的内核驱动 + 管理员安装,开发维护成本极高 |
| WDAC / App Control | 代码完整性准入:只在策略允许时才运行代码 | 只管"能不能运行",不管"运行时能访问什么" |
| Windows Sandbox (WSB) | hypervisor + 独立内核的真正隔离 | 是 VM 级,几秒启动,需 Pro/Enterprise,不支持多实例 |
归纳一下 Windows 难做的五个原因:
- 没有"非特权自施加"原语——Linux 有
no_new_privs+ Landlock + unprivileged userns,任意用户可自锁 - 没有 user namespace——无法像 bwrap 那样零特权组装私有根和 private netns
- 路径级 FS 策略要落到内核驱动——绕不开签名 minifilter
- 网络隔离靠 capability 授权——没有 per-socket / per-端口原语
- 层级要么太轻要么太重——轻的(Job / MIC / 令牌)不管路径访问,重的(WSB)是 VM 级
【广泛记载,建议复核】AppContainer 默认阻断 loopback,需用
CheckNetIsolation.exe LoopbackExempt做例外。
Codex 的具体实现
Codex 的沙箱代码集中在 codex-rs/sandboxing/(跨平台编排)与各平台目录(linux-sandbox/、windows-sandbox-rs/、mxc-sandbox/)。
⚠️ 命名变更提醒:早期资料里的
SandboxMode枚举在本版本已不存在。运行期策略枚举是SandboxPolicy;sandbox_mode仍作为配置键和模型提示词中的字符串存在。老的 TUI 预设名suggest/auto-edit/full-auto在整个仓库中零命中,未发现兼容层。
数据模型
codex-rs/protocol/src/protocol.rs 里的 SandboxPolicy:
#[serde(tag = "type", rename_all = "kebab-case")]
pub enum SandboxPolicy {
/// No restrictions whatsoever. Use with caution.
DangerFullAccess,
/// 只读;network_access 默认 false
ReadOnly { network_access: bool },
/// 进程已在外部沙箱中:允许全盘访问,但遵守网络设置
ExternalSandbox { network_access: NetworkAccess },
/// 同 ReadOnly,外加对 cwd 的写权限
WorkspaceWrite {
writable_roots: Vec<AbsolutePathBuf>,
network_access: bool, // 默认 false
exclude_tmpdir_env_var: bool,
exclude_slash_tmp: bool,
},
}几个值得注意的实现细节:
new_workspace_write_policy()硬编码network_access: false——这就是"workspace-write 默认禁网"的来源has_full_disk_read_access()恒返回true:对应 issue #11316:读限制是 TODO,实际未强制。也就是说pub fn has_full_disk_read_access(&self) -> bool { true }workspace-write下cat /etc/passwd照样能读,只有写被限制了get_writable_roots_with_cwd():默认写入根 = 配置的 roots +cwd+/tmp(Unix)+ macOS 用户的TMPDIRWritableRoot带read_only_subpaths和protected_metadata_names,is_path_writable()会排除只读子路径与受保护元数据名
审批侧是 AskForApproval:
pub enum AskForApproval {
UnlessTrusted, // 序列化为 "untrusted"
#[serde(alias = "on-failure")]
OnRequest, // 默认
Granular(GranularApprovalConfig), // 细粒度开关
Never,
}GranularApprovalConfig 的五个开关(每个是 bool,true = 允许该类审批,false = 自动拒绝而非弹给用户):sandbox_approval(shell 命令)、rules(execpolicy prompt 规则)、skill_approval、request_permissions、mcp_elicitations。
⚠️ 源码与配置层不一致:issue #39973(👍41)指出 0.149.0 起
approval_policy = "untrusted"被移除,配置加载时直接报approval_policy = "untrusted" is no longer supported(移除 PR #39630),但枚举在源码里仍在。这是弱化执行审批边界的实质变更,社区标记为 open。
沙箱与审批是两个正交的旋钮——SandboxPolicy 决定"能力边界",AskForApproval 决定"何时问你",各自独立配置。这个设计比单一 permission mode 表达力强,但也是新手容易混淆的地方(见后文 issue)。
跨平台编排
核心是 codex-rs/sandboxing/src/manager.rs。SandboxManager::transform() 按 SandboxType 分派:
SandboxType | 平台 | 实际做法 |
|---|---|---|
None | — | 原样执行 |
MacosSeatbelt | macOS | 前置 /usr/bin/sandbox-exec |
LinuxSeccomp | Linux | 前置 codex-linux-sandbox helper,传 --arg0 override |
WindowsRestrictedToken | Windows(默认) | 前置 Codex 自身作为 wrapper |
WindowsMxc | Windows(新,实验性) | 走 codex_mxc_sandbox |
另外 codex-rs/process-hardening/ 在 main 之前(用 #[ctor::ctor])做进程加固:禁用 core dump、禁用 ptrace attach、移除 LD_PRELOAD / DYLD_* 等危险环境变量。
Linux 实现
权威说明在 codex-rs/linux-sandbox/README.md:
bubblewrap 做文件系统与 namespace 隔离,seccomp 做 syscall 过滤
Landlock 已降级为 legacy——由 features.use_legacy_landlock 控制,且对受限文件系统策略会直接拒绝(报 "filesystem-restricted execution requires bubblewrap")。这一点常被旧资料说反。
bubblewrap 的组装方式(bwrap.rs):
--ro-bind / / # 默认全盘只读
--bind <root> <root> # 对每个可写根覆盖为可写
--ro-bind <protected> ... # .git / gitdir / .codex 再设回只读
--unshare-user # 独立 user namespace
--unshare-pid # 默认
--unshare-net # 禁网时
--proc /procbwrap 的查找顺序:PATH 上(且在 cwd 之外)的第一个 bwrap → 否则回退到打包自带的 codex-resources/bwrap。bwrap 太老不支持 --argv0 时走兼容路径。WSL1 明确不支持(无法建 userns),WSL2 走正常路径。
启动时有个 500ms 探测(sandboxing/src/bwrap.rs 的 system_bwrap_has_user_namespace_access()),跑一条 --unshare-user --unshare-net --ro-bind / / /bin/true,匹配 userns 失败的几种特征串:
setting up uid map: Permission denied
loopback: Failed RTM_NEWADDR
...seccomp 细节(linux-sandbox/src/seccomp.rs)——分三种模式:
| 模式 | 行为 |
|---|---|
Restricted(默认禁网) | deny connect / accept / bind / listen / sendto / sendmmsg / recvmmsg / getsockopt / setsockopt 等;socket / socketpair 仅当 arg0 == AF_UNIX 时放行,其余返回 EPERM |
ProxyRouted(托管代理) | 允许 AF_INET / AF_INET6(连本地 TCP 桥),AF_UNIX 需显式授权 |
VmSocketRestricted | 仅 deny AF_VSOCK(防 WSL2 通过 interop socket 拉起 Windows 进程) |
所有带 socket 限制的模式都额外 deny io_uring_setup / io_uring_enter / io_uring_register(防止绕过 socket 家族限制),Restricted / ProxyRouted 还 deny ptrace / process_vm_readv / process_vm_writev。
有个细节很能说明工程权衡:Restricted 模式保留了 recvfrom,注释说明是为了让 cargo clippy 之类用 socketpair 管理子进程的工具能正常工作。
macOS 实现
codex-rs/sandboxing/src/seatbelt.rs:
- 调用方式是
/usr/bin/sandbox-exec -p <inline policy> [-DKEY=VALUE ...] -- <command> - 源码里有个明确注释:只信任
/usr/bin下的那个 sandbox-exec,因为 PATH 查找会被攻击 - 策略以
-p内联字符串传递(不是临时文件),路径参数用-D<KEY>=<path>注入
策略文件是编译期内嵌的(include_str!):
const MACOS_SEATBELT_BASE_POLICY: &str = include_str!("seatbelt_base_policy.sbpl");
const MACOS_SEATBELT_NETWORK_POLICY: &str = include_str!("seatbelt_network_policy.sbpl");base policy 文件头注释写着 "inspired by Chrome's sandbox policy",主体形态:
(deny default)
(allow process-exec)
(allow process-fork)
(allow signal (target same-sandbox))
(allow process-info* (target same-sandbox))
(allow file-write* (regex #"^/dev/null$")) ; 仅 /dev/null 可写
(allow sysctl-read) ; 放行 hw.* / kern.* 等
(allow iokit-open (iokit-registry-entry-class "RootDomainUserClient"))
(allow ipc-posix-sem) ; python multiprocessing动态部分由 FileSystemSandboxPolicy 生成:对每个可写根产出 (subpath (param "WRITABLE_ROOT_N")),满盘写时退化为 (allow file-write* (regex #"^/")),受保护元数据用 (deny file-write* (regex ...)) 拦回。
profile 分两档:Process 与 FileSystemHelper。网络策略单独一个文件,默认只放行 AF_SYSTEM socket + 一批 mach-lookup(dirhelper、SecurityServer、networkd、ocspd、trustd、DNSConfiguration)——真正的出网规则由 codex-core 按环境注入。
Windows 实现
两条后端加一条新的 MXC 后端。
Unelevated(默认)WindowsRestrictedToken —— windows-sandbox-rs/:
token.rs:CreateRestrictedToken,flags =DISABLE_MAX_PRIVILEGE | LUA_TOKEN | WRITE_RESTRICTED。用的是受限令牌 + ACL,而不是 AppContainerwfp.rs:Windows Filtering Platform 持久过滤器(FwpmFilterAdd0,provider 名"Codex Windows Sandbox WFP"),用于网络隔离acl.rs/deny_read_acl.rs/hide_users.rs/spawn_prep.rs做 ACL 与 setup- 服务化安装:
windows-sandbox-service/+codex-windows-sandbox-setup.exe
有个明确的限制:unelevated 的 WRITE_RESTRICTED 令牌只对写生效——它不支持 capability-SID deny-read ACE 参与读检查,所以分裂式读限制必须用 elevated 后端。
Elevated:能额外强制 deny-read,托管网络(managed network)要求 elevated 后端。
MXC(新,实验性) —— mxc-sandbox/README.md 自述:直接调用 Microsoft 的 MXC BaseContainerRunner,"never invokes MXC's AppContainer dispatcher, edits host ACLs, creates sandbox users, runs setup, or requests elevation"。用 MXC 的 PSE_SUPPORT_FS_DENY 能力做 deny 路径。
README 里明确列出未验证的点:deny glob 沿用旧快照语义、volume-root grant 不递归、命令失败不会回退后端。开关是 windows.sandbox = "mxc" + 默认关闭的 features.prefer_mxc。
未确认:MXC 是否已 GA。README 语气仍是 rollout / experimental,
features.prefer_mxc默认关闭。
网络隔离
workspace-write / read-only 默认禁网,Linux 上是双层实现:
- bubblewrap
--unshare-net(BwrapNetworkMode::Isolated) - seccomp
NetworkSeccompMode::Restricted
即namespace 断网 + socket syscall 拦截双保险,再加上 io_uring / AF_VSOCK / ptrace 的额外拦截。
禁网时还会注入环境变量标记,便于工具链自查:
CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR = "1"
CODEX_SANDBOX_ENV_VAR = "seatbelt" // macOS seatbelt 场景需要联网时走本地代理白名单(codex-rs/network-proxy/):
- 本地 HTTP 代理(默认
127.0.0.1:3128)+ SOCKS5 代理(默认127.0.0.1:8081) - 域名 allow / deny 策略:
[permissions.<profile>.network.domains],支持*.x.com、**.x.com、? - 无 allow 条目时默认全阻断(allowlist-first),且 deny 优先于 allow
mode = "limited"只允许GET/HEAD/OPTIONS(只读网络)- HTTPS MITM:limited 模式或配了
mitm.hooks时自动启用,CA 私钥只在内存 - 阻止响应返回
403+ headerx-proxy-error: blocked-by-allowlist | blocked-by-denylist | blocked-by-method-policy - 安全默认:非 loopback 绑定会被 clamp(除非
dangerously_allow_non_loopback_proxy)
托管网络在 Linux 上走 --managed-network + --proxy-route-spec,内部是"TCP → UDS → TCP 路由桥",隔离 netns 内只允许连到配置的代理端点。
审批闭环:那句 "retry without sandbox?" 从哪来
沙箱拒绝后并不是直接失败,而是一整套升级流程。三个环节:
1. 判定是不是沙箱拒绝(sandboxing/src/denial.rs):
// sandbox != None 且 exit_code != 0 时
// exit_code ∈ {2, 126, 127} → 判为"不像"沙箱拒绝
// LinuxSeccomp 且 exit_code == 128 + SIGSYS(31) → 判为沙箱拒绝另外还有个文本匹配,在输出里找关键词:["operation not permitted", "permission denied", "read-only file system", "seccomp", "sandbox", "landlock", "failed to write file"]。
2. 编排重试(core/src/tools/orchestrator.rs):
fn build_denial_reason_from_output(_output: &ExecToolCallOutput) -> String {
// Keep approval reason terse and stable for UX/tests...
"command failed; retry without sandbox?".to_string()
}这就是那句提示的确切来源。有个重要分支:AskForApproval::Never / OnRequest 下不会重试脱沙箱,而是把沙箱拒绝直接透传给模型。只有需要审批时才会构造 SandboxAttempt 做第二次尝试,并打上 otel.sandbox_outcome(..., "escalated", ...)。
网络被阻断时,审批原因会换成 format!("Network access to \"{}\" is blocked by policy.", host)。
3. 结构化规则(codex-rs/execpolicy/):Starlark 语法定义规则
prefix_rule(pattern=["cmd", ["alt1","alt2"]], decision="prompt",
justification="...", match=[...], not_match=[...])
host_executable(name="git", paths=["/usr/bin/git", ...])Decision 三值:Allow(可直接跑)/ Prompt(需批准;approval_policy="never" 时直接被拒)/ Forbidden(直接阻止)。有效决策取所有命中规则里最严的那个(forbidden > prompt > allow)。匹配时可附带"以后都允许此前缀"的 ExecPolicyAmendment,用户批准一次后就不用反复问。
Guardian:用第二个模型复核
这是 Codex 比较独特的设计——高风险操作交给另一个模型(codex-auto-review)审,而不是弹给用户。
- 配置项
ApprovalsReviewer:User(默认)/AutoReview(别名guardian_subagent) - 路由条件:
AskForApproval::OnRequest或Granular且 reviewer 是AutoReview - 同步执行,常量
MAX_REVIEW_ATTEMPTS = 3、REVIEW_TIMEOUT = 90s,另有连续拒绝熔断器 - 评审覆盖面比"只审 shell"广:
Command/Execve/WriteStdin/ApplyPatch/NetworkAccess/McpToolCall/RequestPermissions - 输出契约:
low-risk: {"outcome":"allow"} else: {"risk_level":"low|medium|high|critical", "user_authorization":"unknown|low|medium|high", "outcome":"allow|deny", "rationale":"..."} - 风险政策是写死的提示词(
prompts/templates/guardian/policy.md):Data Exfiltration(未授权外发敏感数据 → deny)、Credential Probing、Persistent Security Weakening、Destructive Actions(rm -rf、破坏性 git 操作)。里面还有一条很有意思的指令:"不要仅因路径在工作区外就判 high",以及"沙箱重试/升级本身不算可疑"——这是在防止 Guardian 把正常的沙箱升级流程误判成攻击
未确认:
codex-auto-review是独立部署的模型/服务,还是同一 catalogue 内的 slug。源码只把它当 slug 处理;issue #21928 提到在 Azure / Microsoft Foundry 上codex-auto-review is not deployable。
沙箱 vs 容器 vs 虚拟机
进程级沙箱只是隔离方案的一个层次。三者的边界厚度和开销反向相关:
| 隔离边界 | 内核 | 启动开销 | 典型用途 | |
|---|---|---|---|---|
| 进程级沙箱 | 内核里的访问控制决策 | 共享 | 近乎零 | 同机、低延迟执行 LLM 生成的代码 |
| 容器(Docker/OCI) | namespaces + cgroups + capabilities + LSM + seccomp | 仍共享 | ~百毫秒 | 可复现的环境,逃逸面仍是内核与配置缺陷 |
| microVM(Firecracker) | KVM 虚拟化边界 | 独立内核 | boot < 125 ms、< 5 MiB 开销 | 多租户、强隔离、可丢弃 |
Firecracker 官方对自身设计的描述值得引用:
The first layer of isolation is provided by the Linux KVM and the Firecracker virtualization boundary.
Each Firecracker process encapsulates one and only one microVM.
它还有几个值得注意的工程细节:生产环境应只通过 jailer 启动(jailer 负责 cgroup / chroot 设置、drop 权限、再 exec() 进 Firecracker);默认启用 seccomp 过滤;设备模型极简(只有 5 个模拟设备)。官方对威胁模型的态度也很直接:"All vCPU threads are considered to be running malicious code as soon as they have been started."
容器容易和沙箱混淆,本质区别是:容器是"加了配菜的同层机制"——它没有引入新的隔离边界,只是把 namespace / cgroup / LSM / seccomp 打包成一套可复现的配置。而 microVM 换了一层边界(从内核访问控制换成虚拟化边界)。
已知问题与边界
沙箱的工程复杂度体现在 issue 列表里。以下几类是反复出现的:
Ubuntu 24.04 的 userns 限制(最高频)
- #17525 每次普通编辑都要跳过沙箱,workaround 是加 AppArmor profile
- #14919 / #29908
bwrap: loopback: Failed RTM_NEWADDR - #34207 同一条只读命令(
pwd/find/rg --files)都失败 → 升级重试 → 一轮 15 次审批提示
Full Access 仍然被沙箱 / 仍然弹审批
- #19196 Full Access 下网络调用仍被沙箱
- #29503 选了 Full Access,会话仍以 workspace-write 启动
- #25590 桌面端恢复会话时退回 workspace-write
workspace-write 默认禁网的副作用
安全边界问题
- #11316 Landlock 读限制未强制(与源码
has_full_disk_read_access() == true互相印证) - #39973(👍41)移除
untrusted审批档未做弃用过渡,弱化了执行审批边界 - #52809 标记 CRITICAL:VS Code 扩展忽略显式只读沙箱配置,未经批准执行命令并修改文件
三个需要记住的结构性边界
- 读限制目前是空的——
workspace-write只限制写,读是 TODO - 已打开 fd 不受限——这是进程级沙箱的共性(Linux 和 macOS 官方文档都明说),不是实现 bug
- Windows 的读限制需要 elevated 后端——unelevated 的受限令牌只对写生效
实践要点
如果你在用 Codex:
- Ubuntu 24.04 上先确认 bwrap 能不能建 userns。跑一条
bwrap --unshare-user --unshare-net --ro-bind / / /bin/true就知道了 - 沙箱反复失败时优先修环境(AppArmor profile),而不是直接切 Full Access——后者会把整套机制关掉
- 理解沙箱与审批是两个旋钮:想"少问但仍有边界"应该调
AskForApproval,而不是关沙箱 .git只读导致的git add失败是已知问题,需要显式配置可写根
如果你在设计 Agent 的沙箱:
- 区分"限制对象"和"限制调用"——路径级策略和 syscall 级策略是两套东西,需要组合使用
- 默认拒绝比黑名单可靠——
(deny default)/ Landlock 的未匹配即拒,比枚举危险命令可靠得多 - 注意"已打开 fd"这条边——如果你把 fd 传进沙箱,沙箱的写限制就失效了
- 为沙箱失败设计降级路径——Codex 的"拒绝 → 判定 → 升级审批"闭环比"直接失败"体验好得多,但要注意别变成"每次都绕沙箱"
- 考虑第二模型复核——Guardian 用便宜的模型审高风险操作,比让用户点几十次确认有价值得多;但提示词里要明确"沙箱升级本身不可疑",否则会误判
延伸阅读
内核机制(官方)
- Landlock 用户态接口:https://docs.kernel.org/userspace-api/landlock.html
- Landlock 设计文档:https://docs.kernel.org/security/landlock.html
- Landlock 审计:https://docs.kernel.org/admin-guide/LSM/landlock.html
- seccomp filter:https://docs.kernel.org/userspace-api/seccomp_filter.html
- no_new_privs:https://docs.kernel.org/userspace-api/no_new_privs.html
- bubblewrap:https://github.com/containers/bubblewrap
- Ubuntu 24.04 release notes(userns 限制):https://discourse.ubuntu.com/t/noble-numbat-release-notes/39890
macOS(官方 + 经典逆向资料)
man sandbox(7)/man sandbox-exec(1)/man 3 sandbox_init- App Sandbox(现行官方路径):https://developer.apple.com/documentation/security/app-sandbox
- Apple's Sandbox Guide v1.0(Blazakis,SBPL 最详细的公开参考):https://reverse.put.as/wp-content/uploads/2011/09/Apple-Sandbox-Guide-v1.0.pdf
Windows(官方)
- AppContainer 隔离:https://learn.microsoft.com/en-us/windows/win32/secauthz/appcontainer-isolation
- Job Objects:https://learn.microsoft.com/en-us/windows/win32/procthread/job-objects
- Restricted Tokens:https://learn.microsoft.com/en-us/windows/win32/secauthz/restricted-tokens
- Mandatory Integrity Control:https://learn.microsoft.com/en-us/windows/win32/secauthz/mandatory-integrity-control
- Filter Manager(minifilter):https://learn.microsoft.com/en-us/windows-hardware/drivers/ifs/filter-manager-concepts
- Windows Sandbox:https://learn.microsoft.com/en-us/windows/security/application-security/application-isolation/windows-sandbox/
Codex 源码(github.com/openai/codex,main 分支)
- 策略枚举:
codex-rs/protocol/src/protocol.rs - 跨平台编排:
codex-rs/sandboxing/src/manager.rs - Linux 说明:
codex-rs/linux-sandbox/README.md - Linux seccomp:
codex-rs/linux-sandbox/src/seccomp.rs - macOS 策略:
codex-rs/sandboxing/src/seatbelt_base_policy.sbpl - 网络代理:
codex-rs/network-proxy/README.md - execpolicy:
codex-rs/execpolicy/README.md - 审批重试:
codex-rs/core/src/tools/orchestrator.rs - Guardian 策略提示词:
codex-rs/prompts/templates/guardian/policy.md
隔离层次对比
- Firecracker 设计文档:https://github.com/firecracker-microvm/firecracker/blob/main/docs/design.md
