Preserve Windows sandbox identity during credential retry (#29624)

## Summary

- recognize stale Windows sandbox credentials from both runner logon and
child startup failures
- refresh credentials once without changing the original command,
permissions, file rules, desktop mode, or managed-network identity
- add a Windows regression test that forces error 1312 and inspects the
real retry arguments

## Why

Elevated unified exec starts commands in two steps:

```text
Codex -> sandbox command runner -> requested command
```

Either process start can fail when Windows invalidates the sandbox logon
session. The child-side failure was previously returned as text, so the
parent could not reliably recognize Windows error 1312.

The existing retry also refreshed credentials with `proxy_enforced =
false`, even when the original request used managed networking. That
could change the selected Windows sandbox identity from offline to
online during the retry.

## How

- carry the failure stage and numeric Windows error code through the
command-runner IPC protocol
- preserve native `CreateProcessAsUserW` error codes instead of parsing
error messages
- keep every retry-sensitive field in one request and use it for both
attempts
- retry exactly once after refreshing credentials, then return the
second failure
- share the retry rule with the elevated capture path

The Windows test injects error 1312 on both attempts and verifies:

- two spawn attempts and one credential refresh
- stale credentials are replaced by refreshed credentials
- both attempts receive the same command, environment, cwd, permissions,
roots, deny paths, TTY settings, and private-desktop mode
- credential refresh receives the original `proxy_enforced` value

## Tests

- `just test -p codex-windows-sandbox`
- the new Windows-only regression test is included in the Windows
nextest CI archive
This commit is contained in:
jif authored and GitHub committed 2026-06-24 20:20:52 +01:00
1 parent 5013d10824
commit 4907f0c2c3
9 files changed
+488 -103

No files matched your search

@@ -12,7 +12,7 @@ use crate::ipc_framed::Message;
use crate::ipc_framed::SpawnRequest;
use crate::resolved_permissions::ResolvedWindowsSandboxPermissions;
use crate::runner_client::RunnerTransport;
use crate::runner_client::is_stale_sandbox_creds_error;
use crate::runner_client::retry_runner_spawn_once;
use crate::runner_client::spawn_runner_transport;
use crate::spawn_prep::prepare_elevated_spawn_context_for_permissions;
use anyhow::Result;
@@ -27,20 +27,79 @@ use tokio::sync::broadcast;
use tokio::sync::mpsc;
use tokio::sync::oneshot;
async fn spawn_runner_transport_task(
struct RunnerTransportRequest {
permissions: ResolvedWindowsSandboxPermissions,
codex_home: PathBuf,
cwd: PathBuf,
sandbox_creds: SandboxCreds,
env_map: HashMap<String, String>,
logs_base_dir: Option<PathBuf>,
spawn_request: SpawnRequest,
read_roots_override: Option<Vec<PathBuf>>,
read_roots_include_platform_defaults: bool,
write_roots_override: Option<Vec<PathBuf>>,
deny_read_paths_override: Vec<PathBuf>,
deny_write_paths_override: Vec<PathBuf>,
proxy_enforced: bool,
proxy_settings_mode: crate::WindowsSandboxProxySettingsMode,
}
fn spawn_runner_transport_with_retry<T>(
sandbox_creds: SandboxCreds,
request: &RunnerTransportRequest,
mut spawn: impl FnMut(&Path, &Path, &SandboxCreds, Option<&Path>, SpawnRequest) -> Result<T>,
refresh: impl FnOnce(
&ResolvedWindowsSandboxPermissions,
&Path,
&HashMap<String, String>,
&Path,
Option<&[PathBuf]>,
bool,
Option<&[PathBuf]>,
&[PathBuf],
&[PathBuf],
bool,
crate::WindowsSandboxProxySettingsMode,
) -> Result<SandboxCreds>,
) -> Result<T> {
retry_runner_spawn_once(
sandbox_creds,
|sandbox_creds| {
spawn(
&request.codex_home,
&request.cwd,
&sandbox_creds,
request.logs_base_dir.as_deref(),
request.spawn_request.clone(),
)
},
|| {
refresh(
&request.permissions,
&request.cwd,
&request.env_map,
&request.codex_home,
request.read_roots_override.as_deref(),
request.read_roots_include_platform_defaults,
request.write_roots_override.as_deref(),
&request.deny_read_paths_override,
&request.deny_write_paths_override,
request.proxy_enforced,
request.proxy_settings_mode,
)
},
)
}
async fn spawn_runner_transport_task(
sandbox_creds: SandboxCreds,
request: RunnerTransportRequest,
) -> Result<RunnerTransport> {
tokio::task::spawn_blocking(move || -> Result<_> {
spawn_runner_transport(
&codex_home,
&cwd,
&sandbox_creds,
logs_base_dir.as_deref(),
spawn_request,
spawn_runner_transport_with_retry(
sandbox_creds,
&request,
spawn_runner_transport,
refresh_logon_sandbox_creds,
)
})
.await
@@ -95,59 +154,36 @@ pub(crate) async fn spawn_windows_sandbox_session_elevated_for_permission_profil
proxy_settings_mode,
)?;
let spawn_request = SpawnRequest {
command: command.clone(),
cwd: cwd.to_path_buf(),
env: env_map.clone(),
permission_profile: permission_profile.clone(),
workspace_roots: workspace_roots.to_vec(),
codex_home: elevated.sandbox_base.clone(),
real_codex_home: codex_home.to_path_buf(),
cap_sids: elevated.cap_sids.clone(),
timeout_ms,
tty,
stdin_open,
use_private_desktop,
};
let codex_home = codex_home.to_path_buf();
let cwd = cwd.to_path_buf();
let sandbox_creds = elevated.sandbox_creds;
let logs_base_dir = elevated.logs_base_dir.clone();
let transport = match spawn_runner_transport_task(
codex_home.clone(),
cwd.clone(),
sandbox_creds,
logs_base_dir.clone(),
spawn_request.clone(),
)
.await
{
Ok(transport) => transport,
Err(err) if is_stale_sandbox_creds_error(&err) => {
let sandbox_creds = refresh_logon_sandbox_creds(
&permissions,
&cwd,
&env_map,
&codex_home,
read_roots_override,
read_roots_include_platform_defaults,
write_roots_override,
&deny_read_paths_override,
&deny_write_paths_override,
/*proxy_enforced*/ false,
proxy_settings_mode,
)?;
spawn_runner_transport_task(
codex_home,
cwd,
sandbox_creds,
logs_base_dir,
spawn_request,
)
.await?
}
Err(err) => return Err(err),
let request = RunnerTransportRequest {
permissions,
codex_home: codex_home.to_path_buf(),
cwd: cwd.to_path_buf(),
env_map: env_map.clone(),
logs_base_dir: elevated.logs_base_dir,
spawn_request: SpawnRequest {
command,
cwd: cwd.to_path_buf(),
env: env_map,
permission_profile: permission_profile.clone(),
workspace_roots: workspace_roots.to_vec(),
codex_home: elevated.sandbox_base,
real_codex_home: codex_home.to_path_buf(),
cap_sids: elevated.cap_sids,
timeout_ms,
tty,
stdin_open,
use_private_desktop,
},
read_roots_override: read_roots_override.map(<[PathBuf]>::to_vec),
read_roots_include_platform_defaults,
write_roots_override: write_roots_override.map(<[PathBuf]>::to_vec),
deny_read_paths_override,
deny_write_paths_override,
proxy_enforced,
proxy_settings_mode,
};
let transport = spawn_runner_transport_task(sandbox_creds, request).await?;
let (pipe_write, pipe_read) = transport.into_files();
let (writer_tx, writer_rx) = mpsc::channel::<Vec<u8>>(128);
@@ -197,3 +233,7 @@ pub(crate) async fn spawn_windows_sandbox_session_elevated_for_permission_profil
stdin_open,
))
}
#[cfg(test)]
#[path = "elevated_tests.rs"]
mod tests;
@@ -0,0 +1,192 @@
use super::RunnerTransportRequest;
use super::spawn_runner_transport_with_retry;
use crate::WindowsSandboxProxySettingsMode;
use crate::identity::SandboxCreds;
use crate::ipc_framed::ErrorPayload;
use crate::ipc_framed::ErrorStage;
use crate::ipc_framed::SpawnRequest;
use crate::resolved_permissions::ResolvedWindowsSandboxPermissions;
use crate::runner_client::RunnerStartupError;
use codex_protocol::models::PermissionProfile;
use codex_utils_absolute_path::AbsolutePathBuf;
use pretty_assertions::assert_eq;
use serde_json::Value;
use std::cell::Cell;
use std::cell::RefCell;
use std::collections::HashMap;
use std::path::Path;
use std::path::PathBuf;
use windows_sys::Win32::Foundation::ERROR_NO_SUCH_LOGON_SESSION;
#[derive(Debug, Clone, PartialEq, Eq)]
struct SpawnObservation {
codex_home: PathBuf,
cwd: PathBuf,
username: String,
password: String,
logs_base_dir: Option<PathBuf>,
spawn_request: Value,
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct RefreshObservation {
permissions: ResolvedWindowsSandboxPermissions,
cwd: PathBuf,
env_map: HashMap<String, String>,
codex_home: PathBuf,
read_roots_override: Option<Vec<PathBuf>>,
read_roots_include_platform_defaults: bool,
write_roots_override: Option<Vec<PathBuf>>,
deny_read_paths_override: Vec<PathBuf>,
deny_write_paths_override: Vec<PathBuf>,
proxy_enforced: bool,
proxy_settings_mode: WindowsSandboxProxySettingsMode,
}
#[test]
fn retry_uses_original_unified_exec_request_and_stops_after_second_failure() {
let workspace_root = AbsolutePathBuf::from_absolute_path(PathBuf::from(r"C:\workspace"))
.expect("absolute workspace root");
let permission_profile = PermissionProfile::workspace_write();
let permissions =
ResolvedWindowsSandboxPermissions::try_from_permission_profile_for_workspace_roots(
&permission_profile,
std::slice::from_ref(&workspace_root),
)
.expect("resolved permissions");
let env_map = HashMap::from([
("Path".to_string(), r"C:\tools".to_string()),
(
"HTTPS_PROXY".to_string(),
"http://localhost:1234".to_string(),
),
]);
let request = RunnerTransportRequest {
permissions: permissions.clone(),
codex_home: PathBuf::from(r"C:\Users\codex"),
cwd: PathBuf::from(r"C:\workspace"),
env_map: env_map.clone(),
logs_base_dir: Some(PathBuf::from(r"C:\Users\codex\.sandbox")),
spawn_request: SpawnRequest {
command: vec!["pwsh.exe".to_string(), "-NoProfile".to_string()],
cwd: PathBuf::from(r"C:\workspace"),
env: env_map.clone(),
permission_profile,
workspace_roots: vec![workspace_root],
codex_home: PathBuf::from(r"C:\Users\codex\.sandbox"),
real_codex_home: PathBuf::from(r"C:\Users\codex"),
cap_sids: vec!["S-1-15-3-1024-1".to_string()],
timeout_ms: Some(5_000),
tty: true,
stdin_open: true,
use_private_desktop: true,
},
read_roots_override: Some(vec![PathBuf::from(r"C:\workspace\read")]),
read_roots_include_platform_defaults: true,
write_roots_override: Some(vec![PathBuf::from(r"C:\workspace\write")]),
deny_read_paths_override: vec![PathBuf::from(r"C:\secrets")],
deny_write_paths_override: vec![PathBuf::from(r"C:\workspace\.codex")],
proxy_enforced: true,
proxy_settings_mode: WindowsSandboxProxySettingsMode::Preserve,
};
let expected_spawn_request =
serde_json::to_value(&request.spawn_request).expect("serialize spawn request");
let expected_refresh = RefreshObservation {
permissions,
cwd: request.cwd.clone(),
env_map,
codex_home: request.codex_home.clone(),
read_roots_override: request.read_roots_override.clone(),
read_roots_include_platform_defaults: true,
write_roots_override: request.write_roots_override.clone(),
deny_read_paths_override: request.deny_read_paths_override.clone(),
deny_write_paths_override: request.deny_write_paths_override.clone(),
proxy_enforced: true,
proxy_settings_mode: WindowsSandboxProxySettingsMode::Preserve,
};
let spawn_observations = RefCell::new(Vec::new());
let refresh_observations = RefCell::new(Vec::new());
let spawn_attempts = Cell::new(0);
let result = spawn_runner_transport_with_retry(
SandboxCreds {
username: "stale".to_string(),
password: "old".to_string(),
},
&request,
|codex_home, cwd, sandbox_creds, logs_base_dir, spawn_request| {
spawn_observations.borrow_mut().push(SpawnObservation {
codex_home: codex_home.to_path_buf(),
cwd: cwd.to_path_buf(),
username: sandbox_creds.username.clone(),
password: sandbox_creds.password.clone(),
logs_base_dir: logs_base_dir.map(Path::to_path_buf),
spawn_request: serde_json::to_value(spawn_request)
.expect("serialize spawn request"),
});
spawn_attempts.set(spawn_attempts.get() + 1);
Err::<(), _>(anyhow::Error::new(RunnerStartupError::new(ErrorPayload {
message: format!("spawn attempt {} failed", spawn_attempts.get()),
stage: ErrorStage::SpawnChild,
windows_error_code: Some(ERROR_NO_SUCH_LOGON_SESSION),
})))
},
|permissions,
cwd,
env_map,
codex_home,
read_roots_override,
read_roots_include_platform_defaults,
write_roots_override,
deny_read_paths_override,
deny_write_paths_override,
proxy_enforced,
proxy_settings_mode| {
refresh_observations.borrow_mut().push(RefreshObservation {
permissions: permissions.clone(),
cwd: cwd.to_path_buf(),
env_map: env_map.clone(),
codex_home: codex_home.to_path_buf(),
read_roots_override: read_roots_override.map(<[PathBuf]>::to_vec),
read_roots_include_platform_defaults,
write_roots_override: write_roots_override.map(<[PathBuf]>::to_vec),
deny_read_paths_override: deny_read_paths_override.to_vec(),
deny_write_paths_override: deny_write_paths_override.to_vec(),
proxy_enforced,
proxy_settings_mode,
});
Ok(SandboxCreds {
username: "refreshed".to_string(),
password: "new".to_string(),
})
},
);
let err = result.expect_err("second spawn should fail");
assert_eq!(
"runner failed during SpawnChild: spawn attempt 2 failed (Windows error 1312)",
err.to_string()
);
assert_eq!(
vec![
SpawnObservation {
codex_home: request.codex_home.clone(),
cwd: request.cwd.clone(),
username: "stale".to_string(),
password: "old".to_string(),
logs_base_dir: request.logs_base_dir.clone(),
spawn_request: expected_spawn_request.clone(),
},
SpawnObservation {
codex_home: request.codex_home.clone(),
cwd: request.cwd.clone(),
username: "refreshed".to_string(),
password: "new".to_string(),
logs_base_dir: request.logs_base_dir,
spawn_request: expected_spawn_request,
},
],
*spawn_observations.borrow()
);
assert_eq!(vec![expected_refresh], *refresh_observations.borrow());
}