跳转到内容

Codex 的安全沙箱实现

Codex 的安全沙箱实现

学习目标

  • 理解 Codex 的多层沙箱架构的具体实现
  • 掌握 Linux(bwrap+seccomp)和 macOS(Seatbelt)沙箱配置
  • 分析 Codex 的 execpolicy 策略引擎和权限提升控制

前置知识

本章涉及安全沙箱的通用原理,建议先阅读:

下文假设你已理解上述概念,直接聚焦 Codex 的具体实现。


项目实践

沙箱层次架构

Codex 的沙箱体系分布在多个 crate:

sandboxing crate — 沙箱抽象层

sandboxing/src/manager.rs 定义了跨平台的沙箱管理:

pub enum SandboxType {
None,
MacosSeatbelt,
LinuxSeccomp,
WindowsRestrictedToken,
}
pub enum SandboxablePreference {
Auto, // 根据策略自动选择
Require, // 强制启用沙箱
Forbid, // 强制禁用沙箱
}

策略选择逻辑select_initial()):

  • 当文件系统策略非 ExternalSandbox 且网络受限时,强制启用平台沙箱
  • 当存在 has_managed_network_requirements 时,强制启用平台沙箱

权限配置合并effective_permission_profile()):

  • 合并基础权限与 additional_permissions(如用户临时授权)
  • 支持 glob 模式的 deny-read 条目

Linux 沙箱实现

阶段一:Bubblewrap 构建文件系统视图

linux-sandbox/src/bwrap.rs 生成 bwrap 参数:

fn create_bwrap_command_args(policy: &FileSystemSandboxPolicy) -> Vec<String> {
let mut args = vec![];
// 默认全盘只读或从零构建
args.push("--ro-bind".into());
args.push("/".into());
args.push("/".into());
// 逐个开放可写根目录
for root in &policy.writable_roots {
args.push("--bind".into());
args.push(root.clone());
args.push(root.clone());
}
// 在可写根内重新施加只读保护
for subpath in &policy.readonly_subpaths {
args.push("--ro-bind".into());
args.push(subpath.clone());
args.push(subpath.clone());
}
// 最小设备树
args.push("--dev".into());
args.push("/dev".into());
args
}

unreadable glob 处理:通过 ripgrep 预展开 glob 模式为具体路径掩码,避免 bwrap 不支持 glob 的问题。

阶段二:seccomp + no_new_privs

linux-sandbox/src/landlock.rs(命名保留自旧版 Landlock,实际主要用 seccomp):

fn install_network_seccomp_filter_on_current_thread(mode: NetworkMode) {
match mode {
NetworkMode::Restricted => {
// 阻断 connect/accept/bind/listen/sendto
// 仅允许 AF_UNIX socket
}
NetworkMode::ProxyRouted => {
// 仅允许 AF_INET/AF_INET6
// 阻断 AF_UNIX,强制走代理桥
}
}
// 同时阻断 ptrace、process_vm_readv/writev、io_uring_*
}

代理路由桥

linux-sandbox/src/proxy_routing.rs

  • 主机侧创建 Unix Domain Socket 桥接器
  • bwrap 隔离的网络命名空间内,代理环境变量指向本地 UDS
  • 实现”网络命名空间隔离 + 代理强制转发”

execpolicy 策略引擎

execpolicy crate 使用 Starlark DSL 定义策略:

# 示例策略文件
prefix_rule(pattern=["git", "status"], decision="allow")
prefix_rule(pattern=["rm", "-rf", "/"], decision="forbidden")
network_rule(host="api.github.com", protocol="https", decision="allow",
justification="访问 GitHub API")

核心结构

  • Policy:包含 rules_by_program(按首命令分组)、network_rules
  • PrefixRulepattern + decision
  • PolicyParser:使用 starlark 解析器加载 .rules 文件

运行时追加amend.rs):

  • 用户批准后持久化新规则
  • 使用文件锁保护并发修改

shell-escalation 权限提升控制

shell-escalation crate 解决沙箱内 shell 执行外部命令时的权限边界:

三种决策

  • Run:沙箱内直接执行
  • Escalate(EscalationExecution):提升到服务器侧,可选 Unsandboxed / TurnDefault / Permissions(...)
  • Deny:拒绝执行

关键安全设计

  • 每个请求创建独立的 stream socket
  • Wrapper 通过 SuperExecMessage 转发 stdin/stdout/stderr 的 fd
  • prepare_escalated_exec() 由调用方实现,可注入沙箱状态

问题与规避

bwrap 在容器中的兼容

Codex 检测容器环境,当 --proc /proc 被拒绝时静默禁用。提供 --dangerously-bypass-approvals-and-sandbox 作为应急选项。

策略文件并发修改

Codex 使用 advisory file lock 保护策略文件的追加操作,确保多进程并发安全。


设计取舍

为什么用 Starlark 作为策略 DSL?

Codex 选择 Starlark(Python 子集)而非 JSON/YAML 作为策略语言。优势是:

  • 表达能力:支持变量、函数、条件逻辑
  • 安全性:Starlark 是确定性的、无 I/O 的,不会被恶意利用
  • 可读性:对开发者友好

代价是引入了额外的解析依赖(starlark crate),且 Starlark 的学习曲线略高于 JSON。

为什么保留 landlock.rs 的文件名但主要用 seccomp?

Codex 最初使用 Landlock 进行文件系统限制,后迁移到 bwrap + seccomp 的组合。保留 landlock.rs 文件名是为了兼容旧代码,实际逻辑已改为 seccomp 为主。这反映了项目演进中的技术债务。


参考来源