Skip to content

Unified Exec失败与竞态

汇总 Unified Exec 的创建失败、sandbox denial、网络拒绝、写入竞态、终止竞态与旧句柄保护。

基于rust-v0.150.0
CodexRustExecution

Unified Exec失败与竞态 ​

前九篇分别讲了创建、状态、输入、等待、watcher、截断和回收。本篇不再介绍单个机制,而是从失败分支反向检查整个 Unified Exec 是否保持不变量:没有进程的错误是否释放 ID,进程失败是否保留根因,sandbox denial 是否带回输出,网络拒绝是否等 reader 与 watcher 收尾,旧 Arc 是否会误删新会话。

本文面向已读过Unified Exec创建进程、Unified Exec会话状态机和AsyncWatcher后台监控并理解异步竞态的读者。范围是 UnifiedExecError、manager 的失败转换、process 的失败状态和现有竞态测试;不展开安全策略本身。读完后,你应能按“请求前、spawn 后、写入中、退出后”定位错误类型和清理责任。

1. 错误分层 ​

1.1 无进程错误 ​

CreateProcess、MissingCommandLine 和 ForeignPath 发生在 child 建立前;UnknownProcessId 是 store 查找失败;StdinClosed 表示 pipe 会话拒绝普通输入。这些错误不能被解释成 child 的 exit code。

源码位置:codex-rs/core/src/unified_exec/errors.rs :: UnifiedExecError

rust
pub(crate) enum UnifiedExecError {
    CreateProcess { message: String },
    ProcessFailed { message: String },
    UnknownProcessId { process_id: i32 },
    WriteToStdin,
    StdinClosed,
    MissingCommandLine,
    SandboxDenied { message: String, output: ExecToolCallOutput, .. },
    ForeignPath { path: PathUri },
}

1.2 有进程错误 ​

ProcessFailed 表示 transport、网络或终止路径需要传播的失败;SandboxDenied 还携带输出、原始 token 估计和省略字节元数据,允许模型同时看到拒绝原因与命令输出。

源码位置:codex-rs/core/src/unified_exec/errors.rs :: with_output_collection_metadata

rust
pub(crate) fn with_output_collection_metadata(
    self,
    original_token_count: usize,
    output_omitted_bytes: Option<NonZeroUsize>,
) -> Self {
    match self {
        Self::SandboxDenied { message, output, .. } => Self::SandboxDenied {
            message,
            output,
            original_token_count: Some(original_token_count),
            output_omitted_bytes,
        },
        other => other,
    }
}

2. 创建失败 ​

manager 在 open_session_with_sandbox 或 open_session_with_prepared_exec_env 出错时,调用方负责 release_process_id。本地 foreign path、空命令、spawn error 和远程 inherited fd 拒绝都不会插入可交互条目。

源码位置:codex-rs/core/src/unified_exec/process_manager.rs :: exec_command、open_session_with_prepared_exec_env

rust
let process = self
    .open_session_with_sandbox(&request, cwd.clone(), context)
    .await;
let (attempt, mut deferred_network_approval) = match process {
    Ok((attempt, deferred_network_approval)) => (attempt, deferred_network_approval),
    Err(err) => {
        self.release_process_id(request.process_id).await;
        return Err(err);
    }
};
let UnifiedExecAttempt {
    process,
    metrics_sidecar,
} = attempt;
let process = Arc::new(process);

这个分支的证明重点是资源回收,不是错误文本:即使 backend 返回详细错误,ID 也不能继续占用。只有 runtime 已经返回 UnifiedExecAttempt 后,manager 才接管进程与 metrics sidecar;创建失败之前不存在需要后台 watcher 收尾的 sidecar。

3. Sandbox与网络 ​

3.1 denial输出 ​

sandbox denial 可能在进程已经产生输出后才被识别。manager 将输出、退出码、token 估计和 omission bytes 一起包装为 SandboxDenied,不把它伪装成普通 ProcessFailed。

源码位置:codex-rs/core/src/tools/handlers/unified_exec/exec_command.rs :: ExecCommandHandler::handle_call

rust
Err(UnifiedExecError::SandboxDenied {
    output,
    original_token_count,
    output_omitted_bytes,
    ..
}) => {
    let output_text = output.aggregated_output.text;
    let original_token_count =
        original_token_count.unwrap_or_else(|| approx_token_count(&output_text));
    Ok(boxed_tool_output(ExecCommandToolOutput {
        event_call_id: context.call_id.clone(),
        chunk_id: generate_chunk_id(),
        wall_time: output.duration,
        raw_output: output_text.into_bytes(),
        truncation_policy: turn.model_info.truncation_policy.into(),
        max_output_tokens,
        process_id: None,
        exit_code: Some(output.exit_code),
        original_token_count: Some(original_token_count),
        output_omitted_bytes,
        hook_command: Some(hook_command),
    }))
}

3.2 网络拒绝 ​

网络拒绝 monitor 监听 deferred approval token 与 process exit。若拒绝先到,调用 fail_and_terminate;若 process 先退出,仍等待短暂 late-denial grace,避免把拒绝误分类为成功。

源码位置:codex-rs/core/src/unified_exec/process_manager.rs :: terminate_process_on_network_denial

rust
let denied = tokio::select! {
    _ = network_cancelled.cancelled() => true,
    _ = process_exited.cancelled() => {
        wait_for_late_network_denial(Some(network_cancelled.clone())).await
    }
};
if denied {
    let session = session.upgrade();
    let message = network_denial_message_for_session(session.as_ref(), Some(deferred)).await;
    process.fail_and_terminate(message);
}

3.3 未入store的失败 ​

进程可能已经创建,但在首次状态判断时不再存活,因此不会进入 ProcessStore,也没有后台 exit watcher 负责发结束事件。emit_failed_initial_exec_end_if_unstored 只在这种情况下补发 failed end;已经入 store 的活进程直接返回,由 watcher 保证唯一终态。

源码位置:codex-rs/core/src/unified_exec/process_manager.rs :: emit_failed_initial_exec_end_if_unstored

rust
if process_started_alive {
    return;
}

emit_failed_exec_end_for_unified_exec(
    Arc::clone(&context.session),
    Arc::clone(&context.step_context.turn),
    context.call_id.clone(),
    request.command.clone(),
    cwd,
    Some(request.process_id.to_string()),
    plugin_attribution,
    transcript,
    fallback_output,
    message,
    wall_time,
)
.await;

这条路径避免两种错误:短命令失败却没有结束事件,以及已存储后台命令由 manager 和 watcher 各发一次结束事件。

4. 写入竞态 ​

write_stdin 在同一进程上持有 interaction lock。写入返回错误后,它先刷新状态:如果进程已经退出,继续形成退出响应;只有仍活跃且是 ProcessFailed 才终止并释放 ID。这样 transport race 不会丢掉可用 exit code。

源码位置:codex-rs/core/src/unified_exec/process_manager.rs :: write_stdin

rust
Err(err) => {
    let status = self.refresh_process_state(process_id).await;
    if matches!(status, ProcessStatus::Exited { .. }) {
        status_after_write = Some(status);
    } else if matches!(err, UnifiedExecError::ProcessFailed { .. }) {
        process.terminate();
        self.release_process_id(process_id).await;
        return Err(err);
    } else {
        return Err(err);
    }
}

远程 UnknownProcess 或 StdinClosed 还会先把统一状态标记为 exited;这使后续 refresh 有机会走正常终态分支。

5. 旧句柄竞态 ​

poll 先从 store 克隆 Arc,等待期间条目可能被 refresh 或 terminate 移除。再次准备句柄时用 Arc::ptr_eq 校验;poll 手中的旧进程若已经退出,最终仍可返回 exit metadata,否则报告 Unknown。

源码位置:codex-rs/core/src/unified_exec/process_manager.rs :: prepare_process_handles、write_stdin

rust
if !Arc::ptr_eq(&entry.process, expected_process) {
    return Err(UnifiedExecError::UnknownProcessId { process_id });
}

6. 失败根因与资源 ​

6.1 首个根因 ​

fail_and_terminate 只写入第一个 failure message,之后终止底层 process;fail_process_with_message 如果已有 failure,则保留原根因而不被后续清理错误覆盖。

源码位置:codex-rs/core/src/unified_exec/process.rs :: fail_and_terminate、codex-rs/core/src/unified_exec/process_manager.rs :: fail_process_with_message

rust
pub(super) fn fail_and_terminate(&self, message: String) {
    let state = self.state_rx.borrow().clone();
    if state.failure_message.is_none() {
        let _ = self.state_tx.send_replace(state.failed(message));
    }
    self.terminate();
}

fn fail_process_with_message(process: &UnifiedExecProcess, message: String) -> UnifiedExecError {
    if let Some(message) = process.failure_message() {
        process.terminate();
        return UnifiedExecError::process_failed(message);
    }
    process.fail_and_terminate(message.clone());
    UnifiedExecError::process_failed(process.failure_message().unwrap_or(message))
}

6.2 Metrics收尾 ​

后台进程的 plugin metrics sidecar 由 ProcessEntry 和 exit watcher 共享。watcher 在终态屏障后 take 一次:成功分支用真实 exit code 完成测量;失败分支直接 drop,不把统一失败码 -1 当成插件正常退出结果。

源码位置:codex-rs/core/src/unified_exec/async_watcher.rs :: spawn_exit_watcher

rust
let plugin_metrics_sidecar = plugin_metrics_sidecar
    .as_ref()
    .and_then(take_plugin_metrics_sidecar);
if let Some(message) = process.failure_message() {
    drop(plugin_metrics_sidecar);
    emit_failed_exec_end_for_unified_exec(
        session_ref,
        turn_ref,
        call_id,
        command,
        cwd,
        Some(process_id.to_string()),
        plugin_attribution,
        transcript,
        String::new(),
        message,
        duration,
    )
    .await;
} else {
    let exit_code = process.exit_code().unwrap_or(-1);
    finish_and_track_measurements(
        plugin_metrics_sidecar,
        exit_code,
        &session_ref,
        &turn_ref,
        &call_id,
    )
    .await;
}

sidecar 的 Option::take 还防止首次 exec_command 与 watcher 同时观察到快速退出时重复完成测量。失败分类因此不仅决定事件内容,也决定哪些附属资源允许提交结果。

7. 四组验证 ​

7.1 Process局部状态 ​

process 模块的 5 项测试验证 UnknownProcess/StdinClosed 会标记 exited、第二个失败不会覆盖首个根因、远程 terminate 只有成功才改变状态,以及 executor 报告的 sandbox type 会被统一进程保留。

源码位置:codex-rs/core/src/unified_exec/process_tests.rs

text
cd codex-rs
cargo test -p codex-core --lib 'unified_exec::process_tests::' -- --test-threads=1

7.2 首次失败与grace ​

fallback 测试断言 Session 不可用时仍返回明确的 sandbox network proxy 消息;grace 测试在 exit 后 10ms 才取消 token,断言窗口能观察拒绝;未入 store 测试断言 fallback output 与 denial message 合并成唯一 failed item。

源码位置:

  • codex-rs/core/src/unified_exec/process_manager_tests.rs :: network_denial_fallback_message_names_sandbox_network_proxy
  • codex-rs/core/src/unified_exec/process_manager_tests.rs :: late_network_denial_grace_observes_cancellation_after_exit
  • codex-rs/core/src/unified_exec/process_manager_tests.rs :: failed_initial_end_for_unstored_process_uses_fallback_output
text
cd codex-rs
cargo test -p codex-core --lib network_denial_fallback_message_names_sandbox_network_proxy -- --test-threads=1
cargo test -p codex-core --lib late_network_denial_grace_observes_cancellation_after_exit -- --test-threads=1
cargo test -p codex-core --lib failed_initial_end_for_unstored_process_uses_fallback_output -- --test-threads=1

7.3 异步竞态 ​

exit_watcher_waits_for_late_network_denial_before_classifying_end 让 denial monitor 晚于 process exit,断言结束事件等待 monitor 后才分类;terminating_during_stdin_poll_returns_exited_response 验证 store 移除后旧 Arc 已退出仍能返回终态。

源码位置:codex-rs/core/src/unified_exec/async_watcher_tests.rs :: exit_watcher_waits_for_late_network_denial_before_classifying_end、codex-rs/core/src/unified_exec/mod_tests.rs :: terminating_during_stdin_poll_returns_exited_response

text
cd codex-rs
cargo test -p codex-core --lib unified_exec::async_watcher::tests::exit_watcher_waits_for_late_network_denial_before_classifying_end -- --test-threads=1
cargo test -p codex-core --lib terminating_during_stdin_poll_returns_exited_response -- --test-threads=1

7.4 真实网络拒绝 ​

两个集成测试分别让命令先进入后台 store,以及在首次等待内快速结束。两种输入都会经 managed proxy 触发拒绝,并断言最终状态 Failed、exit code -1、聚合输出包含 Network access,且只发布对应命令的结束事件。

源码位置:

  • codex-rs/core/tests/suite/unified_exec.rs :: unified_exec_network_denial_emits_failed_background_end_event
  • codex-rs/core/tests/suite/unified_exec.rs :: unified_exec_short_lived_network_denial_emits_failed_end_event
text
cd codex-rs
RUST_MIN_STACK=8388608 cargo test -p codex-core --test all unified_exec_network_denial_emits_failed_background_end_event -- --test-threads=1
RUST_MIN_STACK=8388608 cargo test -p codex-core --test all unified_exec_short_lived_network_denial_emits_failed_end_event -- --test-threads=1

这些测试没有传入真实 plugin metrics sidecar,因此失败分支 drop sidecar、成功分支按 exit code 完成测量的结论来自当前所有权源码,没有在本组测试中单独断言。

8. 故障排查 ​

text
rg -n "enum UnifiedExecError|SandboxDenied|with_output_collection_metadata" codex-rs/core/src/unified_exec
rg -n "fail_process_with_message|terminate_process_on_network_denial|refresh_process_state" codex-rs/core/src/unified_exec/process_manager.rs
rg -n "late_network_denial|terminating_during_stdin_poll|preserves_failure" codex-rs/core/src/unified_exec

看到没有 process_id 的错误,先查 reserved ID 是否释放;看到 sandbox denial,查 output 和 omission metadata;看到失败原因被覆盖,查 failure_message 首写规则;看到 Unknown,查第一次 store 查找、ptr_eq 和旧 Arc 是否已经退出。