diff --git a/codex-rs/windows-sandbox-rs/src/legacy_cwd.rs b/codex-rs/windows-sandbox-rs/src/legacy_cwd.rs new file mode 100644 index 0000000000..033f5ba90d --- /dev/null +++ b/codex-rs/windows-sandbox-rs/src/legacy_cwd.rs @@ -0,0 +1,209 @@ +#![cfg(target_os = "windows")] + +use crate::log_note; +use crate::path_normalization::normalize_spawn_cwd; +use std::collections::hash_map::DefaultHasher; +use std::hash::Hash; +use std::hash::Hasher; +use std::os::windows::fs::MetadataExt as _; +use std::os::windows::process::CommandExt as _; +use std::path::Component; +use std::path::Path; +use std::path::PathBuf; +use std::path::Prefix; +use windows_sys::Win32::Storage::FileSystem::FILE_ATTRIBUTE_REPARSE_POINT; + +fn junction_name_for_path(path: &Path) -> String { + let mut hasher = DefaultHasher::new(); + path.to_string_lossy().hash(&mut hasher); + format!("{:x}", hasher.finish()) +} + +fn junction_root_for_userprofile(userprofile: &str) -> PathBuf { + PathBuf::from(userprofile) + .join(".codex") + .join(".sandbox") + .join("cwd") +} + +fn drive_letter(path: &Path) -> Option { + match path.components().next()? { + Component::Prefix(prefix) => match prefix.kind() { + Prefix::Disk(drive) | Prefix::VerbatimDisk(drive) => { + Some((drive as char).to_ascii_uppercase()) + } + _ => None, + }, + _ => None, + } +} + +fn system_drive_letter(system_drive: Option<&str>) -> Option { + drive_letter(Path::new(system_drive?)) +} + +fn should_materialize_junction(requested_cwd: &Path, system_drive: Option<&str>) -> bool { + let Some(requested_drive) = drive_letter(requested_cwd) else { + return false; + }; + let Some(system_drive) = system_drive_letter(system_drive) else { + return false; + }; + requested_drive != system_drive +} + +fn create_cwd_junction(requested_cwd: &Path, log_dir: Option<&Path>) -> Option { + let userprofile = std::env::var("USERPROFILE").ok()?; + let junction_root = junction_root_for_userprofile(&userprofile); + if let Err(err) = std::fs::create_dir_all(&junction_root) { + log_note( + &format!( + "junction: failed to create {}: {err}", + junction_root.display() + ), + log_dir, + ); + return None; + } + + let junction_path = junction_root.join(junction_name_for_path(requested_cwd)); + if junction_path.exists() { + match std::fs::symlink_metadata(&junction_path) { + Ok(md) if (md.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT) != 0 => { + log_note( + &format!("junction: reusing existing {}", junction_path.display()), + log_dir, + ); + return Some(junction_path); + } + Ok(_) => { + log_note( + &format!( + "junction: existing path is not a reparse point, recreating {}", + junction_path.display() + ), + log_dir, + ); + } + Err(err) => { + log_note( + &format!( + "junction: failed to stat existing {}: {err}", + junction_path.display() + ), + log_dir, + ); + return None; + } + } + + if let Err(err) = std::fs::remove_dir(&junction_path) { + log_note( + &format!( + "junction: failed to remove existing {}: {err}", + junction_path.display() + ), + log_dir, + ); + return None; + } + } + + let link = junction_path.to_string_lossy().to_string(); + let target = requested_cwd.to_string_lossy().to_string(); + let link_quoted = format!("\"{link}\""); + let target_quoted = format!("\"{target}\""); + log_note( + &format!("junction: creating via cmd /c mklink /J {link_quoted} {target_quoted}"), + log_dir, + ); + let output = match std::process::Command::new("cmd") + .raw_arg("/c") + .raw_arg("mklink") + .raw_arg("/J") + .raw_arg(&link_quoted) + .raw_arg(&target_quoted) + .output() + { + Ok(output) => output, + Err(err) => { + log_note(&format!("junction: mklink failed to run: {err}"), log_dir); + return None; + } + }; + if output.status.success() && junction_path.exists() { + log_note( + &format!( + "junction: created {} -> {}", + junction_path.display(), + requested_cwd.display() + ), + log_dir, + ); + return Some(junction_path); + } + + let stdout = String::from_utf8_lossy(&output.stdout); + let stderr = String::from_utf8_lossy(&output.stderr); + log_note( + &format!( + "junction: mklink failed status={:?} stdout={} stderr={}", + output.status, + stdout.trim(), + stderr.trim() + ), + log_dir, + ); + None +} + +pub(crate) fn effective_legacy_spawn_cwd(cwd: &Path, log_dir: Option<&Path>) -> PathBuf { + let normalized_cwd = normalize_spawn_cwd(cwd); + if should_materialize_junction( + &normalized_cwd, + std::env::var("SystemDrive").ok().as_deref(), + ) { + create_cwd_junction(&normalized_cwd, log_dir).unwrap_or_else(|| normalized_cwd.clone()) + } else { + normalized_cwd + } +} + +#[cfg(test)] +mod tests { + use super::effective_legacy_spawn_cwd; + use super::should_materialize_junction; + use pretty_assertions::assert_eq; + use std::path::Path; + use std::path::PathBuf; + + #[test] + fn skips_system_drive_workspaces() { + assert!(!should_materialize_junction( + Path::new(r"C:\repo"), + Some("C:"), + )); + } + + #[test] + fn uses_junction_for_non_system_drive_workspaces() { + assert!(should_materialize_junction( + Path::new(r"F:\repo"), + Some("C:"), + )); + } + + #[test] + fn skips_unc_paths() { + assert!(!should_materialize_junction( + Path::new(r"\\server\share\repo"), + Some("C:"), + )); + } + + #[test] + fn leaves_system_drive_paths_unchanged() { + let cwd = PathBuf::from(r"C:\repo"); + assert_eq!(effective_legacy_spawn_cwd(&cwd, None), cwd); + } +} diff --git a/codex-rs/windows-sandbox-rs/src/lib.rs b/codex-rs/windows-sandbox-rs/src/lib.rs index f11f3f762e..e3aea8699e 100644 --- a/codex-rs/windows-sandbox-rs/src/lib.rs +++ b/codex-rs/windows-sandbox-rs/src/lib.rs @@ -22,6 +22,7 @@ windows_modules!( helper_materialization, hide_users, identity, + legacy_cwd, logging, path_normalization, policy, @@ -253,6 +254,7 @@ mod windows_impl { use super::allow::compute_allow_paths; use super::cap::load_or_create_cap_sids; use super::cap::workspace_cap_sid_for_cwd; + use super::legacy_cwd::effective_legacy_spawn_cwd; use super::logging::log_failure; use super::logging::log_success; use super::path_normalization::canonicalize_path; @@ -461,11 +463,12 @@ mod windows_impl { } let (stdin_pair, stdout_pair, stderr_pair) = unsafe { setup_stdio_pipes()? }; let ((in_r, in_w), (out_r, out_w), (err_r, err_w)) = (stdin_pair, stdout_pair, stderr_pair); + let effective_cwd = effective_legacy_spawn_cwd(cwd, logs_base_dir); let spawn_res = unsafe { create_process_as_user( h_token, &command, - cwd, + &effective_cwd, &env_map, logs_base_dir, Some((in_r, out_w, err_w)), 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..356844664b 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 @@ -3,6 +3,7 @@ use super::windows_common::normalize_windows_tty_input; use crate::acl::revoke_ace; use crate::conpty::spawn_conpty_process_as_user; use crate::desktop::LaunchDesktop; +use crate::legacy_cwd::effective_legacy_spawn_cwd; use crate::logging::log_failure; use crate::logging::log_success; use crate::process::StderrMode; @@ -66,11 +67,12 @@ fn spawn_legacy_process( writer_rx: mpsc::Receiver>, logs_base_dir: Option<&Path>, ) -> Result { + let effective_cwd = effective_legacy_spawn_cwd(cwd, logs_base_dir); let (pi, output_join, writer_handle, hpc, desktop) = if tty { let (pi, conpty) = spawn_conpty_process_as_user( h_token, command, - cwd, + &effective_cwd, env_map, use_private_desktop, logs_base_dir, @@ -87,7 +89,7 @@ fn spawn_legacy_process( let pipe_handles = spawn_process_with_pipes( h_token, command, - cwd, + &effective_cwd, env_map, if stdin_open { StdinMode::Open