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 b908e7a4ee..4c49747e7d 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 @@ -38,6 +38,7 @@ use codex_windows_sandbox::log_note; use codex_windows_sandbox::parse_policy; use codex_windows_sandbox::read_frame; use codex_windows_sandbox::read_handle_loop; +use codex_windows_sandbox::sanitize_windows_profile_env; use codex_windows_sandbox::spawn_process_with_pipes; use codex_windows_sandbox::to_wide; use codex_windows_sandbox::write_frame; @@ -222,6 +223,8 @@ fn effective_cwd(req_cwd: &Path, log_dir: Option<&Path>) -> PathBuf { fn spawn_ipc_process(req: &SpawnRequest) -> Result { let log_dir = req.codex_home.clone(); hide_current_user_profile_dir(req.codex_home.as_path()); + let mut child_env = req.env.clone(); + sanitize_windows_profile_env(&mut child_env); let policy = parse_policy(&req.policy_json_or_preset).context("parse policy_json_or_preset")?; let mut cap_psids: Vec = Vec::new(); for sid in &req.cap_sids { @@ -271,7 +274,7 @@ fn spawn_ipc_process(req: &SpawnRequest) -> Result { h_token.raw(), &req.command, &effective_cwd, - &req.env, + &child_env, req.use_private_desktop, Some(log_dir.as_path()), )?; @@ -302,7 +305,7 @@ fn spawn_ipc_process(req: &SpawnRequest) -> Result { h_token.raw(), &req.command, &effective_cwd, - &req.env, + &child_env, stdin_mode, StderrMode::Separate, req.use_private_desktop, diff --git a/codex-rs/windows-sandbox-rs/src/env.rs b/codex-rs/windows-sandbox-rs/src/env.rs index 93275e1abd..74a7f02a77 100644 --- a/codex-rs/windows-sandbox-rs/src/env.rs +++ b/codex-rs/windows-sandbox-rs/src/env.rs @@ -42,6 +42,101 @@ pub fn inherit_path_env(env_map: &mut HashMap) { } } +fn normalize_windows_path_for_compare(path: &str) -> String { + let normalized = path.trim().replace('/', "\\"); + if normalized.len() <= 3 { + return normalized.to_ascii_lowercase(); + } + normalized + .trim_end_matches('\\') + .to_ascii_lowercase() +} + +fn path_is_within_windows_root(path: &str, root: &str) -> bool { + let path = normalize_windows_path_for_compare(path); + let root = normalize_windows_path_for_compare(root); + if root.is_empty() || !path.starts_with(&root) { + return false; + } + path.len() == root.len() || path.as_bytes().get(root.len()) == Some(&b'\\') +} + +fn split_windows_drive(path: &str) -> Option<(String, String)> { + if path.as_bytes().get(1) != Some(&b':') { + return None; + } + let drive = path[..2].to_string(); + let rest = match path.get(2..) { + Some("") | None => "\\".to_string(), + Some(rest) => rest.to_string(), + }; + Some((drive, rest)) +} + +fn default_roaming_appdata(userprofile: &str) -> String { + format!(r"{userprofile}\AppData\Roaming") +} + +fn default_local_appdata(userprofile: &str) -> String { + format!(r"{userprofile}\AppData\Local") +} + +fn sanitize_windows_profile_env_with_current( + env_map: &mut HashMap, + current_userprofile: Option, + current_appdata: Option, + current_localappdata: Option, + current_temp: Option, + current_tmp: Option, +) { + let Some(current_userprofile) = current_userprofile.filter(|value| !value.trim().is_empty()) + else { + return; + }; + + let original_userprofile = env_map.get("USERPROFILE").cloned(); + let appdata = current_appdata.unwrap_or_else(|| default_roaming_appdata(¤t_userprofile)); + let localappdata = + current_localappdata.unwrap_or_else(|| default_local_appdata(¤t_userprofile)); + + env_map.insert("USERPROFILE".into(), current_userprofile.clone()); + env_map.insert("HOME".into(), current_userprofile.clone()); + if let Some((drive, homedir)) = split_windows_drive(¤t_userprofile) { + env_map.insert("HOMEDRIVE".into(), drive); + env_map.insert("HOMEPATH".into(), homedir); + } + env_map.insert("APPDATA".into(), appdata); + env_map.insert("LOCALAPPDATA".into(), localappdata); + if let Some(temp) = current_temp { + env_map.insert("TEMP".into(), temp); + } + if let Some(tmp) = current_tmp { + env_map.insert("TMP".into(), tmp); + } + + if let Some(original_userprofile) = original_userprofile + && !original_userprofile.eq_ignore_ascii_case(¤t_userprofile) + && let Some(path) = env_map.get_mut("PATH") + { + let filtered: Vec<&str> = path + .split(';') + .filter(|entry| !path_is_within_windows_root(entry, &original_userprofile)) + .collect(); + *path = filtered.join(";"); + } +} + +pub fn sanitize_windows_profile_env(env_map: &mut HashMap) { + sanitize_windows_profile_env_with_current( + env_map, + env::var("USERPROFILE").ok(), + env::var("APPDATA").ok(), + env::var("LOCALAPPDATA").ok(), + env::var("TEMP").ok(), + env::var("TMP").ok(), + ); +} + fn prepend_path(env_map: &mut HashMap, prefix: &str) { let existing = env_map .get("PATH") @@ -172,3 +267,106 @@ pub fn apply_no_network_to_env(env_map: &mut HashMap) -> Result< reorder_pathext_for_stubs(env_map); Ok(()) } + +#[cfg(test)] +mod tests { + use super::sanitize_windows_profile_env_with_current; + use std::collections::HashMap; + + #[test] + fn rewrites_profile_vars_and_filters_host_profile_path_entries() { + let mut env_map = HashMap::from([ + ("USERPROFILE".to_string(), r"C:\Users\andre".to_string()), + ("HOME".to_string(), r"C:\Users\andre".to_string()), + ("APPDATA".to_string(), r"C:\Users\andre\AppData\Roaming".to_string()), + ( + "LOCALAPPDATA".to_string(), + r"C:\Users\andre\AppData\Local".to_string(), + ), + ("TEMP".to_string(), r"C:\Users\andre\AppData\Local\Temp".to_string()), + ("TMP".to_string(), r"C:\Users\andre\AppData\Local\Temp".to_string()), + ( + "PATH".to_string(), + [ + r"C:\Windows\System32", + r"C:\Users\andre\AppData\Local\Microsoft\WindowsApps", + r"C:\Users\andre\AppData\Roaming\npm", + r"D:\tools", + ] + .join(";"), + ), + ]); + + sanitize_windows_profile_env_with_current( + &mut env_map, + Some(r"C:\Users\CodexSandboxOffline".to_string()), + Some(r"C:\Users\CodexSandboxOffline\AppData\Roaming".to_string()), + Some(r"C:\Users\CodexSandboxOffline\AppData\Local".to_string()), + Some(r"C:\Users\CodexSandboxOffline\AppData\Local\Temp".to_string()), + Some(r"C:\Users\CodexSandboxOffline\AppData\Local\Temp".to_string()), + ); + + assert_eq!( + env_map.get("USERPROFILE").map(String::as_str), + Some(r"C:\Users\CodexSandboxOffline") + ); + assert_eq!( + env_map.get("HOME").map(String::as_str), + Some(r"C:\Users\CodexSandboxOffline") + ); + assert_eq!(env_map.get("HOMEDRIVE").map(String::as_str), Some("C:")); + assert_eq!( + env_map.get("HOMEPATH").map(String::as_str), + Some(r"\Users\CodexSandboxOffline") + ); + assert_eq!( + env_map.get("APPDATA").map(String::as_str), + Some(r"C:\Users\CodexSandboxOffline\AppData\Roaming") + ); + assert_eq!( + env_map.get("LOCALAPPDATA").map(String::as_str), + Some(r"C:\Users\CodexSandboxOffline\AppData\Local") + ); + assert_eq!( + env_map.get("TEMP").map(String::as_str), + Some(r"C:\Users\CodexSandboxOffline\AppData\Local\Temp") + ); + assert_eq!( + env_map.get("PATH").map(String::as_str), + Some(r"C:\Windows\System32;D:\tools") + ); + } + + #[test] + fn leaves_path_unchanged_when_userprofile_already_matches() { + let mut env_map = HashMap::from([ + ("USERPROFILE".to_string(), r"C:\Users\CodexSandboxOffline".to_string()), + ( + "PATH".to_string(), + [r"C:\Windows\System32", r"C:\Users\CodexSandboxOffline\bin"].join(";"), + ), + ]); + + sanitize_windows_profile_env_with_current( + &mut env_map, + Some(r"C:\Users\CodexSandboxOffline".to_string()), + None, + None, + None, + None, + ); + + assert_eq!( + env_map.get("PATH").map(String::as_str), + Some(r"C:\Windows\System32;C:\Users\CodexSandboxOffline\bin") + ); + assert_eq!( + env_map.get("APPDATA").map(String::as_str), + Some(r"C:\Users\CodexSandboxOffline\AppData\Roaming") + ); + assert_eq!( + env_map.get("LOCALAPPDATA").map(String::as_str), + Some(r"C:\Users\CodexSandboxOffline\AppData\Local") + ); + } +} diff --git a/codex-rs/windows-sandbox-rs/src/lib.rs b/codex-rs/windows-sandbox-rs/src/lib.rs index 16b47f2933..04b71f93f4 100644 --- a/codex-rs/windows-sandbox-rs/src/lib.rs +++ b/codex-rs/windows-sandbox-rs/src/lib.rs @@ -109,6 +109,8 @@ pub use elevated_impl::ElevatedSandboxCaptureRequest; #[cfg(target_os = "windows")] pub use elevated_impl::run_windows_sandbox_capture as run_windows_sandbox_capture_elevated; #[cfg(target_os = "windows")] +pub use env::sanitize_windows_profile_env; +#[cfg(target_os = "windows")] pub use helper_materialization::resolve_current_exe_for_launch; #[cfg(target_os = "windows")] pub use hide_users::hide_current_user_profile_dir;