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_rulesPrefixRule:pattern+decisionPolicyParser:使用 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 为主。这反映了项目演进中的技术债务。