Agent X-Ray
NotesSkillsAbout
Notes/源码拆解/Codex Harness/第10章

第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行数机制
macOSsandboxing/src/seatbelt.rs + 4 个 .sbpl1036 + 策略文件Seatbelt(sandbox-exec
Linuxlinux-sandbox/9694bubblewrap(文件系统)+ seccomp(网络)+ Landlock(备用)
Windowswindows-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.activecpuhw.byteorderhw.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 更合适(不需要命名空间、更轻)。但它有两个现实问题:

  1. 内核版本要求。Landlock 的 ABI 一路在演进(ABI 枚举、CompatLevel 兼容级别都在代码里),老内核上能力不全
  2. 粒度。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 就被限制住了,后面所有命令都出不去。所以必须:

  1. 起一个新线程(或新进程)
  2. 在那里施加限制
  3. 从那里 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.rs2322沙箱环境准备
acl.rs822访问控制列表操作
spawn_prep.rs719进程创建前的准备
bin/setup_main/win.rs1324codex-windows-sandbox-setup 二进制
bin/command_runner/win.rs723codex-command-runner 二进制
unified_exec/~2000Windows 上的进程管理

两个独立二进制(第 2 章列过):一个负责管理员级的环境准备,一个负责在沙箱内跑命令。

几个 Windows 特有的复杂度:

  • WindowsSandboxLevel —— 分级,不是开关
  • 管理员 vs 非管理员两套后端 [源码 sandboxing/src/windows.rs]:windows_sandbox_uses_elevated_backendresolve_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.comcurl attacker.com内核只看到"开了一个 socket",不理解 HTTP 和域名信任
判断 rm -rf ./build 是清理还是破坏都在可写目录内,沙箱正常放行
发现 git push 推的是不该推的分支就是一次正常的网络访问
识别提示词注入命令语法完全合法
判断把 .env 内容发到某处是否越界读文件合法、发网络合法,组合起来才有问题

沙箱是语法层的防御,威胁在语义层。

Codex 给出的两个补充答案:

  1. 网络代理network-proxy,18491 行)—— 沙箱允许联网时,流量走应用层代理,那里才能按域名/路径过滤
  2. 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     # 应该被拦

九、总结

  1. 四种沙箱类型、三套内核实现:macOS Seatbelt(SBPL,抄 Chrome,(deny default) 起步)、Linux bubblewrap + seccomp + no_new_privs(Landlock 降级为备用)、Windows 受限令牌 + ACL
  2. Windows 最贵:19852 : 9694 : 1036,这个比例可以拿去做跨平台安全功能的成本估算
  3. Linux 的核心约束是"限制不可撤销"——必须在新线程/新进程里施加,这直接导致了第 2 章的 arg0 技巧
  4. 自带 bwrap 并做 SHA-256 校验:分发用于隔离的可执行文件,就得能证明它没被换
  5. 写死 /usr/bin/sandbox-exec 并声明威胁模型边界——边界声明和防御本身一样重要
  6. PermissionProfile 是解耦层:用户策略 → 规范形式 → 平台机制,上层逻辑不碰平台细节
  7. ExternalSandbox 变体处理"我已经在容器里了"这个真实场景
  8. 拦截必须可观测,否则模型会一遍遍重试被拦的操作
  9. 沙箱是语法层防御,威胁在语义层——这是下一章存在的理由

下一章补上语义层:从静态判定矩阵,到 Starlark 规则,到用模型当判官。


  • 第9章-命令执行-快照统一执行与后台终端
  • 第11章-审批与策略-从静态规则到模型判官
  • 第2章-工程骨架-102个crate与单二进制多入口 —— arg0 技巧为什么是沙箱的前提
  • dsh 教程第 10 章 —— 可换后端 seam 的对照

本章目录
一、统一的入口:四种沙箱类型二、macOS:Seatbelt三、Linux:三种机制分工四、Windows:为什么最大五、统一策略如何变换成三种东西六、违规记录:沙箱不只是拦截七、沙箱管不了什么八、动手复核九、总结Related Documents
苏ICP备2025204887号-2