From c637d10e835e30fbefb791acc338d5cee6798702 Mon Sep 17 00:00:00 2001 From: David Wiesen Date: Sat, 25 Apr 2026 20:43:35 -0700 Subject: [PATCH] fix(windows): normalize sandbox cwd for mapped drives --- .../src/elevated/command_runner_win.rs | 22 +++++- codex-rs/windows-sandbox-rs/src/lib.rs | 4 + .../src/path_normalization.rs | 74 ++++++++++++++++++- codex-rs/windows-sandbox-rs/src/process.rs | 16 ++-- 4 files changed, 106 insertions(+), 10 deletions(-) diff --git a/codex-rs/windows-sandbox-rs/src/elevated/command_runner_win.rs b/codex-rs/windows-sandbox-rs/src/elevated/command_runner_win.rs index dcca40abff..804adc1dca 100644 --- a/codex-rs/windows-sandbox-rs/src/elevated/command_runner_win.rs +++ b/codex-rs/windows-sandbox-rs/src/elevated/command_runner_win.rs @@ -35,7 +35,9 @@ use codex_windows_sandbox::encode_bytes; use codex_windows_sandbox::get_current_token_for_restriction; use codex_windows_sandbox::hide_current_user_profile_dir; use codex_windows_sandbox::log_note; +use codex_windows_sandbox::normalize_spawn_cwd; use codex_windows_sandbox::parse_policy; +use codex_windows_sandbox::path_uses_unc_prefix; use codex_windows_sandbox::read_frame; use codex_windows_sandbox::read_handle_loop; use codex_windows_sandbox::spawn_process_with_pipes; @@ -167,6 +169,21 @@ fn read_spawn_request(reader: &mut File) -> Result { /// Pick an effective CWD, using a junction if the ACL helper is active. fn effective_cwd(req_cwd: &Path, log_dir: Option<&Path>) -> PathBuf { + let normalized_cwd = normalize_spawn_cwd(req_cwd); + if path_uses_unc_prefix(&normalized_cwd) { + if normalized_cwd != req_cwd { + log_note( + &format!( + "junction: using UNC cwd {} resolved from {}", + normalized_cwd.display(), + req_cwd.display() + ), + log_dir, + ); + } + return normalized_cwd; + } + let use_junction = match read_acl_mutex::read_acl_mutex_exists() { Ok(exists) => exists, Err(err) => { @@ -180,9 +197,10 @@ fn effective_cwd(req_cwd: &Path, log_dir: Option<&Path>) -> PathBuf { } }; if use_junction { - cwd_junction::create_cwd_junction(req_cwd, log_dir).unwrap_or_else(|| req_cwd.to_path_buf()) + cwd_junction::create_cwd_junction(req_cwd, log_dir) + .unwrap_or_else(|| normalized_cwd.clone()) } else { - req_cwd.to_path_buf() + normalized_cwd } } diff --git a/codex-rs/windows-sandbox-rs/src/lib.rs b/codex-rs/windows-sandbox-rs/src/lib.rs index 8110c3237d..5abd0228b0 100644 --- a/codex-rs/windows-sandbox-rs/src/lib.rs +++ b/codex-rs/windows-sandbox-rs/src/lib.rs @@ -149,6 +149,10 @@ pub use logging::log_note; #[cfg(target_os = "windows")] pub use path_normalization::canonicalize_path; #[cfg(target_os = "windows")] +pub use path_normalization::normalize_spawn_cwd; +#[cfg(target_os = "windows")] +pub use path_normalization::path_uses_unc_prefix; +#[cfg(target_os = "windows")] pub use policy::SandboxPolicy; #[cfg(target_os = "windows")] pub use policy::parse_policy; diff --git a/codex-rs/windows-sandbox-rs/src/path_normalization.rs b/codex-rs/windows-sandbox-rs/src/path_normalization.rs index fe6a932306..d9bb7b6a14 100644 --- a/codex-rs/windows-sandbox-rs/src/path_normalization.rs +++ b/codex-rs/windows-sandbox-rs/src/path_normalization.rs @@ -1,10 +1,32 @@ use std::path::Path; use std::path::PathBuf; +#[cfg(target_os = "windows")] +use std::path::Prefix; pub fn canonicalize_path(path: &Path) -> PathBuf { dunce::canonicalize(path).unwrap_or_else(|_| path.to_path_buf()) } +pub fn normalize_spawn_cwd(path: &Path) -> PathBuf { + let simplified = dunce::simplified(path).to_path_buf(); + if path_uses_unc_prefix(&simplified) { + return simplified; + } + + let canonical = dunce::canonicalize(path).ok(); + let canonical = canonical + .as_deref() + .map(dunce::simplified) + .map(Path::to_path_buf); + if let Some(canonical) = canonical + && path_uses_unc_prefix(&canonical) + { + return canonical; + } + + simplified +} + pub fn canonical_path_key(path: &Path) -> String { canonicalize_path(path) .to_string_lossy() @@ -12,17 +34,67 @@ pub fn canonical_path_key(path: &Path) -> String { .to_ascii_lowercase() } +pub fn path_uses_unc_prefix(path: &Path) -> bool { + #[cfg(target_os = "windows")] + { + matches!( + path.components().next(), + Some(std::path::Component::Prefix(prefix)) + if matches!(prefix.kind(), Prefix::UNC(..) | Prefix::VerbatimUNC(..)) + ) + } + #[cfg(not(target_os = "windows"))] + { + let _ = path; + false + } +} + #[cfg(test)] mod tests { use super::canonical_path_key; + use super::normalize_spawn_cwd; + use super::path_uses_unc_prefix; use pretty_assertions::assert_eq; use std::path::Path; + use std::path::PathBuf; #[test] fn canonical_path_key_normalizes_case_and_separators() { let windows_style = Path::new(r"C:\Users\Dev\Repo"); let slash_style = Path::new("c:/users/dev/repo"); - assert_eq!(canonical_path_key(windows_style), canonical_path_key(slash_style)); + assert_eq!( + canonical_path_key(windows_style), + canonical_path_key(slash_style) + ); + } + + #[cfg(target_os = "windows")] + #[test] + fn path_uses_unc_prefix_matches_standard_and_verbatim_unc_paths() { + assert!(path_uses_unc_prefix(Path::new(r"\\server\share\repo"))); + assert!(path_uses_unc_prefix(Path::new( + r"\\?\UNC\server\share\repo" + ))); + assert!(!path_uses_unc_prefix(Path::new(r"C:\repo"))); + } + + #[test] + fn normalize_spawn_cwd_preserves_regular_local_paths() { + let path = PathBuf::from(r"C:\repo"); + + assert_eq!(normalize_spawn_cwd(&path), path); + } + + #[cfg(target_os = "windows")] + #[test] + fn normalize_spawn_cwd_simplifies_verbatim_unc_paths() { + let path = PathBuf::from(r"\\?\UNC\server\share\repo"); + + assert_eq!( + normalize_spawn_cwd(&path), + PathBuf::from(r"\\server\share\repo") + ); } } diff --git a/codex-rs/windows-sandbox-rs/src/process.rs b/codex-rs/windows-sandbox-rs/src/process.rs index 1571bb8ccf..fe08a00bbb 100644 --- a/codex-rs/windows-sandbox-rs/src/process.rs +++ b/codex-rs/windows-sandbox-rs/src/process.rs @@ -1,29 +1,30 @@ use crate::desktop::LaunchDesktop; use crate::logging; +use crate::path_normalization::normalize_spawn_cwd; use crate::proc_thread_attr::ProcThreadAttributeList; use crate::winutil::argv_to_command_line; use crate::winutil::format_last_error; use crate::winutil::to_wide; -use anyhow::anyhow; use anyhow::Result; +use anyhow::anyhow; use std::collections::HashMap; use std::ffi::c_void; use std::path::Path; use std::ptr; -use windows_sys::Win32::Foundation::GetLastError; use windows_sys::Win32::Foundation::CloseHandle; -use windows_sys::Win32::Foundation::SetHandleInformation; +use windows_sys::Win32::Foundation::GetLastError; use windows_sys::Win32::Foundation::HANDLE; use windows_sys::Win32::Foundation::HANDLE_FLAG_INHERIT; use windows_sys::Win32::Foundation::INVALID_HANDLE_VALUE; +use windows_sys::Win32::Foundation::SetHandleInformation; use windows_sys::Win32::Storage::FileSystem::ReadFile; use windows_sys::Win32::System::Console::GetStdHandle; use windows_sys::Win32::System::Console::STD_ERROR_HANDLE; use windows_sys::Win32::System::Console::STD_INPUT_HANDLE; use windows_sys::Win32::System::Console::STD_OUTPUT_HANDLE; use windows_sys::Win32::System::Pipes::CreatePipe; -use windows_sys::Win32::System::Threading::CreateProcessAsUserW; use windows_sys::Win32::System::Threading::CREATE_UNICODE_ENVIRONMENT; +use windows_sys::Win32::System::Threading::CreateProcessAsUserW; use windows_sys::Win32::System::Threading::EXTENDED_STARTUPINFO_PRESENT; use windows_sys::Win32::System::Threading::PROCESS_INFORMATION; use windows_sys::Win32::System::Threading::STARTF_USESTDHANDLES; @@ -89,7 +90,8 @@ pub unsafe fn create_process_as_user( let env_block = make_env_block(env_map); let desktop = LaunchDesktop::prepare(use_private_desktop, logs_base_dir)?; let mut pi: PROCESS_INFORMATION = std::mem::zeroed(); - let cwd_wide = to_wide(cwd); + let spawn_cwd = normalize_spawn_cwd(cwd); + let cwd_wide = to_wide(&spawn_cwd); let env_block_len = env_block.len(); match stdio { Some((stdin_h, stdout_h, stderr_h)) => { @@ -139,7 +141,7 @@ pub unsafe fn create_process_as_user( "CreateProcessAsUserW failed: {} ({}) | cwd={} | cmd={} | env_u16_len={} | si_flags={} | creation_flags={}", err, format_last_error(err), - cwd.display(), + spawn_cwd.display(), cmdline_str, env_block_len, si.StartupInfo.dwFlags, @@ -180,7 +182,7 @@ pub unsafe fn create_process_as_user( "CreateProcessAsUserW failed: {} ({}) | cwd={} | cmd={} | env_u16_len={} | si_flags={} | creation_flags={}", err, format_last_error(err), - cwd.display(), + spawn_cwd.display(), cmdline_str, env_block_len, si.dwFlags,