Skip to content

ApplyPatch安全与原子性

解析 Apply Patch 的可写路径检查、审批分支、filesystem sandbox、symlink边界和部分失败 delta。

基于rust-v0.150.0
CodexRustExecutionApplyPatchSecurity

ApplyPatch安全与原子性 ​

Apply Patch 的“安全”不是一句“运行在 sandbox 中”,也不是事务式文件系统。当前实现分成三层: assess_patch_safety 决定自动批准、询问或拒绝;ApplyPatchRuntime 把 environment、approval requirement、 additional permissions 和 sandbox attempt 组合成 filesystem context;实际写入按文件逐个提交,并用 AppliedPatchDelta.exact 表示已提交变化是否仍可精确重建。local 与 remote 使用同一 runtime contract, 但 remote sandbox 由 executor 负责执行。

本文承接ApplyPatch文件更新算法和ExecServer文件系统RPC,面向理解权限 profile、sandbox context、symlink 和异步 I/O 的读者。范围是 Apply Patch 的安全决策、执行边界和部分失败语义;不宣称事务回滚或覆盖所有平台安全机制。读完后,你应能解释为什么同一个 patch 在不同 approval policy 下结果不同,以及为什么失败 delta 可能是 inexact。

1. 安全决策 ​

1.1 空patch ​

空 action 直接 Reject,不进入 sandbox 或审批流程。安全检查首先保证没有“看起来成功但没有变化”的 patch。

源码位置:codex-rs/core/src/safety.rs :: assess_patch_safety

rust
if action.is_empty() {
    return SafetyCheck::Reject {
        reason: "empty patch".to_string(),
    };
}

1.2 可写路径 ​

安全检查将 Add/Delete/Update 的源路径和 move destination 都转换为绝对、规范化路径,并询问 filesystem policy 是否允许写入;full-disk write policy 直接视为 constrained。

源码位置:codex-rs/core/src/safety.rs :: is_write_patch_constrained_to_writable_paths

rust
for (path, change) in action.changes() {
    match change {
        ApplyPatchFileChange::Add { .. } | ApplyPatchFileChange::Delete { .. } => {
            if !is_path_writable(path) {
                return false;
            }
        }
        ApplyPatchFileChange::Update { move_path, .. } => {
            if !is_path_writable(path) {
                return false;
            }
            if let Some(dest) = move_path && !is_path_writable(dest) {
                return false;
            }
        }
    }
}
true

2. Approval策略 ​

UnlessTrusted 直接 AskUser;Never 或 granular sandbox approval 关闭时,越出 writable roots 的 patch 会 Reject。即使路径表面在 writable roots,hard link 等 filesystem 关系仍可能绕出目录前缀,因此 Managed profile 只有在平台 sandbox 可用时才 AutoApprove。Disabled 与 External profile 明确不使用外层 Codex filesystem sandbox,路径受限时可以直接 AutoApprove。

源码位置:codex-rs/core/src/safety.rs :: assess_patch_safety

rust
match policy {
    AskForApproval::Never | AskForApproval::OnRequest | AskForApproval::Granular(_) => {}
    AskForApproval::UnlessTrusted => return SafetyCheck::AskUser,
}
let rejects_sandbox_approval = matches!(policy, AskForApproval::Never)
    || matches!(policy, AskForApproval::Granular(config) if !config.sandbox_approval);

prepare_apply_patch 不直接弹窗,而是把 SafetyCheck 转成 ExecApprovalRequirement:AutoApprove 对应 Skip { bypass_sandbox: false },AskUser 对应 NeedsApproval,Reject 直接返回模型错误。实际审批、缓存和 sandbox retry 仍由 ToolOrchestrator 统一处理。

源码位置:codex-rs/core/src/apply_patch.rs :: prepare_apply_patch

rust
match assess_patch_safety(/* action and policies */) {
    SafetyCheck::AutoApprove => Ok(ApplyPatchRuntimeInvocation {
        action,
        auto_approved: true,
        exec_approval_requirement: ExecApprovalRequirement::Skip {
            bypass_sandbox: false,
            proposed_execpolicy_amendment: None,
        },
    }),
    SafetyCheck::AskUser => Ok(ApplyPatchRuntimeInvocation {
        action,
        auto_approved: false,
        exec_approval_requirement: ExecApprovalRequirement::NeedsApproval {
            reason: None,
            proposed_execpolicy_amendment: None,
        },
    }),
    SafetyCheck::Reject { reason } => Err(/* patch rejected */),
}

审批 action 携带 environment ID、cwd、文件 PathUri、原 patch 和结构化 changes;缓存键按 environment_id + path 生成,因此不同 executor 上相同文本路径不会共享批准。

源码位置:

  • codex-rs/core/src/tools/runtimes/apply_patch.rs :: ApplyPatchApprovalKey、ApplyPatchRuntime::build_approval_action
  • codex-rs/core/src/tools/approvals.rs :: ApprovalAction::cache_keys

3. Sandbox运行时 ​

3.1 context构造 ​

runtime 根据 SandboxAttempt 生成 FileSystemSandboxContext,携带 effective permissions、sandbox cwd、workspace roots、executor Windows 等级和 legacy Landlock 标记。判断依据是 sandbox_requested,而不是 SandboxType:remote executor-managed attempt 可能显示 SandboxType::None,但仍需要把 context 发给 executor。

源码位置:codex-rs/core/src/tools/runtimes/apply_patch.rs :: ApplyPatchRuntime::file_system_sandbox_context_for_attempt

rust
if !attempt.sandbox_requested {
    return None;
}
let permissions = effective_permission_profile(
    attempt.exec_server_permissions,
    req.additional_permissions.as_ref(),
);
Some(FileSystemSandboxContext {
    permissions: permissions.into(),
    cwd: Some(attempt.sandbox_cwd.clone()),
    workspace_roots: attempt.workspace_roots.to_vec(),
    windows_sandbox_level: executor_windows_sandbox_level(
        attempt.windows_sandbox_level,
        attempt.sandbox_cwd,
    ),
    windows_sandbox_private_desktop: attempt.windows_sandbox_private_desktop,
    windows_sandbox_proxy_settings_mode: None,
    use_legacy_landlock: attempt.use_legacy_landlock,
})

3.2 执行位置 ​

ApplyPatchRuntime 使用选定 TurnEnvironment 的 filesystem 执行 patch;uses_executor_managed_process_sandbox 对 remote environment 返回 true,让 orchestrator 知道 enforcement 位于 executor。runtime 调用 apply_patch_with_options,将 verified action 的行尾模式继续传入,并动态决定 follow_symlinks。 sandbox denial 被识别时,runtime 转成 Codex Sandbox error,同时保留 stdout/stderr 和 committed delta。

源码位置:codex-rs/core/src/tools/runtimes/apply_patch.rs :: ApplyPatchRuntime::run、uses_executor_managed_process_sandbox

rust
let fs = req.turn_environment.environment.get_filesystem();
let sandbox = Self::file_system_sandbox_context_for_attempt(req, attempt);
let result = codex_apply_patch::apply_patch_with_options(
    &req.action.patch,
    ApplyPatchOptions {
        update_file_mode: req.action.update_file_mode(),
        follow_symlinks: attempt.sandbox_requested
            || !attempt.manager.should_sandbox(
                attempt.permissions,
                self.sandbox_preference(),
                attempt.enforce_managed_network,
            ),
    },
    &req.action.cwd,
    &mut stdout,
    &mut stderr,
    fs.as_ref(),
    sandbox.as_ref(),
).await;
let failed = result.is_err();
let delta = match result {
    Ok(delta) => delta,
    Err(failure) => failure.into_parts().1,
};
self.committed_delta.append(delta);

这个 follow_symlinks 条件只在一种情况下为 false:策略认为本应 sandbox,但当前 attempt 没有请求 sandbox。此时 apply 层必须逐次 no-follow 检查叶子和祖先路径,防止“验证后把目录换成 symlink”的 TOCTOU 路径替换。正常 sandbox attempt 或本来无需 sandbox 的执行仍允许 follow,由 sandbox/policy 承担边界。

4. exact与inexact ​

4.1 写入失败 ​

apply 通过 try_write! 包装写入;写入失败时将 delta exact 置 false,因为底层写入可能已经产生部分副作用,错误返回不能证明文件完全未变。

源码位置:codex-rs/apply-patch/src/lib.rs :: apply_hunks_to_files

rust
macro_rules! try_write {
    ($result:expr) => {
        match $result {
            Ok(value) => value,
            Err(error) => {
                delta.exact = false;
                return Err(anyhow::Error::from(error));
            }
        }
    };
}

4.2 symlink与不可读 ​

删除前会检查 metadata;如果目标是 symlink、不是普通 file、或无法读取原内容,delta exact 可能降为 false。这个标记表达“已提交变化的重建可信度”,不是简单的成功/失败布尔值。

源码位置:codex-rs/apply-patch/src/lib.rs :: note_existing_path_delta_support、remove_failure_was_side_effect_free

rust
match fs
    .get_metadata(path, GetMetadataOptions { follow_symlinks }, sandbox)
    .await
{
    Ok(metadata) if metadata.is_file && !metadata.is_symlink => {}
    Ok(_) | Err(_) => *exact = false,
}

4.3 no-follow重检 ​

follow_symlinks: false 会传播到 read、metadata、write、mkdir 和 remove。它不只检查最终文件是否为 symlink,也拒绝任意祖先 component 的 link;而且应用阶段重新检查路径,不能依赖 verify 阶段的旧 结果。这样即使攻击者在 verify 后把 approved/ 换成指向 outside 的 symlink,正式写入仍会失败。

源码位置:

  • codex-rs/apply-patch/src/lib.rs :: ApplyPatchOptions、apply_hunks_to_files
  • codex-rs/apply-patch/tests/suite/no_follow.rs :: no_follow_rechecks_paths_after_verification
rust
ApplyPatchOptions {
    update_file_mode: req.action.update_file_mode(),
    follow_symlinks: false,
}

默认 standalone apply_patch 仍 follow links;no-follow 是受 orchestrator attempt 控制的强化路径,不是 语言 parser 的固定语义。

5. Move边界 ​

move 先写 destination,再删除 source;source 删除失败时 destination 变化已提交,ApplyPatchFailure.delta() 返回这部分信息。源码没有把 write + remove 包成跨路径事务。

源码位置:codex-rs/apply-patch/src/lib.rs :: apply_hunks_to_files 的 move 分支

rust
try_write!(write_file_with_missing_parent_retry(
    fs,
    &dest_uri,
    new_contents.clone().into_bytes(),
    follow_symlinks,
    sandbox,
).await);
delta.changes.push(AppliedPatchChange {
    path: dest_uri.clone(),
    change: AppliedPatchFileChange::Add {
        content: new_contents.clone(),
        overwritten_content: overwritten_move_content.clone(),
    },
});
// source remove follows and may fail

6. 验证 ​

6.1 Safety检查 ​

safety tests 覆盖 workspace writable roots、outside path、read-only subpaths、external sandbox 和 granular sandbox approval;断言 AutoApprove、AskUser 或 Reject。

源码位置:codex-rs/core/src/safety_tests.rs :: granular_sandbox_approval_false_rejects_out_of_root_patch、explicit_read_only_subpaths_prevent_auto_approval_for_external_sandbox

text
cd codex-rs
cargo test -p codex-core --lib safety::tests -- --test-threads=1
cargo test -p codex-core --lib tools::runtimes::apply_patch::tests -- --test-threads=1

6.2 结果可信度 ​

Apply Patch tests 覆盖 write error 导致 inexact、unreadable destination、delete symlink 和 failed move delta;这些测试直接验证“失败后能证明什么”。

源码位置:codex-rs/apply-patch/src/lib.rs :: test_apply_patch_fails_on_write_error、test_unreadable_destinations_return_inexact_delta、test_delete_symlink_returns_inexact_delta

text
cd codex-rs
cargo test -p codex-apply-patch --lib test_apply_patch_fails_on_write_error -- --test-threads=1
cargo test -p codex-apply-patch --lib test_unreadable_destinations_return_inexact_delta -- --test-threads=1
cargo test -p codex-apply-patch --lib test_delete_symlink_returns_inexact_delta -- --test-threads=1
cargo test -p codex-apply-patch --lib test_failed_move_returns_committed_destination_delta -- --test-threads=1
cargo test -p codex-apply-patch --test all suite::no_follow -- --test-threads=1

这些测试不证明多文件 patch 会回滚,反而明确覆盖 partial success、move destination 已提交和 inexact delta。不同平台 sandbox backend 的系统调用实现也不能由当前平台测试外推。

7. 源码排查 ​

text
rg -n "assess_patch_safety|is_write_patch_constrained|PATCH_REJECTED" codex-rs/core/src/safety.rs
rg -n "sandbox_requested|file_system_sandbox_context_for_attempt|follow_symlinks|committed_delta" codex-rs/core/src/tools/runtimes/apply_patch.rs
rg -n "delta.exact|try_write|remove_failure_was_side_effect_free" codex-rs/apply-patch/src/lib.rs
rg -n "no_follow|rechecks_paths_after_verification" codex-rs/apply-patch/tests/suite/no_follow.rs

安全主线是:先基于路径和 approval policy 生成审批要求,再把 environment-scoped attempt 转成 filesystem sandbox context,并在必要时启用 no-follow 重检;文件逐个提交并记录 exactness,失败不会自动回滚。 下一篇ApplyPatch语法与回归测试将分析场景矩阵与跨层回归策略。