ShellEscalation
Shell escalation 不是给命令加一层 sudo,也不是把 sandbox denial 自动改成 unsandboxed。当前实现的 owner 位于 core/src/tools/runtimes/zsh_fork/unix_escalation.rs:统一 exec 在满足 zsh-fork 条件时创建 EscalateServer,由 CoreShellActionProvider 把 exec-policy、approval policy、sandbox permissions、permission profile 和当前环境组合成 EscalationDecision;只有决策允许,CoreShellCommandExecutor 才会把执行映射为 TurnDefault、Unsandboxed 或一个 resolved permission profile。
本文承接权限审批沙箱三层模型、命令规范化与审批缓存和跨平台Sandbox抽象。范围是 zsh-fork 的准备条件、策略来源、审批分支、执行映射、环境合并和统一 exec 重试边界;不把 escalation 描述为获得 root,也不展开 shell 本身的 startup files。
1. 入口迁移
旧版文章引用的 core/src/tools/runtimes/shell/unix_escalation.rs 已删除。当前 owner 在 zsh-fork runtime:prepare_unified_exec_zsh_fork 先解析 shell command,确认 program 等于配置的 zsh 路径,再冻结 environment、sandbox policy cwd、PermissionProfile、network proxy 和 workspace roots,最后创建 escalation session。
源码位置:codex-rs/core/src/tools/runtimes/zsh_fork/unix_escalation.rs :: prepare_unified_exec_zsh_fork
pub(crate) async fn prepare_unified_exec_zsh_fork(
req: &crate::tools::runtimes::unified_exec::UnifiedExecRequest,
_attempt: &SandboxAttempt<'_>,
ctx: &ToolCtx,
exec_request: ExecRequest,
shell_zsh_path: &std::path::Path,
main_execve_wrapper_exe: &std::path::Path,
) -> Result<Option<PreparedUnifiedExecZshFork>, ToolError> {
let parsed = match extract_shell_script(&exec_request.command) {
Ok(parsed) => parsed,
Err(err) => {
tracing::warn!("ZshFork unified exec fallback: {err:?}");
return Ok(None);
}
};
if parsed.program != shell_zsh_path.to_string_lossy() {
tracing::warn!(
"ZshFork backend specified, but unified exec command targets `{}` instead of `{}`.",
parsed.program,
shell_zsh_path.display(),
);
return Ok(None);
}
let exec_policy = Arc::new(RwLock::new(
ctx.session
.services
.exec_policy
.current_for_environment(
req.turn_environment.config().exec_policy.as_ref(),
ctx.step_context.turn.allow_prefix_rules(),
)
.as_ref()
.clone(),
));不满足 shell 形态或 zsh 路径时返回 None,让其他 Unified Exec runtime 接管;这不是 escalation denial,而是 backend 不适用。
2. 创建会话
准备阶段把 ExecRequest 转成 CoreShellCommandExecutor,同时创建 CoreShellActionProvider。其中 approval_sandbox_permissions 会在 additional permissions 已预批准时把 WithAdditionalPermissions 变为 UseDefault,避免把同一份追加权限重复计算。
源码位置:codex-rs/core/src/tools/runtimes/zsh_fork/unix_escalation.rs :: approval_sandbox_permissions、CoreShellActionProvider 构造
fn approval_sandbox_permissions(
sandbox_permissions: SandboxPermissions,
additional_permissions_preapproved: bool,
) -> SandboxPermissions {
if additional_permissions_preapproved
&& matches!(
sandbox_permissions,
SandboxPermissions::WithAdditionalPermissions
)
{
SandboxPermissions::UseDefault
} else {
sandbox_permissions
}
}源码位置:codex-rs/core/src/tools/runtimes/zsh_fork/unix_escalation.rs :: prepare_unified_exec_zsh_fork
let escalation_policy = CoreShellActionProvider {
policy: Arc::clone(&exec_policy),
session: Arc::clone(&ctx.session),
review_context: GuardianReviewContext::from(&ctx.step_context),
call_id: ctx.call_id.clone(),
environment_id: req.turn_environment.selection.environment_id.clone(),
source: GuardianCommandSource::UnifiedExec,
tool_name: ctx.tool_name.clone(),
approval_policy: ctx.step_context.turn.approval_policy(),
permission_profile: exec_request.permission_profile.clone(),
sandbox_permissions: req.sandbox_permissions,
approval_sandbox_permissions: approval_sandbox_permissions(
req.sandbox_permissions,
req.additional_permissions_preapproved,
),
prompt_permissions: req.additional_permissions.clone(),
stopwatch: Stopwatch::unlimited(),
};
let escalate_server = EscalateServer::new(
shell_zsh_path.to_path_buf(),
main_execve_wrapper_exe.to_path_buf(),
escalation_policy,
);
let escalation_session = escalate_server
.start_session(CancellationToken::new(), Arc::new(command_executor))
.map_err(|err| ToolError::Rejected(err.to_string()))?;
let mut exec_request = exec_request;
exec_request.env.extend(escalation_session.env().clone());session 环境只提供 escalation socket/wrapper 等后续执行需要的变量;它不会直接决定最终 sandbox mode。
3. 执行模式映射
shell_request_escalation_execution 把 shell tool 的 sandbox request 映射为 EscalationExecution。RequireEscalated 只有在当前 PermissionProfile 的 filesystem policy 允许 unsandboxed 时才返回 Unsandboxed;否则保留 TurnDefault。追加权限则包装成 resolved permission profile。
源码位置:codex-rs/core/src/tools/runtimes/zsh_fork/unix_escalation.rs :: shell_request_escalation_execution
fn shell_request_escalation_execution(
sandbox_permissions: SandboxPermissions,
permission_profile: &PermissionProfile,
additional_permissions: Option<&AdditionalPermissionProfile>,
) -> EscalationExecution {
match sandbox_permissions {
SandboxPermissions::UseDefault => EscalationExecution::TurnDefault,
SandboxPermissions::RequireEscalated => {
if unsandboxed_execution_allowed(&permission_profile.file_system_sandbox_policy()) {
EscalationExecution::Unsandboxed
} else {
EscalationExecution::TurnDefault
}
}
SandboxPermissions::WithAdditionalPermissions => additional_permissions
.map(|_| {
EscalationExecution::Permissions(
EscalationPermissions::ResolvedPermissionProfile(
ResolvedPermissionProfile {
permission_profile: permission_profile.clone(),
},
),
)
})
.unwrap_or(EscalationExecution::TurnDefault),
}
}这里的 Unsandboxed 是策略允许的执行模式,不等于获得操作系统 root;命令仍会经过后续 execve wrapper 和环境构造。
4. Policy决策
determine_action 先调用 evaluate_intercepted_exec_policy,再根据 DecisionSource 判断是否需要 escalation。PrefixRule 和 UnmatchedCommandFallback 的 execution mapping 不同;启用的 shell wrapper parsing 目前是 false,因为脚本文本无法可靠得到最终 executable path。
源码位置:codex-rs/core/src/tools/runtimes/zsh_fork/unix_escalation.rs :: determine_action
const ENABLE_INTERCEPTED_EXEC_POLICY_SHELL_WRAPPER_PARSING: bool = false;
async fn determine_action(
&self,
program: &AbsolutePathBuf,
argv: &[String],
workdir: &AbsolutePathBuf,
) -> anyhow::Result<EscalationDecision> {
let evaluation = {
let policy = self.policy.read().await;
evaluate_intercepted_exec_policy(
&policy,
program,
argv,
InterceptedExecPolicyContext {
approval_policy: self.approval_policy,
permission_profile: self.permission_profile.clone(),
windows_sandbox_level: self.review_context.turn().windows_sandbox_level,
sandbox_permissions: self.approval_sandbox_permissions,
enable_shell_wrapper_parsing:
ENABLE_INTERCEPTED_EXEC_POLICY_SHELL_WRAPPER_PARSING,
},
)
};
let decision_driven_by_policy =
Self::decision_driven_by_policy(&evaluation.matched_rules, evaluation.decision);
let unsandboxed_allowed =
unsandboxed_execution_allowed(&self.permission_profile.file_system_sandbox_policy());
let needs_escalation = match self.sandbox_permissions {
SandboxPermissions::UseDefault => unsandboxed_allowed && decision_driven_by_policy,
SandboxPermissions::RequireEscalated => unsandboxed_allowed,
SandboxPermissions::WithAdditionalPermissions => true,
};
let decision_source = if decision_driven_by_policy {
DecisionSource::PrefixRule
} else {
DecisionSource::UnmatchedCommandFallback
};源码位置:codex-rs/core/src/tools/runtimes/zsh_fork/unix_escalation.rs :: process_decision
let action = match decision {
Decision::Forbidden => {
EscalationDecision::deny(Some("Execution forbidden by policy".to_string()))
}
Decision::Prompt => {
if execve_prompt_is_rejected_by_policy(self.approval_policy, &decision_source)
.is_some()
{
EscalationDecision::deny(Some("Execution forbidden by policy".to_string()))
} else {
let decision = self
.prompt(program, argv, workdir, &self.stopwatch, prompt_permissions)
.await?;
match decision {
ReviewDecision::Approved
| ReviewDecision::ApprovedForSession
| ReviewDecision::ApprovedExecpolicyAmendment { .. } => {
if needs_escalation {
EscalationDecision::escalate(escalation_execution.clone())
} else {
EscalationDecision::run()
}
}
ReviewDecision::NetworkPolicyAmendment {
network_policy_amendment,
} => match network_policy_amendment.action {
NetworkPolicyRuleAction::Allow => {
if needs_escalation {
EscalationDecision::escalate(escalation_execution.clone())
} else {
EscalationDecision::run()
}
}
NetworkPolicyRuleAction::Deny => {
EscalationDecision::deny(Some("User denied execution".to_string()))
}
},
ReviewDecision::Denied { rejection } => {
EscalationDecision::deny(Some(rejection))
}
ReviewDecision::TimedOut => EscalationDecision::deny(Some(
crate::guardian::guardian_timeout_message(
&self.review_context.turn().model_info,
),
)),
ReviewDecision::Abort => {
EscalationDecision::deny(Some("User cancelled execution".to_string()))
}
_ => EscalationDecision::deny(Some("Error while requesting approval".to_string())),
}
}
}
Decision::Allow => {
if needs_escalation {
EscalationDecision::escalate(escalation_execution)
} else {
EscalationDecision::run()
}
}
};ApprovedMcpPolicyAmendment 在 shell escalation 中仍是拒绝;网络 amendment 的 Allow 只影响网络规则,Deny 仍结束当前执行。
5. 执行准备与环境
CoreShellCommandExecutor::prepare_escalated_exec 根据执行模式走四条分支:Unsandboxed 使用 RequireEscalated 环境投影;TurnDefault 使用原 permission profile;AdditionalPermissionProfile 合并追加权限;ResolvedPermissionProfile 使用完整替换 profile。所有分支最后返回 PreparedExec,并不直接运行命令。
源码位置:codex-rs/core/src/tools/runtimes/zsh_fork/unix_escalation.rs :: CoreShellCommandExecutor::prepare_escalated_exec
let prepared = match execution {
EscalationExecution::Unsandboxed => {
if let Some(network) = self.network.as_ref() {
network.restore_brokered_credentials(&mut env, &mut []);
}
PreparedExec {
command,
cwd: workdir.to_path_buf(),
env: exec_env_for_sandbox_permissions(
&env,
SandboxPermissions::RequireEscalated,
),
arg0: Some(first_arg.clone()),
}
}
EscalationExecution::TurnDefault => {
self.prepare_sandboxed_exec(PrepareSandboxedExecParams {
command,
workdir,
env,
permission_profile: &self.permission_profile,
additional_permissions: None,
})?
}
EscalationExecution::Permissions(
EscalationPermissions::AdditionalPermissionProfile(permission_profile),
) => self.prepare_sandboxed_exec(PrepareSandboxedExecParams {
command,
workdir,
env,
permission_profile: &self.permission_profile,
additional_permissions: Some(permission_profile),
})?,
EscalationExecution::Permissions(EscalationPermissions::ResolvedPermissionProfile(
permissions,
)) => self.prepare_sandboxed_exec(PrepareSandboxedExecParams {
command,
workdir,
env,
permission_profile: &permissions.permission_profile,
additional_permissions: None,
})?,
};
Ok(prepared)6. 重新进入sandbox
prepare_sandboxed_exec 重新通过 SandboxManager::select_initial 和 SandboxManager::transform 生成平台执行请求,然后再次应用 managed network environment。即使 escalation 使用了 resolved profile,也不会绕过统一 sandbox transform、Windows sandbox level 或 Linux helper 参数。
源码位置:codex-rs/core/src/tools/runtimes/zsh_fork/unix_escalation.rs :: prepare_sandboxed_exec
let sandbox_manager = SandboxManager::new();
let sandbox = sandbox_manager.select_initial(
permission_profile,
SandboxablePreference::Auto,
self.windows_sandbox_level,
self.network.is_some(),
);
let cwd = PathUri::from_abs_path(workdir);
let sandbox_policy_cwd = PathUri::from_abs_path(&self.sandbox_policy_cwd);
let command = SandboxCommand {
program: program.clone().into(),
args: args.to_vec(),
cwd,
env,
managed_network: None,
additional_permissions,
};
let exec_request = sandbox_manager.transform(SandboxTransformRequest {
command,
permissions: permission_profile,
sandbox,
enforce_managed_network: self.network.is_some(),
environment_id: self.network_environment_id.as_deref(),
network: self.network.as_ref(),
sandbox_policy_cwd: &sandbox_policy_cwd,
codex_linux_sandbox_exe: self.codex_linux_sandbox_exe.as_deref(),
use_legacy_landlock: self.use_legacy_landlock,
windows_sandbox_level: self.windows_sandbox_level,
windows_sandbox_private_desktop: false,
})?;7. 运行与取消
真正运行由 CoreShellCommandExecutor::run 调用 execute_exec_request_with_after_spawn,只从 escalation session overlay 合并 CODEX_ESCALATE_SOCKET 与 EXEC_WRAPPER,并把 cancellation token 作为 ExecExpiration::Cancellation 传入。Unsandboxed 的 execution mapping 不会把取消语义改成无限等待。
源码位置:codex-rs/core/src/tools/runtimes/zsh_fork/unix_escalation.rs :: CoreShellCommandExecutor::run
let mut exec_env = self.env.clone();
for var in ["CODEX_ESCALATE_SOCKET", "EXEC_WRAPPER"] {
if let Some(value) = env_overlay.get(var) {
exec_env.insert(var.to_string(), value.clone());
}
}
let result = crate::sandboxing::execute_exec_request_with_after_spawn(
crate::sandboxing::ExecRequest {
command: self.command.clone(),
cwd: self.cwd.clone().into(),
env: exec_env,
network: self.network.clone(),
network_environment_id: self.network_environment_id.clone(),
expiration: ExecExpiration::Cancellation(cancel_rx),
capture_policy: ExecCapturePolicy::ShellTool,
sandbox: self.sandbox,
windows_sandbox_policy_cwd: self.sandbox_policy_cwd.clone().into(),
windows_sandbox_workspace_roots: self.windows_sandbox_workspace_roots.clone(),
windows_sandbox_level: self.windows_sandbox_level,
windows_sandbox_private_desktop: false,
permission_profile: self.permission_profile.clone(),
..Default::default()
},
/*stdout_stream*/ None,
after_spawn,
)
.await?;8. 测试与边界
测试覆盖 execution mapping、preapproved additional permissions、Granular sandbox approval 拒绝、policy decision source、用户取消和 timeout。它们验证 escalation 决策与请求构造,不证明 sudo/doas、root 权限或 shell startup files 的行为。
源码位置:codex-rs/core/src/tools/runtimes/zsh_fork/unix_escalation_tests.rs :: shell_request_escalation_execution_is_explicit
assert_eq!(
CoreShellActionProvider::shell_request_escalation_execution(
SandboxPermissions::RequireEscalated,
&permission_profile,
None,
),
EscalationExecution::TurnDefault,
);源码位置:codex-rs/core/src/tools/runtimes/zsh_fork/unix_escalation_tests.rs :: preapproved_additional_permissions_escalate_intercepted_exec
assert_eq!(
action,
codex_shell_escalation::EscalationDecision::Escalate(
EscalationExecution::Permissions(EscalationPermissions::ResolvedPermissionProfile(
ResolvedPermissionProfile { permission_profile },
)),
)
);源码位置:codex-rs/core/src/tools/runtimes/zsh_fork/unix_escalation_tests.rs :: denied_reads_keep_granular_sandbox_rejection_for_escalation
assert_eq!(
action,
codex_shell_escalation::EscalationDecision::Deny {
reason: Some("Execution forbidden by policy".to_string())
}
);输入和断言分别证明:RequireEscalated 在 deny-read profile 下不能变成 Unsandboxed;预批准追加权限使用 resolved profile;Granular 关闭 sandbox approval 时直接拒绝。它们不能外推到真实提权、所有 Unix shell、所有 execve wrapper 或系统安全策略。
在源码仓库中可运行:
cd codex-rs
cargo test -p codex-core unix_escalation -- --test-threads=1
cargo test -p codex-core zsh_fork -- --test-threads=1补充升级执行的三段式边界:先决定权限,再准备环境,最后运行并收集取消结果。
9. 阅读闭环
建议按新 owner 路径 → zsh command extraction → EscalateServer → intercepted exec policy → execution mapping → approval decision → prepared exec → SandboxManager transform → cancellation/output 阅读。读完后应能解释:为什么旧路径已失效;为什么 RequireEscalated 仍可能得到 TurnDefault;为什么追加权限要变成 resolved profile;为什么 deny-read 阻止无沙箱分支;以及 escalation 决策与最终进程创建之间还隔着统一 sandbox transform。
下一篇进入 ProcessHardening 的平台进程加固入口。
