diff --git a/codex-rs/windows-sandbox-rs/src/command_runner_win.rs b/codex-rs/windows-sandbox-rs/src/command_runner_win.rs index 8f0558e09..173939990 100644 --- a/codex-rs/windows-sandbox-rs/src/command_runner_win.rs +++ b/codex-rs/windows-sandbox-rs/src/command_runner_win.rs @@ -8,6 +8,7 @@ use codex_windows_sandbox::create_process_as_user; use codex_windows_sandbox::create_readonly_token_with_cap_from; use codex_windows_sandbox::create_workspace_write_token_with_cap_from; 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::parse_policy; use codex_windows_sandbox::to_wide; @@ -91,6 +92,7 @@ pub fn main() -> Result<()> { } let req: RunnerRequest = serde_json::from_str(&input).context("parse runner request json")?; let log_dir = Some(req.codex_home.as_path()); + hide_current_user_profile_dir(req.codex_home.as_path()); log_note( &format!( "runner start cwd={} cmd={:?} real_codex_home={}", diff --git a/codex-rs/windows-sandbox-rs/src/hide_users.rs b/codex-rs/windows-sandbox-rs/src/hide_users.rs new file mode 100644 index 000000000..10964d1c1 --- /dev/null +++ b/codex-rs/windows-sandbox-rs/src/hide_users.rs @@ -0,0 +1,160 @@ +#![cfg(target_os = "windows")] + +use crate::logging::log_note; +use crate::winutil::format_last_error; +use crate::winutil::to_wide; +use anyhow::anyhow; +use std::ffi::OsStr; +use std::path::Path; +use std::path::PathBuf; +use windows_sys::Win32::Foundation::GetLastError; +use windows_sys::Win32::Storage::FileSystem::GetFileAttributesW; +use windows_sys::Win32::Storage::FileSystem::SetFileAttributesW; +use windows_sys::Win32::Storage::FileSystem::FILE_ATTRIBUTE_HIDDEN; +use windows_sys::Win32::Storage::FileSystem::FILE_ATTRIBUTE_SYSTEM; +use windows_sys::Win32::Storage::FileSystem::INVALID_FILE_ATTRIBUTES; +use windows_sys::Win32::System::Registry::RegCloseKey; +use windows_sys::Win32::System::Registry::RegCreateKeyExW; +use windows_sys::Win32::System::Registry::RegSetValueExW; +use windows_sys::Win32::System::Registry::HKEY; +use windows_sys::Win32::System::Registry::HKEY_LOCAL_MACHINE; +use windows_sys::Win32::System::Registry::KEY_WRITE; +use windows_sys::Win32::System::Registry::REG_DWORD; +use windows_sys::Win32::System::Registry::REG_OPTION_NON_VOLATILE; + +const USERLIST_KEY_PATH: &str = + r"SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon\SpecialAccounts\UserList"; + +pub fn hide_newly_created_users(usernames: &[String], log_base: &Path) { + if usernames.is_empty() { + return; + } + if let Err(err) = hide_users_in_winlogon(usernames, log_base) { + log_note( + &format!("hide users: failed to update Winlogon UserList: {err}"), + Some(log_base), + ); + } +} + +/// Best-effort: hides the current sandbox user's profile directory once it exists. +/// +/// Windows only creates profile directories when that user first logs in. +/// This intentionally runs in the command-runner (as the sandbox user) because +/// command running is what causes us to log in as a particular sandbox user. +pub fn hide_current_user_profile_dir(log_base: &Path) { + let Some(profile) = std::env::var_os("USERPROFILE") else { + return; + }; + let profile_dir = PathBuf::from(profile); + if !profile_dir.exists() { + return; + } + + match hide_directory(&profile_dir) { + Ok(true) => { + // Log only when we actually change attributes, so this stays one-time per profile dir. + log_note( + &format!( + "hide users: profile dir hidden for current user ({})", + profile_dir.display() + ), + Some(log_base), + ); + } + Ok(false) => {} + Err(err) => { + log_note( + &format!( + "hide users: failed to hide current user profile dir ({}): {err}", + profile_dir.display() + ), + Some(log_base), + ); + } + } +} + +fn hide_users_in_winlogon(usernames: &[String], log_base: &Path) -> anyhow::Result<()> { + let key = create_userlist_key()?; + for username in usernames { + let name_w = to_wide(OsStr::new(username)); + let value: u32 = 0; + let status = unsafe { + RegSetValueExW( + key, + name_w.as_ptr(), + 0, + REG_DWORD, + &value as *const u32 as *const u8, + std::mem::size_of_val(&value) as u32, + ) + }; + if status != 0 { + log_note( + &format!( + "hide users: failed to set UserList value for {username}: {status} ({error})", + error = format_last_error(status as i32) + ), + Some(log_base), + ); + } + } + unsafe { + RegCloseKey(key); + } + Ok(()) +} + +fn create_userlist_key() -> anyhow::Result { + let key_path = to_wide(USERLIST_KEY_PATH); + let mut key: HKEY = 0; + let status = unsafe { + RegCreateKeyExW( + HKEY_LOCAL_MACHINE, + key_path.as_ptr(), + 0, + std::ptr::null_mut(), + REG_OPTION_NON_VOLATILE, + KEY_WRITE, + std::ptr::null_mut(), + &mut key, + std::ptr::null_mut(), + ) + }; + if status != 0 { + return Err(anyhow!( + "RegCreateKeyExW failed: {status} ({error})", + error = format_last_error(status as i32) + )); + } + Ok(key) +} + +/// Sets HIDDEN|SYSTEM on `path` if needed, returning whether it changed anything. +fn hide_directory(path: &Path) -> anyhow::Result { + let wide = to_wide(path); + let attrs = unsafe { GetFileAttributesW(wide.as_ptr()) }; + if attrs == INVALID_FILE_ATTRIBUTES { + let err = unsafe { GetLastError() } as i32; + return Err(anyhow!( + "GetFileAttributesW failed for {}: {err} ({error})", + path.display(), + error = format_last_error(err) + )); + } + let new_attrs = attrs | FILE_ATTRIBUTE_HIDDEN | FILE_ATTRIBUTE_SYSTEM; + if new_attrs == attrs { + return Ok(false); + } + let ok = unsafe { SetFileAttributesW(wide.as_ptr(), new_attrs) }; + if ok == 0 { + let err = unsafe { GetLastError() } as i32; + return Err(anyhow!( + "SetFileAttributesW failed for {}: {err} ({error})", + path.display(), + error = format_last_error(err) + )); + } + Ok(true) +} diff --git a/codex-rs/windows-sandbox-rs/src/lib.rs b/codex-rs/windows-sandbox-rs/src/lib.rs index 4ffab10dc..a336b00fa 100644 --- a/codex-rs/windows-sandbox-rs/src/lib.rs +++ b/codex-rs/windows-sandbox-rs/src/lib.rs @@ -5,7 +5,8 @@ macro_rules! windows_modules { } windows_modules!( - acl, allow, audit, cap, dpapi, env, identity, logging, policy, process, token, winutil + acl, allow, audit, cap, dpapi, env, hide_users, identity, logging, policy, process, token, + winutil ); #[cfg(target_os = "windows")] @@ -38,6 +39,10 @@ pub use dpapi::unprotect as dpapi_unprotect; #[cfg(target_os = "windows")] pub use elevated_impl::run_windows_sandbox_capture as run_windows_sandbox_capture_elevated; #[cfg(target_os = "windows")] +pub use hide_users::hide_current_user_profile_dir; +#[cfg(target_os = "windows")] +pub use hide_users::hide_newly_created_users; +#[cfg(target_os = "windows")] pub use identity::require_logon_sandbox_creds; #[cfg(target_os = "windows")] pub use logging::log_note; diff --git a/codex-rs/windows-sandbox-rs/src/setup_main_win.rs b/codex-rs/windows-sandbox-rs/src/setup_main_win.rs index d6cf9d033..e1b914859 100644 --- a/codex-rs/windows-sandbox-rs/src/setup_main_win.rs +++ b/codex-rs/windows-sandbox-rs/src/setup_main_win.rs @@ -9,6 +9,7 @@ use base64::Engine; use codex_windows_sandbox::convert_string_sid_to_sid; use codex_windows_sandbox::ensure_allow_mask_aces_with_inheritance; use codex_windows_sandbox::ensure_allow_write_aces; +use codex_windows_sandbox::hide_newly_created_users; use codex_windows_sandbox::load_or_create_cap_sids; use codex_windows_sandbox::log_note; use codex_windows_sandbox::path_mask_allows; @@ -448,6 +449,11 @@ fn run_setup_full(payload: &Payload, log: &mut File, sbx_dir: &Path) -> Result<( &payload.online_username, log, )?; + let users = vec![ + payload.offline_username.clone(), + payload.online_username.clone(), + ]; + hide_newly_created_users(&users, sbx_dir); } let offline_sid = resolve_sid(&payload.offline_username)?; let offline_sid_str = string_from_sid_bytes(&offline_sid).map_err(anyhow::Error::msg)?;