第10章:沙箱 —— 四个操作系统四套实现
约 9 分钟 · 更新于 2026-09-01
第10章:沙箱 —— 四个操作系统四套实现
Pi 明确不做沙箱,dsh 把沙箱做成可换后端的 seam。Codex 的做法是把三个操作系统的内核隔离机制各实现一遍,加起来 37903 行。本章讲这些机制各自能做什么、不能做什么,以及一个统一策略如何变换成三种完全不同的东西。
一、统一的入口:四种沙箱类型
rust
pub enum SandboxType {
None,
MacosSeatbelt,
LinuxSeccomp,
WindowsRestrictedToken,
}
[源码 sandboxing/src/manager.rs:38]
对应的代码分布:
| 平台 | crate | 行数 | 机制 |
|---|
| macOS | sandboxing/src/seatbelt.rs + 4 个 .sbpl | 1036 + 策略文件 | Seatbelt(sandbox-exec) |
| Linux | linux-sandbox/ | 9694 | bubblewrap(文件系统)+ seccomp(网络)+ Landlock(备用) |
| Windows | windows-sandbox-rs/ | 19852 | 受限令牌 + ACL |
| 通用抽象 | sandboxing/ | 8357 | 策略变换、违规记录、拒绝判定 |
Windows 的实现比 Linux 和 macOS 加起来还大。 这不是偶然——后面会讲。
二、macOS:Seatbelt
2.1 策略是一份 Scheme 方言
macOS 的沙箱策略语言叫 SBPL(Sandbox Profile Language),语法是 Scheme。Codex 把基础策略直接写成文件编译进二进制 [源码 sandboxing/src/seatbelt.rs:21]:
rust
const MACOS_SEATBELT_BASE_POLICY: &str = include_str!("seatbelt_base_policy.sbpl");
const MACOS_SEATBELT_NETWORK_POLICY: &str = include_str!("seatbelt_network_policy.sbpl");
const MACOS_SEATBELT_PREFERENCES_POLICY: &str = include_str!("seatbelt_preferences_policy.sbpl");
const MACOS_RESTRICTED_READ_ONLY_PLATFORM_DEFAULTS: &str =
include_str!("restricted_read_only_platform_defaults.sbpl");
基础策略开头就交代了出处 [源码 sandboxing/src/seatbelt_base_policy.sbpl:1]:
scheme
(version 1)
; inspired by Chrome's sandbox policy:
; https://source.chromium.org/chromium/chromium/src/+/main:sandbox/policy/mac/common.sb
; https://source.chromium.org/chromium/chromium/src/+/main:sandbox/policy/mac/renderer.sb
; start with closed-by-default
(deny default)
; child processes inherit the policy of their parent
(allow process-exec)
(allow process-fork)
(allow signal (target same-sandbox))
(allow process-info* (target same-sandbox))
(allow file-write-data
(require-all
(path "/dev/null")
(vnode-type CHARACTER-DEVICE)))
(allow sysctl-read
(sysctl-name "hw.activecpu")
(sysctl-name "hw.byteorder")
…
三件事值得学:
① (deny default) —— 默认全禁,然后逐条放行。 这是唯一正确的沙箱写法。反过来(默认放行、逐条禁止)永远会漏。
② 抄 Chrome。 浏览器沙箱是这个星球上被攻击得最多、也因此最成熟的沙箱。在安全领域抄成熟方案是美德,不是偷懒。
③ sysctl-read 白名单具体到每一个名字:hw.activecpu、hw.byteorder、hw.cachelinesize_compat……这些是编译器、运行时、包管理器要读的机器信息。少一个就有某个工具跑不起来。
这份列表是被 bug 报告喂出来的,不是设计出来的。
2.2 一个防御性细节
rust
/// 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";
[源码 sandboxing/src/seatbelt.rs:55]
不查 PATH,写死绝对路径。 因为 PATH 是攻击者可控的——如果沙箱启动器本身能被替换,沙箱就等于没有。
后半句"如果 /usr/bin/sandbox-exec 被改了,那攻击者已经有 root 了"是一句正确的威胁模型边界声明:明确说出"这个防御到此为止,再往里不归我管"。
可迁移的判断 ⑰
安全相关的外部程序一律写死绝对路径,并在注释里写清楚威胁模型的边界。
第二句和第一句一样重要。没有边界声明的安全代码会让后来的人产生错误的安全感,或者做过度的、无效的加固。
2.3 策略是拼出来的
rust
policy_sections.push(MACOS_SEATBELT_BASE_POLICY.to_string());
if … { policy_sections.push(MACOS_SEATBELT_PREFERENCES_POLICY.to_string()); }
…
return format!("{policy}{MACOS_SEATBELT_NETWORK_POLICY}");
基础策略 + 可选的偏好设置策略 + 可选的网络策略 + 按 writable_roots 生成的写入规则,拼成最终的 SBPL 文本再交给 sandbox-exec。
还有一个 MacosSeatbeltProfile 枚举区分两种档案 [源码 sandboxing/src/seatbelt.rs:30]:Process(跑命令)和 FileSystemHelper(第 9 章说的文件系统助手进程)——读文件和跑命令用不同的沙箱策略,因为需要的权限不一样。
三、Linux:三种机制分工
Linux 的实现最复杂,因为没有一个机制能单独搞定。模块注释说得很清楚 [源码 linux-sandbox/src/landlock.rs:1]:
In-process Linux sandbox primitives: no_new_privs and seccomp.
Filesystem restrictions are enforced by bubblewrap in linux_run_main. Landlock helpers remain available here as legacy/backup utilities.
分工是这样的:
| 机制 | 管什么 | 代码位置 | 状态 |
|---|
| bubblewrap | 文件系统隔离(挂载命名空间) | bwrap.rs(2766 行) | 主力 |
| seccomp | 网络(拦截 socket 系统调用) | landlock.rs | 主力 |
| no_new_privs | 禁止提权(setuid 失效) | landlock.rs | 主力 |
| Landlock | 文件系统(内核 LSM) | landlock.rs | 降级为备用 |
3.1 为什么 Landlock 被降级
Landlock 是 Linux 5.13+ 的内核 LSM,理论上比 bubblewrap 更合适(不需要命名空间、更轻)。但它有两个现实问题:
- 内核版本要求。Landlock 的 ABI 一路在演进(ABI 枚举、CompatLevel 兼容级别都在代码里),老内核上能力不全
- 粒度。Landlock 早期版本管不了某些操作
bubblewrap 用挂载命名空间,把不该看见的路径直接不挂进去——比"能看见但拒绝访问"更彻底,而且行为在各内核版本上更一致。
代价是需要 bwrap 这个外部程序。
3.2 自带 bwrap
Codex 的解法是把 bubblewrap 打包进发行版 [源码 linux-sandbox/src/bundled_bwrap.rs],同时保留查找系统 bwrap 的路径:
rust
pub use bwrap::find_system_bwrap_in_path;
pub use bwrap::system_bwrap_warning;
自带的 bwrap 会做SHA-256 校验 [源码 bundled_bwrap.rs:17-21]:
rust
use sha2::Digest as _;
use sha2::Sha256;
const SHA256_HEX_LEN: usize = 64;
const NULL_SHA256_DIGEST: [u8; 32] = [0; 32];
分发一个会被用来做隔离的可执行文件,就得能证明它没被换过。
仓库里还有个独立的 bwrap crate(151 行)声明了 bin name = "bwrap"——第 2 章讲的独立二进制目标之一。
3.3 seccomp 管网络
rust
/// Apply sandbox policies inside this thread so only the child inherits
/// them, not the entire CLI process.
///
/// This function is responsible for:
/// - enabling `PR_SET_NO_NEW_PRIVS` when restrictions apply, and
/// - installing the network seccomp filter when network access is disabled.
///
/// Filesystem restrictions are intentionally handled by bubblewrap.
pub(crate) fn apply_permission_profile_to_current_thread(…)
[源码 linux-sandbox/src/landlock.rs:34]
注意第一句:"在这个线程内施加策略,这样只有子进程继承它们,而不是整个 CLI 进程"。
这是 Linux 沙箱最关键的一个约束:seccomp 和 no_new_privs 一旦施加就不可撤销。如果在主进程上施加,整个 Codex 就被限制住了,后面所有命令都出不去。所以必须:
- 起一个新线程(或新进程)
- 在那里施加限制
- 从那里 exec 目标命令
这也解释了第 2 章的 arg0 技巧为什么必须存在——codex-linux-sandbox 这个别名调用的就是"新进程里施加限制然后 exec"这条路径。
3.4 WSL1 例外
rust
SandboxTransformError::Wsl1UnsupportedForBubblewrap => {
CodexErr::UnsupportedOperation(crate::bwrap::WSL1_BWRAP_WARNING.to_string())
}
WSL1 不是真的 Linux 内核,没有命名空间。代码里有 is_wsl1() 检测和专门的告警文案。这种"某个环境就是不行"的分支,是真实产品才会有的。
四、Windows:为什么最大
19852 行,比 Linux 和 macOS 加起来还多。原因是 Windows 没有一个现成的"沙箱"原语。
Codex 用的是受限令牌(Restricted Token)+ ACL:
| 文件 | 行数 | 干什么 |
|---|
| setup.rs | 2322 | 沙箱环境准备 |
| acl.rs | 822 | 访问控制列表操作 |
| spawn_prep.rs | 719 | 进程创建前的准备 |
| bin/setup_main/win.rs | 1324 | codex-windows-sandbox-setup 二进制 |
| bin/command_runner/win.rs | 723 | codex-command-runner 二进制 |
| unified_exec/ | ~2000 | Windows 上的进程管理 |
两个独立二进制(第 2 章列过):一个负责管理员级的环境准备,一个负责在沙箱内跑命令。
几个 Windows 特有的复杂度:
- WindowsSandboxLevel —— 分级,不是开关
- 管理员 vs 非管理员两套后端 [源码 sandboxing/src/windows.rs]:windows_sandbox_uses_elevated_backend、resolve_windows_elevated_filesystem_overrides / resolve_windows_restricted_token_filesystem_overrides
- 可能不支持 —— 有一个专门的函数返回"为什么这台机器上用不了" [源码]:
rust
pub use windows::unsupported_windows_restricted_token_sandbox_reason;
- 环境变量白名单 [源码 sandboxing/src/manager.rs:34]:
rust
const WINDOWS_SANDBOX_WRAPPER_SETUP_ENV_ALLOWLIST: &[&str] = &["USERNAME", "USERPROFILE"];
沙箱准备阶段只传两个环境变量
- ssh_config_dependencies —— 连 SSH 配置的依赖都得单独处理
跨平台安全功能的真实成本
如果你要给一个工具加沙箱,预算不能按"实现一次"算。macOS 有现成的 Seatbelt(相对容易),Linux 要组合三种机制(中等),Windows 基本等于从零搭一套(最贵)。
Codex 的 19852 : 9694 : 1036 这个比例,是一个可以拿来做估算的真实数据点。
五、统一策略如何变换成三种东西
用户面对的策略只有第 3 章那四个变体 [源码 protocol/src/protocol.rs:1010]:
rust
pub enum SandboxPolicy {
DangerFullAccess,
ReadOnly { network_access: bool },
ExternalSandbox { network_access: NetworkAccess },
WorkspaceWrite {
writable_roots: Vec<AbsolutePathBuf>,
network_access: bool,
exclude_tmpdir_env_var: bool,
…
},
}
从这四个变体到三套内核机制,中间隔着 policy_transforms.rs(565 行 + 1035 行测试)。它做的事:
text
SandboxPolicy(用户视角)
↓ effective_permission_profile
PermissionProfile(内部规范形式)
↓ to_runtime_permissions
(FileSystemSandboxPolicy, NetworkSandboxPolicy)
↓ 平台分发
SBPL 文本 / bwrap 参数 + seccomp 过滤器 / Windows ACL + 受限令牌
中间那层 PermissionProfile 是关键:它把"用户怎么说"和"内核怎么执行"解耦。第 6 章讲的权限说明模板、第 11 章要讲的审批判定,都是基于这一层,而不是基于平台细节。
ExternalSandbox 这个变体
rust
/// Indicates the process is already in an external sandbox. Allows full
/// disk access while honoring the provided network setting.
ExternalSandbox { network_access: NetworkAccess },
"我已经在别人的沙箱里了"——跑在 Docker 容器、CI runner、云端环境里时用这个。此时再套一层自己的沙箱既没必要(外层已经隔离)又可能失败(容器里往往没有命名空间权限)。
这是一个很实际的变体。很多工具在容器里会因为"沙箱初始化失败"直接不能用。
六、违规记录:沙箱不只是拦截
sandboxing/src/violation.rs(297 行)定义了一套违规事件 [源码 sandboxing/src/lib.rs]:
rust
pub use violation::FileSystemSandboxViolation;
pub use violation::FileSystemSandboxViolationReason;
pub use violation::NetworkSandboxViolation;
pub use violation::SandboxViolationBackend;
pub use violation::SandboxViolationEvent;
pub use violation::record_filesystem_sandbox_violation;
pub use violation::record_network_sandbox_violation;
沙箱拦下一次操作,不只是返回 EACCES,还要记录一条结构化事件:哪个后端拦的、什么类型、什么原因。
这些数据的用途:
- 遥测——线上到底哪些操作被沙箱拦了?拦对了吗?
- 给模型的反馈——第 9 章那个 is_likely_sandbox_denied 启发式,理想情况下应该被这个替代
- 给用户的解释——"你的命令失败是因为沙箱不让写 /etc",而不是一个裸的 Permission denied
可迁移的判断 ⑱
拦截必须可观测。 一个只会拒绝、不会说明理由的安全层,在 agent 场景里比没有更糟——模型不知道为什么失败,就会一遍遍重试同一个被拦的操作,烧完整个轮次预算。
七、沙箱管不了什么
这一节是为第 11 章铺垫,也是本章最重要的认知。
内核级沙箱能做的:限制进程能碰哪些文件、能不能开 socket、能不能提权。
它做不了的:
| 做不了 | 为什么 |
|---|
| 区分 curl api.openai.com 和 curl attacker.com | 内核只看到"开了一个 socket",不理解 HTTP 和域名信任 |
| 判断 rm -rf ./build 是清理还是破坏 | 都在可写目录内,沙箱正常放行 |
| 发现 git push 推的是不该推的分支 | 就是一次正常的网络访问 |
| 识别提示词注入 | 命令语法完全合法 |
| 判断把 .env 内容发到某处是否越界 | 读文件合法、发网络合法,组合起来才有问题 |
沙箱是语法层的防御,威胁在语义层。
Codex 给出的两个补充答案:
- 网络代理(network-proxy,18491 行)—— 沙箱允许联网时,流量走应用层代理,那里才能按域名/路径过滤
- Guardian —— 用模型判断语义风险(第 11 章)
八、动手复核
bash
cd codex/codex-rs
# 1. 四种沙箱类型
sed -n '36,55p' sandboxing/src/manager.rs
# 2. macOS 基础策略全文(Scheme,可读)
cat sandboxing/src/seatbelt_base_policy.sbpl
ls sandboxing/src/*.sbpl
# 3. 写死路径与威胁模型边界
sed -n '51,58p' sandboxing/src/seatbelt.rs
# 4. Linux 三机制分工
sed -n '1,10p' linux-sandbox/src/landlock.rs
sed -n '32,50p' linux-sandbox/src/landlock.rs
# 5. 三平台代码量对比
for c in linux-sandbox windows-sandbox-rs sandboxing; do
echo -n "$c: "; find $c -name '*.rs' -print0 | xargs -0 cat | wc -l
done
wc -l sandboxing/src/seatbelt.rs
# 6. 策略变换层
wc -l sandboxing/src/policy_transforms*.rs
# 7. 违规事件
sed -n '1,40p' sandboxing/src/violation.rs
亲手试一下沙箱:
bash
codex sandbox --help
codex sandbox -- ls /etc # 只读策略下能看
codex sandbox -- touch /etc/x # 应该被拦
九、总结
- 四种沙箱类型、三套内核实现:macOS Seatbelt(SBPL,抄 Chrome,(deny default) 起步)、Linux bubblewrap + seccomp + no_new_privs(Landlock 降级为备用)、Windows 受限令牌 + ACL
- Windows 最贵:19852 : 9694 : 1036,这个比例可以拿去做跨平台安全功能的成本估算
- Linux 的核心约束是"限制不可撤销"——必须在新线程/新进程里施加,这直接导致了第 2 章的 arg0 技巧
- 自带 bwrap 并做 SHA-256 校验:分发用于隔离的可执行文件,就得能证明它没被换
- 写死 /usr/bin/sandbox-exec 并声明威胁模型边界——边界声明和防御本身一样重要
- PermissionProfile 是解耦层:用户策略 → 规范形式 → 平台机制,上层逻辑不碰平台细节
- ExternalSandbox 变体处理"我已经在容器里了"这个真实场景
- 拦截必须可观测,否则模型会一遍遍重试被拦的操作
- 沙箱是语法层防御,威胁在语义层——这是下一章存在的理由
下一章补上语义层:从静态判定矩阵,到 Starlark 规则,到用模型当判官。
- 第9章-命令执行-快照统一执行与后台终端
- 第11章-审批与策略-从静态规则到模型判官
- 第2章-工程骨架-102个crate与单二进制多入口 —— arg0 技巧为什么是沙箱的前提
- dsh 教程第 10 章 —— 可换后端 seam 的对照