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
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
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;
}
}
}
}
true2. 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
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
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_actioncodex-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
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
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
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
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_filescodex-rs/apply-patch/tests/suite/no_follow.rs::no_follow_rechecks_paths_after_verification
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 分支
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 fail6. 验证
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
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=16.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
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. 源码排查
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语法与回归测试将分析场景矩阵与跨层回归策略。
