Skip to content

ShellEscalation

从统一 exec 的 zsh-fork 入口到策略决策、升级执行映射、环境合并和失败边界,解释 shell escalation 如何保持沙箱约束。

基于rust-v0.150.0
CodexRustSecurityShellEscalation

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

rust
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 构造

rust
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

rust
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

rust
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

rust
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

rust
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

rust
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

rust
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

rust
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

rust
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

rust
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

rust
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 或系统安全策略。

在源码仓库中可运行:

text
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 的平台进程加固入口。