From 7a735a85c83e119a7e7a49a96c3ecbcf36ff923d Mon Sep 17 00:00:00 2001 From: David Wiesen Date: Wed, 20 May 2026 09:53:58 -0700 Subject: [PATCH] Avoid UAC bootstrap for unelevated Windows sandbox --- codex-rs/core/src/exec.rs | 11 ++++++++ codex-rs/core/src/exec_tests.rs | 16 ++++++++++++ .../windows-sandbox-rs/src/elevated_impl.rs | 3 +++ codex-rs/windows-sandbox-rs/src/identity.rs | 26 ++++++++++++------- codex-rs/windows-sandbox-rs/src/spawn_prep.rs | 1 + 5 files changed, 47 insertions(+), 10 deletions(-) diff --git a/codex-rs/core/src/exec.rs b/codex-rs/core/src/exec.rs index fd5cd7bcdc..aacec85038 100644 --- a/codex-rs/core/src/exec.rs +++ b/codex-rs/core/src/exec.rs @@ -121,6 +121,14 @@ fn windows_sandbox_uses_elevated_backend( proxy_enforced || matches!(sandbox_level, WindowsSandboxLevel::Elevated) } +fn windows_sandbox_allows_setup_bootstrap( + sandbox_level: WindowsSandboxLevel, + proxy_enforced: bool, +) -> bool { + matches!(sandbox_level, WindowsSandboxLevel::Elevated) + || !windows_sandbox_uses_elevated_backend(sandbox_level, proxy_enforced) +} + #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] pub enum ExecCapturePolicy { /// Shell-like execs keep the historical output cap and timeout behavior. @@ -604,6 +612,8 @@ async fn exec_windows_sandbox( let sandbox_level = windows_sandbox_level; let proxy_enforced = network.is_some(); let use_elevated = windows_sandbox_uses_elevated_backend(sandbox_level, proxy_enforced); + let allow_setup_bootstrap = + windows_sandbox_allows_setup_bootstrap(sandbox_level, proxy_enforced); let additional_deny_write_paths = windows_sandbox_filesystem_overrides .map(|overrides| { overrides @@ -641,6 +651,7 @@ async fn exec_windows_sandbox( timeout_ms, use_private_desktop: windows_sandbox_private_desktop, proxy_enforced, + allow_setup_bootstrap, read_roots_override: elevated_read_roots_override.as_deref(), read_roots_include_platform_defaults: elevated_read_roots_include_platform_defaults, diff --git a/codex-rs/core/src/exec_tests.rs b/codex-rs/core/src/exec_tests.rs index 9d335a81c7..31e1692b81 100644 --- a/codex-rs/core/src/exec_tests.rs +++ b/codex-rs/core/src/exec_tests.rs @@ -425,6 +425,22 @@ fn windows_proxy_enforcement_uses_elevated_backend() { )); } +#[test] +fn windows_proxy_enforcement_only_bootstraps_setup_for_explicit_elevated_mode() { + assert!(windows_sandbox_allows_setup_bootstrap( + WindowsSandboxLevel::RestrictedToken, + /*proxy_enforced*/ false, + )); + assert!(!windows_sandbox_allows_setup_bootstrap( + WindowsSandboxLevel::RestrictedToken, + /*proxy_enforced*/ true, + )); + assert!(windows_sandbox_allows_setup_bootstrap( + WindowsSandboxLevel::Elevated, + /*proxy_enforced*/ false, + )); +} + #[test] fn windows_restricted_token_rejects_network_only_restrictions() { let policy = SandboxPolicy::ExternalSandbox { diff --git a/codex-rs/windows-sandbox-rs/src/elevated_impl.rs b/codex-rs/windows-sandbox-rs/src/elevated_impl.rs index 2c1c4f79ce..8cc79251b5 100644 --- a/codex-rs/windows-sandbox-rs/src/elevated_impl.rs +++ b/codex-rs/windows-sandbox-rs/src/elevated_impl.rs @@ -12,6 +12,7 @@ pub struct ElevatedSandboxCaptureRequest<'a> { pub timeout_ms: Option, pub use_private_desktop: bool, pub proxy_enforced: bool, + pub allow_setup_bootstrap: bool, pub read_roots_override: Option<&'a [PathBuf]>, pub read_roots_include_platform_defaults: bool, pub write_roots_override: Option<&'a [PathBuf]>, @@ -121,6 +122,7 @@ mod windows_impl { timeout_ms, use_private_desktop, proxy_enforced, + allow_setup_bootstrap, read_roots_override, read_roots_include_platform_defaults, write_roots_override, @@ -148,6 +150,7 @@ mod windows_impl { write_roots_override, deny_write_paths_override, proxy_enforced, + allow_setup_bootstrap, )?; // Build capability SID for ACL grants. if matches!( diff --git a/codex-rs/windows-sandbox-rs/src/identity.rs b/codex-rs/windows-sandbox-rs/src/identity.rs index 84e72341e2..9922232066 100644 --- a/codex-rs/windows-sandbox-rs/src/identity.rs +++ b/codex-rs/windows-sandbox-rs/src/identity.rs @@ -1,6 +1,10 @@ use crate::dpapi; use crate::logging::debug_log; use crate::policy::SandboxPolicy; +use crate::setup::SandboxNetworkIdentity; +use crate::setup::SandboxUserRecord; +use crate::setup::SandboxUsersFile; +use crate::setup::SetupMarker; use crate::setup::gather_read_roots; use crate::setup::gather_write_roots; use crate::setup::offline_proxy_settings_from_env; @@ -8,15 +12,11 @@ use crate::setup::run_elevated_setup; use crate::setup::run_setup_refresh_with_overrides; use crate::setup::sandbox_users_path; use crate::setup::setup_marker_path; -use crate::setup::SandboxNetworkIdentity; -use crate::setup::SandboxUserRecord; -use crate::setup::SandboxUsersFile; -use crate::setup::SetupMarker; -use anyhow::anyhow; use anyhow::Context; use anyhow::Result; -use base64::engine::general_purpose::STANDARD as BASE64_STANDARD; +use anyhow::anyhow; use base64::Engine; +use base64::engine::general_purpose::STANDARD as BASE64_STANDARD; use std::collections::HashMap; use std::fs; use std::path::Path; @@ -141,6 +141,7 @@ pub fn require_logon_sandbox_creds( write_roots_override: Option<&[PathBuf]>, deny_write_paths_override: &[PathBuf], proxy_enforced: bool, + allow_setup_bootstrap: bool, ) -> Result { let sandbox_dir = crate::setup::sandbox_dir(codex_home); let needed_read = read_roots_override @@ -150,8 +151,7 @@ pub fn require_logon_sandbox_creds( .map(<[PathBuf]>::to_vec) .unwrap_or_else(|| gather_write_roots(policy, policy_cwd, command_cwd, env_map)); let network_identity = SandboxNetworkIdentity::from_policy(policy, proxy_enforced); - let desired_offline_proxy_settings = - offline_proxy_settings_from_env(env_map, network_identity); + let desired_offline_proxy_settings = offline_proxy_settings_from_env(env_map, network_identity); // NOTE: Do not add CODEX_HOME/.sandbox to `needed_write`; it must remain non-writable by the // restricted capability token. The setup helper's `lock_sandbox_dir` is responsible for // granting the sandbox group access to this directory without granting the capability SID. @@ -167,8 +167,9 @@ pub fn require_logon_sandbox_creds( } else { let selected = select_identity(network_identity, codex_home)?; if selected.is_none() { - setup_reason = - Some("sandbox users missing or incompatible with marker version".to_string()); + setup_reason = Some( + "sandbox users missing or incompatible with marker version".to_string(), + ); } selected } @@ -188,6 +189,11 @@ pub fn require_logon_sandbox_creds( } else { crate::logging::log_note("sandbox setup required", Some(&sandbox_dir)); } + if !allow_setup_bootstrap { + return Err(anyhow!( + "windows unelevated sandbox cannot auto-run elevated setup to enforce restricted network access; rerun Windows sandbox setup in elevated mode or enable network access" + )); + } run_elevated_setup( crate::setup::SandboxSetupRequest { policy, diff --git a/codex-rs/windows-sandbox-rs/src/spawn_prep.rs b/codex-rs/windows-sandbox-rs/src/spawn_prep.rs index 1b3704d6a2..959b918c55 100644 --- a/codex-rs/windows-sandbox-rs/src/spawn_prep.rs +++ b/codex-rs/windows-sandbox-rs/src/spawn_prep.rs @@ -299,6 +299,7 @@ pub(crate) fn prepare_elevated_spawn_context( write_roots_override, &deny_write_paths, /*proxy_enforced*/ false, + /*allow_setup_bootstrap*/ true, )?; let caps = load_or_create_cap_sids(codex_home)?; let (psid_to_use, cap_sids) = match &common.policy {