mirror of
https://github.com/openai/codex.git
synced 2026-11-13 06:45:07 +00:00
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:
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());
|
||||
}
|
||||
Reference in new issue
Block a user