diff --git a/codex-rs/windows-sandbox-rs/Cargo.toml b/codex-rs/windows-sandbox-rs/Cargo.toml index e7ca02fed2..f074ef5e13 100644 --- a/codex-rs/windows-sandbox-rs/Cargo.toml +++ b/codex-rs/windows-sandbox-rs/Cargo.toml @@ -80,6 +80,7 @@ features = [ "Win32_Security_Cryptography", "Win32_Security_Authentication_Identity", "Win32_Graphics_Gdi", + "Win32_NetworkManagement_WNet", "Win32_System_StationsAndDesktops", "Win32_UI_WindowsAndMessaging", "Win32_UI_Shell", diff --git a/codex-rs/windows-sandbox-rs/src/elevated_impl.rs b/codex-rs/windows-sandbox-rs/src/elevated_impl.rs index 327425bd07..554139d2f9 100644 --- a/codex-rs/windows-sandbox-rs/src/elevated_impl.rs +++ b/codex-rs/windows-sandbox-rs/src/elevated_impl.rs @@ -38,6 +38,7 @@ mod windows_impl { use crate::logging::log_note; use crate::logging::log_start; use crate::logging::log_success; + use crate::path_normalization::normalize_command_cwd; use crate::policy::SandboxPolicy; use crate::policy::parse_policy; use crate::token::convert_string_sid_to_sid; @@ -241,11 +242,12 @@ mod windows_impl { write_roots_override, deny_write_paths_override, } = request; + let normalized_cwd = normalize_command_cwd(cwd); let policy = parse_policy(policy_json_or_preset)?; normalize_null_device_env(&mut env_map); ensure_non_interactive_pager(&mut env_map); inherit_path_env(&mut env_map); - inject_git_safe_directory(&mut env_map, cwd, None); + inject_git_safe_directory(&mut env_map, &normalized_cwd, None); // Use a temp-based log dir that the sandbox user can write. let sandbox_base = codex_home.join(".sandbox"); ensure_codex_home_exists(&sandbox_base)?; @@ -255,7 +257,7 @@ mod windows_impl { let sandbox_creds = require_logon_sandbox_creds( &policy, sandbox_policy_cwd, - cwd, + &normalized_cwd, &env_map, codex_home, read_roots_override, @@ -289,7 +291,7 @@ mod windows_impl { psid, vec![ caps.workspace, - crate::cap::workspace_cap_sid_for_cwd(codex_home, cwd)?, + crate::cap::workspace_cap_sid_for_cwd(codex_home, &normalized_cwd)?, ], ) } @@ -321,7 +323,7 @@ mod windows_impl { ); let mut cmdline_vec: Vec = to_wide(&runner_full_cmd); let exe_w: Vec = to_wide(&runner_cmdline); - let cwd_w: Vec = to_wide(cwd); + let cwd_w: Vec = to_wide(sandbox_base.as_path()); // Minimal CPWL launch: inherit env, no desktop override, no handle inheritance. let env_block: Option> = None; @@ -339,7 +341,7 @@ mod windows_impl { "runner launch: exe={} cmdline={} cwd={}", runner_exe.display(), runner_full_cmd, - cwd.display() + sandbox_base.display() ), logs_base_dir, ); @@ -413,7 +415,7 @@ mod windows_impl { message: Message::SpawnRequest { payload: Box::new(SpawnRequest { command: command.clone(), - cwd: cwd.to_path_buf(), + cwd: normalized_cwd.clone(), env: env_map.clone(), policy_json_or_preset: policy_json_or_preset.to_string(), sandbox_policy_cwd: sandbox_policy_cwd.to_path_buf(), diff --git a/codex-rs/windows-sandbox-rs/src/path_normalization.rs b/codex-rs/windows-sandbox-rs/src/path_normalization.rs index 0d3fc9ddca..4ffc33d1ce 100644 --- a/codex-rs/windows-sandbox-rs/src/path_normalization.rs +++ b/codex-rs/windows-sandbox-rs/src/path_normalization.rs @@ -1,10 +1,10 @@ -use anyhow::Result; use std::path::Path; use std::path::PathBuf; -use windows_sys::Win32::Storage::FileSystem::DRIVE_REMOTE; -use windows_sys::Win32::Storage::FileSystem::GetDriveTypeW; use crate::winutil::to_wide; +use windows_sys::Win32::Foundation::ERROR_MORE_DATA; +use windows_sys::Win32::Foundation::NO_ERROR; +use windows_sys::Win32::NetworkManagement::WNet::WNetGetConnectionW; pub fn canonicalize_path(path: &Path) -> PathBuf { dunce::canonicalize(path).unwrap_or_else(|_| path.to_path_buf()) @@ -17,41 +17,54 @@ pub fn canonical_path_key(path: &Path) -> String { .to_ascii_lowercase() } -pub fn ensure_windows_sandbox_local_path(path: &Path, context: &str) -> Result<()> { - let raw = path.to_string_lossy(); - let normalized = normalize_windows_device_path(&raw).unwrap_or_else(|| raw.into_owned()); - if normalized.starts_with(r"\\") { - anyhow::bail!( - "windows sandbox does not support {context} on UNC or mapped network paths: {}. Use a local drive workspace or run without the Windows sandbox for this session.", - path.display() - ); - } - - if let Some(root) = windows_drive_root(&normalized) { - let drive_type = unsafe { GetDriveTypeW(to_wide(&root).as_ptr()) }; - if drive_type == DRIVE_REMOTE { - anyhow::bail!( - "windows sandbox does not support {context} on mapped network drives: {}. Use the underlying local drive path or run without the Windows sandbox for this session.", - path.display() - ); - } - } - - Ok(()) +pub fn normalize_command_cwd(path: &Path) -> PathBuf { + let simplified = dunce::simplified(path).to_path_buf(); + normalize_mapped_drive_path_with(&simplified, mapped_drive_remote_root).unwrap_or(simplified) } -fn windows_drive_root(path: &str) -> Option { - let bytes = path.as_bytes(); - if bytes.len() >= 3 && bytes[0].is_ascii_alphabetic() && bytes[1] == b':' { - return Some(format!("{}:\\", path[..1].to_ascii_uppercase())); +fn normalize_mapped_drive_path_with(path: &Path, resolve_remote_root: F) -> Option +where + F: Fn(&str) -> Option, +{ + let raw = path.to_string_lossy(); + let bytes = raw.as_bytes(); + if bytes.len() < 2 || !bytes[0].is_ascii_alphabetic() || bytes[1] != b':' { + return None; + } + + let drive = raw[..2].to_ascii_uppercase(); + let remote_root = resolve_remote_root(&drive)?; + let suffix = raw[2..].trim_start_matches(['\\', '/']); + let mut normalized = PathBuf::from(remote_root); + if !suffix.is_empty() { + normalized.push(suffix); + } + Some(normalized) +} + +fn mapped_drive_remote_root(drive: &str) -> Option { + let drive_wide = to_wide(drive); + let mut len = 260u32; + + loop { + let mut buf = vec![0u16; len as usize]; + let status = unsafe { WNetGetConnectionW(drive_wide.as_ptr(), buf.as_mut_ptr(), &mut len) }; + match status { + NO_ERROR => { + let end = buf.iter().position(|ch| *ch == 0).unwrap_or(buf.len()); + return String::from_utf16(&buf[..end]).ok(); + } + ERROR_MORE_DATA => continue, + _ => return None, + } } - None } #[cfg(test)] mod tests { use super::canonical_path_key; - use super::windows_drive_root; + use super::normalize_command_cwd; + use super::normalize_mapped_drive_path_with; use pretty_assertions::assert_eq; use std::path::Path; @@ -67,9 +80,23 @@ mod tests { } #[test] - fn windows_drive_root_extracts_drive_prefix() { - assert_eq!(windows_drive_root(r"l:\cs-web"), Some(r"L:\".to_string())); - assert_eq!(windows_drive_root(r"C:/repo"), Some(r"C:\".to_string())); - assert_eq!(windows_drive_root(r"\\video1\node\cs-web"), None); + fn mapped_drive_paths_expand_to_unc_roots() { + let path = Path::new(r"L:\cs-web\context"); + let normalized = normalize_mapped_drive_path_with(path, |drive| { + (drive == "L:").then(|| r"\\video1\node".to_string()) + }); + assert_eq!( + normalized, + Some(PathBuf::from(r"\\video1\node\cs-web\context")) + ); + } + + #[test] + fn local_paths_are_left_alone() { + let path = Path::new(r"C:\Users\Dev\Repo"); + assert_eq!( + normalize_command_cwd(path), + PathBuf::from(r"C:\Users\Dev\Repo") + ); } } diff --git a/codex-rs/windows-sandbox-rs/src/setup_orchestrator.rs b/codex-rs/windows-sandbox-rs/src/setup_orchestrator.rs index b608c5155b..65f8aa3176 100644 --- a/codex-rs/windows-sandbox-rs/src/setup_orchestrator.rs +++ b/codex-rs/windows-sandbox-rs/src/setup_orchestrator.rs @@ -15,7 +15,7 @@ use crate::allow::compute_allow_paths; use crate::helper_materialization::helper_bin_dir; use crate::logging::log_note; use crate::path_normalization::canonical_path_key; -use crate::path_normalization::ensure_windows_sandbox_local_path; +use crate::path_normalization::normalize_command_cwd; use crate::policy::SandboxPolicy; use crate::setup_error::SetupErrorCode; use crate::setup_error::SetupFailure; @@ -166,24 +166,30 @@ fn run_setup_refresh_inner( ) { return Ok(()); } - let (read_roots, write_roots) = build_payload_roots(&request, &overrides); - let deny_write_paths = build_payload_deny_write_paths(&request, overrides.deny_write_paths); - ensure_windows_sandbox_local_path(request.command_cwd, "sandbox working directory")?; - for root in &write_roots { - ensure_windows_sandbox_local_path(root, "sandbox writable root")?; - } - for path in &deny_write_paths { - ensure_windows_sandbox_local_path(path, "sandbox protected path")?; - } - let network_identity = - SandboxNetworkIdentity::from_policy(request.policy, request.proxy_enforced); - let offline_proxy_settings = offline_proxy_settings_from_env(request.env_map, network_identity); + let normalized_command_cwd = normalize_command_cwd(request.command_cwd); + let normalized_request = SandboxSetupRequest { + policy: request.policy, + policy_cwd: request.policy_cwd, + command_cwd: &normalized_command_cwd, + env_map: request.env_map, + codex_home: request.codex_home, + proxy_enforced: request.proxy_enforced, + }; + let (read_roots, write_roots) = build_payload_roots(&normalized_request, &overrides); + let deny_write_paths = + build_payload_deny_write_paths(&normalized_request, overrides.deny_write_paths); + let network_identity = SandboxNetworkIdentity::from_policy( + normalized_request.policy, + normalized_request.proxy_enforced, + ); + let offline_proxy_settings = + offline_proxy_settings_from_env(normalized_request.env_map, network_identity); let payload = ElevationPayload { version: SETUP_VERSION, offline_username: OFFLINE_USERNAME.to_string(), online_username: ONLINE_USERNAME.to_string(), - codex_home: request.codex_home.to_path_buf(), - command_cwd: request.command_cwd.to_path_buf(), + codex_home: normalized_request.codex_home.to_path_buf(), + command_cwd: normalized_request.command_cwd.to_path_buf(), read_roots, write_roots, deny_write_paths, @@ -206,14 +212,14 @@ fn run_setup_refresh_inner( cwd.display(), b64.len() ), - Some(&sandbox_dir(request.codex_home)), + Some(&sandbox_dir(normalized_request.codex_home)), ); let status = cmd .status() .map_err(|e| { log_note( &format!("setup refresh: failed to spawn {}: {e}", exe.display()), - Some(&sandbox_dir(request.codex_home)), + Some(&sandbox_dir(normalized_request.codex_home)), ); e }) @@ -221,7 +227,7 @@ fn run_setup_refresh_inner( if !status.success() { log_note( &format!("setup refresh: exited with status {status:?}"), - Some(&sandbox_dir(request.codex_home)), + Some(&sandbox_dir(normalized_request.codex_home)), ); return Err(anyhow!("setup refresh failed with status {status}")); } diff --git a/codex-rs/windows-sandbox-rs/src/spawn_prep.rs b/codex-rs/windows-sandbox-rs/src/spawn_prep.rs index a106f4b0e8..569576b764 100644 --- a/codex-rs/windows-sandbox-rs/src/spawn_prep.rs +++ b/codex-rs/windows-sandbox-rs/src/spawn_prep.rs @@ -13,7 +13,7 @@ use crate::identity::SandboxCreds; use crate::identity::require_logon_sandbox_creds; use crate::logging::log_start; use crate::path_normalization::canonicalize_path; -use crate::path_normalization::ensure_windows_sandbox_local_path; +use crate::path_normalization::normalize_command_cwd; use crate::policy::SandboxPolicy; use crate::policy::parse_policy; use crate::sandbox_utils::ensure_codex_home_exists; @@ -104,7 +104,7 @@ fn prepare_spawn_context_common( anyhow::bail!("DangerFullAccess and ExternalSandbox are not supported for sandboxing") } - ensure_windows_sandbox_local_path(cwd, "sandbox working directory")?; + let normalized_cwd = normalize_command_cwd(cwd); normalize_null_device_env(env_map); ensure_non_interactive_pager(env_map); @@ -112,7 +112,7 @@ fn prepare_spawn_context_common( inherit_path_env(env_map); } if add_git_safe_directory { - inject_git_safe_directory(env_map, cwd); + inject_git_safe_directory(env_map, &normalized_cwd); } ensure_codex_home_exists(codex_home)?; @@ -125,7 +125,7 @@ fn prepare_spawn_context_common( Ok(SpawnContext { policy, - current_dir: cwd.to_path_buf(), + current_dir: normalized_cwd, sandbox_base, logs_base_dir, is_workspace_write, @@ -171,7 +171,8 @@ pub(crate) fn prepare_legacy_session_security( } SandboxPolicy::WorkspaceWrite { .. } => { let psid_generic = LocalSid::from_string(&caps.workspace)?; - let workspace_sid = workspace_cap_sid_for_cwd(codex_home, cwd)?; + let workspace_sid = + workspace_cap_sid_for_cwd(codex_home, &normalize_command_cwd(cwd))?; let psid_workspace = LocalSid::from_string(&workspace_sid)?; let base = get_current_token_for_restriction()?; let h_token = create_workspace_write_token_with_caps_from( @@ -285,12 +286,6 @@ pub(crate) fn prepare_elevated_spawn_context( ); let write_roots: Vec = allow.into_iter().collect(); let deny_write_paths: Vec = deny.into_iter().collect(); - for root in &write_roots { - ensure_windows_sandbox_local_path(root, "sandbox writable root")?; - } - for path in &deny_write_paths { - ensure_windows_sandbox_local_path(path, "sandbox protected path")?; - } let write_roots_override = if common.is_workspace_write { Some(write_roots.as_slice()) } else { @@ -299,7 +294,7 @@ pub(crate) fn prepare_elevated_spawn_context( let sandbox_creds = require_logon_sandbox_creds( &common.policy, sandbox_policy_cwd, - cwd, + &common.current_dir, env_map, codex_home, /*read_roots_override*/ None, @@ -314,7 +309,7 @@ pub(crate) fn prepare_elevated_spawn_context( vec![caps.readonly.clone()], ), SandboxPolicy::WorkspaceWrite { .. } => { - let cap_sid = workspace_cap_sid_for_cwd(codex_home, cwd)?; + let cap_sid = workspace_cap_sid_for_cwd(codex_home, &common.current_dir)?; ( LocalSid::from_string(&caps.workspace)?, vec![caps.workspace.clone(), cap_sid], diff --git a/codex-rs/windows-sandbox-rs/src/unified_exec/backends/elevated.rs b/codex-rs/windows-sandbox-rs/src/unified_exec/backends/elevated.rs index 0ed408fbfe..3a88601296 100644 --- a/codex-rs/windows-sandbox-rs/src/unified_exec/backends/elevated.rs +++ b/codex-rs/windows-sandbox-rs/src/unified_exec/backends/elevated.rs @@ -42,7 +42,7 @@ pub(crate) async fn spawn_windows_sandbox_session_elevated( let spawn_request = SpawnRequest { command: command.clone(), - cwd: cwd.to_path_buf(), + cwd: elevated.common.current_dir.clone(), env: env_map.clone(), policy_json_or_preset: policy_json_or_preset.to_string(), sandbox_policy_cwd: sandbox_policy_cwd.to_path_buf(), @@ -55,12 +55,16 @@ pub(crate) async fn spawn_windows_sandbox_session_elevated( use_private_desktop, }; let codex_home = codex_home.to_path_buf(); - let cwd = cwd.to_path_buf(); + let runner_cwd = elevated.common.sandbox_base.clone(); let sandbox_creds = elevated.sandbox_creds.clone(); let logs_base_dir = elevated.common.logs_base_dir.clone(); let transport = tokio::task::spawn_blocking(move || -> Result<_> { - let mut transport = - spawn_runner_transport(&codex_home, &cwd, &sandbox_creds, logs_base_dir.as_deref())?; + let mut transport = spawn_runner_transport( + &codex_home, + &runner_cwd, + &sandbox_creds, + logs_base_dir.as_deref(), + )?; transport.send_spawn_request(spawn_request)?; transport.read_spawn_ready()?; Ok(transport) diff --git a/codex-rs/windows-sandbox-rs/src/unified_exec/backends/legacy.rs b/codex-rs/windows-sandbox-rs/src/unified_exec/backends/legacy.rs index ba1f15a3be..64abc328f6 100644 --- a/codex-rs/windows-sandbox-rs/src/unified_exec/backends/legacy.rs +++ b/codex-rs/windows-sandbox-rs/src/unified_exec/backends/legacy.rs @@ -300,7 +300,8 @@ pub(crate) async fn spawn_windows_sandbox_session_legacy( if !common.policy.has_full_disk_read_access() { anyhow::bail!("Restricted read-only access requires the elevated Windows sandbox backend"); } - let security = prepare_legacy_session_security(&common.policy, codex_home, cwd)?; + let security = + prepare_legacy_session_security(&common.policy, codex_home, &common.current_dir)?; allow_null_device_for_workspace_write(common.is_workspace_write); let persist_aces = common.is_workspace_write; @@ -333,7 +334,7 @@ pub(crate) async fn spawn_windows_sandbox_session_legacy( } = match spawn_legacy_process( security.h_token, &command, - cwd, + &common.current_dir, &env_map, use_private_desktop, tty,