From fbd599d1a395144b8f64ffffedd8f7f74a6dcf38 Mon Sep 17 00:00:00 2001 From: David Wiesen Date: Mon, 13 Apr 2026 09:16:29 -0700 Subject: [PATCH] fix(windows-sandbox): bootstrap sandbox user profiles --- .../src/elevated/command_runner_win.rs | 10 +- .../windows-sandbox-rs/src/elevated_impl.rs | 98 ++++++++++++++++++- codex-rs/windows-sandbox-rs/src/env.rs | 28 +++++- codex-rs/windows-sandbox-rs/src/lib.rs | 4 + .../windows-sandbox-rs/src/sandbox_users.rs | 70 +++++++++++++ 5 files changed, 206 insertions(+), 4 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 5572c1bf8e..2112a2cfe3 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 @@ -22,6 +22,7 @@ use codex_windows_sandbox::create_readonly_token_with_caps_from; use codex_windows_sandbox::create_workspace_write_token_with_caps_from; use codex_windows_sandbox::get_current_token_for_restriction; use codex_windows_sandbox::hide_current_user_profile_dir; +use codex_windows_sandbox::inherit_path_env; use codex_windows_sandbox::ipc_framed::ErrorPayload; use codex_windows_sandbox::ipc_framed::ExitPayload; use codex_windows_sandbox::ipc_framed::FramedMessage; @@ -33,6 +34,7 @@ use codex_windows_sandbox::ipc_framed::encode_bytes; use codex_windows_sandbox::ipc_framed::read_frame; use codex_windows_sandbox::ipc_framed::write_frame; use codex_windows_sandbox::log_note; +use codex_windows_sandbox::overlay_current_user_profile_env; use codex_windows_sandbox::parse_policy; use codex_windows_sandbox::read_handle_loop; use codex_windows_sandbox::spawn_process_with_pipes; @@ -251,13 +253,17 @@ fn spawn_ipc_process( Some(log_dir.as_path()), ); + let mut child_env = req.env.clone(); + inherit_path_env(&mut child_env); + overlay_current_user_profile_env(&mut child_env); + let mut hpc_handle: Option = None; let (pi, stdout_handle, stderr_handle, stdin_handle) = if req.tty { let (pi, conpty) = codex_windows_sandbox::spawn_conpty_process_as_user( h_token, &req.command, &effective_cwd, - &req.env, + &child_env, req.use_private_desktop, Some(log_dir.as_path()), )?; @@ -287,7 +293,7 @@ fn spawn_ipc_process( h_token, &req.command, &effective_cwd, - &req.env, + &child_env, stdin_mode, StderrMode::Separate, /*use_private_desktop*/ false, diff --git a/codex-rs/windows-sandbox-rs/src/elevated_impl.rs b/codex-rs/windows-sandbox-rs/src/elevated_impl.rs index cc2b7fdad6..e64546a833 100644 --- a/codex-rs/windows-sandbox-rs/src/elevated_impl.rs +++ b/codex-rs/windows-sandbox-rs/src/elevated_impl.rs @@ -21,6 +21,7 @@ mod windows_impl { use crate::cap::load_or_create_cap_sids; use crate::env::ensure_non_interactive_pager; use crate::env::inherit_path_env; + use crate::env::is_profile_env_key; use crate::env::normalize_null_device_env; use crate::helper_materialization::HelperExecutable; use crate::helper_materialization::resolve_helper_for_launch; @@ -58,10 +59,13 @@ mod windows_impl { use windows_sys::Win32::Foundation::CloseHandle; use windows_sys::Win32::Foundation::GetLastError; use windows_sys::Win32::Foundation::HANDLE; + use windows_sys::Win32::Security::Authentication::Identity::LogonUserW; use windows_sys::Win32::Security::Authorization::ConvertStringSecurityDescriptorToSecurityDescriptorW; use windows_sys::Win32::Security::PSECURITY_DESCRIPTOR; use windows_sys::Win32::Security::SECURITY_ATTRIBUTES; use windows_sys::Win32::System::Diagnostics::Debug::SetErrorMode; + use windows_sys::Win32::System::Environment::CreateEnvironmentBlock; + use windows_sys::Win32::System::Environment::DestroyEnvironmentBlock; use windows_sys::Win32::System::Pipes::ConnectNamedPipe; use windows_sys::Win32::System::Pipes::CreateNamedPipeW; const PIPE_ACCESS_INBOUND: u32 = 0x0000_0001; @@ -208,6 +212,93 @@ mod windows_impl { pub use crate::windows_impl::CaptureResult; + fn make_env_block(env_map: &HashMap) -> Vec { + let mut items: Vec<(String, String)> = env_map + .iter() + .map(|(k, v)| (k.clone(), v.clone())) + .collect(); + items.sort_by(|a, b| { + a.0.to_uppercase() + .cmp(&b.0.to_uppercase()) + .then(a.0.cmp(&b.0)) + }); + let mut wide: Vec = Vec::new(); + for (key, value) in items { + let mut pair = to_wide(format!("{key}={value}")); + pair.pop(); + wide.extend_from_slice(&pair); + wide.push(0); + } + wide.push(0); + wide + } + + fn build_logon_environment_block( + username: &[u16], + domain: &[u16], + password: &[u16], + extra_env: &HashMap, + ) -> Result> { + let mut logon_token: HANDLE = 0; + let logon_ok = unsafe { + LogonUserW( + username.as_ptr(), + domain.as_ptr(), + password.as_ptr(), + 2, // LOGON32_LOGON_INTERACTIVE + 0, // LOGON32_PROVIDER_DEFAULT + &mut logon_token, + ) + }; + if logon_ok == 0 { + return Err(anyhow::anyhow!("LogonUserW failed: {}", unsafe { + GetLastError() + })); + } + + let mut raw_block: *mut c_void = ptr::null_mut(); + let ok = unsafe { CreateEnvironmentBlock(&mut raw_block, logon_token, 0) }; + if ok == 0 { + unsafe { + CloseHandle(logon_token); + } + return Err(anyhow::anyhow!( + "CreateEnvironmentBlock failed: {}", + unsafe { GetLastError() } + )); + } + + let mut env_map: HashMap = HashMap::new(); + unsafe { + let mut cursor = raw_block as *const u16; + loop { + let mut len = 0usize; + while *cursor.add(len) != 0 { + len += 1; + } + if len == 0 { + break; + } + let entry = String::from_utf16_lossy(std::slice::from_raw_parts(cursor, len)); + if let Some((key, value)) = entry.split_once('=') { + env_map.insert(key.to_string(), value.to_string()); + } + cursor = cursor.add(len + 1); + } + DestroyEnvironmentBlock(raw_block); + CloseHandle(logon_token); + } + + for (key, value) in extra_env { + if is_profile_env_key(key) && env_map.contains_key(key) { + continue; + } + env_map.insert(key.clone(), value.clone()); + } + + Ok(make_env_block(&env_map)) + } + fn read_spawn_ready(pipe_read: &mut File) -> Result<()> { let msg = read_frame(pipe_read)? .ok_or_else(|| anyhow::anyhow!("runner pipe closed before spawn_ready"))?; @@ -315,7 +406,12 @@ mod windows_impl { let cwd_w: Vec = to_wide(cwd); // Minimal CPWL launch: inherit env, no desktop override, no handle inheritance. - let env_block: Option> = None; + let env_block = Some(build_logon_environment_block( + &user_w, + &domain_w, + &password_w, + &env_map, + )?); let mut si: STARTUPINFOW = unsafe { std::mem::zeroed() }; si.cb = std::mem::size_of::() as u32; let mut pi: PROCESS_INFORMATION = unsafe { std::mem::zeroed() }; diff --git a/codex-rs/windows-sandbox-rs/src/env.rs b/codex-rs/windows-sandbox-rs/src/env.rs index c3f85cfffd..c45af82fd4 100644 --- a/codex-rs/windows-sandbox-rs/src/env.rs +++ b/codex-rs/windows-sandbox-rs/src/env.rs @@ -1,4 +1,4 @@ -use anyhow::{anyhow, Result}; +use anyhow::{Result, anyhow}; use dirs_next::home_dir; use std::collections::HashMap; use std::env; @@ -6,6 +6,24 @@ use std::fs::{self, File}; use std::io::Write; use std::path::{Path, PathBuf}; +pub const PROFILE_ENV_KEYS: &[&str] = &[ + "APPDATA", + "HOMEDRIVE", + "HOMEPATH", + "HOME", + "LOCALAPPDATA", + "TEMP", + "TMP", + "USERNAME", + "USERPROFILE", +]; + +pub fn is_profile_env_key(key: &str) -> bool { + PROFILE_ENV_KEYS + .iter() + .any(|candidate| candidate.eq_ignore_ascii_case(key)) +} + pub fn normalize_null_device_env(env_map: &mut HashMap) { let keys: Vec = env_map.keys().cloned().collect(); for k in keys { @@ -42,6 +60,14 @@ pub fn inherit_path_env(env_map: &mut HashMap) { } } +pub fn overlay_current_user_profile_env(env_map: &mut HashMap) { + for key in PROFILE_ENV_KEYS { + if let Ok(value) = env::var(key) { + env_map.insert((*key).to_string(), value); + } + } +} + fn prepend_path(env_map: &mut HashMap, prefix: &str) { let existing = env_map .get("PATH") diff --git a/codex-rs/windows-sandbox-rs/src/lib.rs b/codex-rs/windows-sandbox-rs/src/lib.rs index b0de8c2e02..4bcfd58b80 100644 --- a/codex-rs/windows-sandbox-rs/src/lib.rs +++ b/codex-rs/windows-sandbox-rs/src/lib.rs @@ -78,6 +78,10 @@ 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::inherit_path_env; +#[cfg(target_os = "windows")] +pub use env::overlay_current_user_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; diff --git a/codex-rs/windows-sandbox-rs/src/sandbox_users.rs b/codex-rs/windows-sandbox-rs/src/sandbox_users.rs index 13460b9444..d2fad96850 100644 --- a/codex-rs/windows-sandbox-rs/src/sandbox_users.rs +++ b/codex-rs/windows-sandbox-rs/src/sandbox_users.rs @@ -12,6 +12,7 @@ use std::ffi::c_void; use std::fs::File; use std::path::Path; use std::path::PathBuf; +use windows_sys::Win32::Foundation::CloseHandle; use windows_sys::Win32::Foundation::ERROR_INSUFFICIENT_BUFFER; use windows_sys::Win32::Foundation::GetLastError; use windows_sys::Win32::Foundation::LocalFree; @@ -33,6 +34,14 @@ use windows_sys::Win32::Security::GetLengthSid; use windows_sys::Win32::Security::LookupAccountNameW; use windows_sys::Win32::Security::LookupAccountSidW; use windows_sys::Win32::Security::SID_NAME_USE; +use windows_sys::Win32::System::Threading::CREATE_NO_WINDOW; +use windows_sys::Win32::System::Threading::CreateProcessWithLogonW; +use windows_sys::Win32::System::Threading::GetExitCodeProcess; +use windows_sys::Win32::System::Threading::INFINITE; +use windows_sys::Win32::System::Threading::LOGON_WITH_PROFILE; +use windows_sys::Win32::System::Threading::PROCESS_INFORMATION; +use windows_sys::Win32::System::Threading::STARTUPINFOW; +use windows_sys::Win32::System::Threading::WaitForSingleObject; use codex_windows_sandbox::SETUP_VERSION; use codex_windows_sandbox::SetupErrorCode; @@ -91,6 +100,67 @@ pub fn provision_sandbox_users( pub fn ensure_sandbox_user(username: &str, password: &str, log: &mut File) -> Result<()> { ensure_local_user(username, password, log)?; ensure_local_group_member(SANDBOX_USERS_GROUP, username)?; + ensure_user_profile_initialized(username, password, log)?; + Ok(()) +} + +fn ensure_user_profile_initialized(username: &str, password: &str, log: &mut File) -> Result<()> { + let username_w = to_wide(OsStr::new(username)); + let domain_w = to_wide("."); + let password_w = to_wide(OsStr::new(password)); + let application = to_wide(r"C:\Windows\System32\cmd.exe"); + let mut command_line = to_wide(r#"cmd.exe /d /c exit 0"#); + let mut startup_info: STARTUPINFOW = unsafe { std::mem::zeroed() }; + startup_info.cb = std::mem::size_of::() as u32; + let mut process_info: PROCESS_INFORMATION = unsafe { std::mem::zeroed() }; + + let ok = unsafe { + CreateProcessWithLogonW( + username_w.as_ptr(), + domain_w.as_ptr(), + password_w.as_ptr(), + LOGON_WITH_PROFILE, + application.as_ptr(), + command_line.as_mut_ptr(), + CREATE_NO_WINDOW, + std::ptr::null(), + std::ptr::null(), + &startup_info, + &mut process_info, + ) + }; + if ok == 0 { + let err = unsafe { GetLastError() }; + super::log_line( + log, + &format!("CreateProcessWithLogonW failed for {username}: {err}"), + )?; + return Err(anyhow::Error::new(SetupFailure::new( + SetupErrorCode::HelperUserCreateOrUpdateFailed, + format!("failed to initialize sandbox user profile for {username}: {err}"), + ))); + } + + unsafe { + WaitForSingleObject(process_info.hProcess, INFINITE); + } + let mut exit_code = 1u32; + unsafe { + let _ = GetExitCodeProcess(process_info.hProcess, &mut exit_code); + CloseHandle(process_info.hThread); + CloseHandle(process_info.hProcess); + } + if exit_code != 0 { + super::log_line( + log, + &format!("profile bootstrap exited with code {exit_code} for {username}"), + )?; + return Err(anyhow::Error::new(SetupFailure::new( + SetupErrorCode::HelperUserCreateOrUpdateFailed, + format!("sandbox user profile bootstrap exited with code {exit_code} for {username}"), + ))); + } + Ok(()) }