use serde::Deserialize; use serde::Serialize; use std::collections::HashMap; use std::ffi::c_void; use std::path::Path; use std::path::PathBuf; use std::process::Command; use anyhow::anyhow; use anyhow::Context; use anyhow::Result; use base64::engine::general_purpose::STANDARD as BASE64_STANDARD; use base64::Engine; use crate::allow::compute_allow_paths; use crate::allow::AllowDenyPaths; use crate::policy::SandboxPolicy; use windows_sys::Win32::Foundation::CloseHandle; use windows_sys::Win32::Foundation::GetLastError; use windows_sys::Win32::Security::AllocateAndInitializeSid; use windows_sys::Win32::Security::CheckTokenMembership; use windows_sys::Win32::Security::FreeSid; use windows_sys::Win32::Security::SECURITY_NT_AUTHORITY; pub const SETUP_VERSION: u32 = 1; pub const OFFLINE_USERNAME: &str = "CodexSandboxOffline"; pub const ONLINE_USERNAME: &str = "CodexSandboxOnline"; const SECURITY_BUILTIN_DOMAIN_RID: u32 = 0x0000_0020; const DOMAIN_ALIAS_RID_ADMINS: u32 = 0x0000_0220; pub fn sandbox_dir(codex_home: &Path) -> PathBuf { codex_home.join("sandbox") } pub fn setup_marker_path(codex_home: &Path) -> PathBuf { sandbox_dir(codex_home).join("setup_marker.json") } pub fn sandbox_users_path(codex_home: &Path) -> PathBuf { sandbox_dir(codex_home).join("sandbox_users.json") } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct SetupMarker { pub version: u32, pub offline_username: String, pub online_username: String, #[serde(default)] pub created_at: Option, } impl SetupMarker { pub fn version_matches(&self) -> bool { self.version == SETUP_VERSION } } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct SandboxUserRecord { pub username: String, /// DPAPI-encrypted password blob, base64 encoded. pub password: String, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct SandboxUsersFile { pub version: u32, pub offline: SandboxUserRecord, pub online: SandboxUserRecord, } impl SandboxUsersFile { pub fn version_matches(&self) -> bool { self.version == SETUP_VERSION } } fn is_elevated() -> Result { unsafe { let mut administrators_group: *mut c_void = std::ptr::null_mut(); let ok = AllocateAndInitializeSid( &SECURITY_NT_AUTHORITY, 2, SECURITY_BUILTIN_DOMAIN_RID, DOMAIN_ALIAS_RID_ADMINS, 0, 0, 0, 0, 0, 0, &mut administrators_group, ); if ok == 0 { return Err(anyhow!( "AllocateAndInitializeSid failed: {}", GetLastError() )); } let mut is_member = 0i32; let check = CheckTokenMembership(0, administrators_group, &mut is_member as *mut _); FreeSid(administrators_group as *mut _); if check == 0 { return Err(anyhow!("CheckTokenMembership failed: {}", GetLastError())); } Ok(is_member != 0) } } fn canonical_existing(paths: &[PathBuf]) -> Vec { paths .iter() .filter_map(|p| { if !p.exists() { return None; } Some(dunce::canonicalize(p).unwrap_or_else(|_| p.clone())) }) .collect() } fn gather_read_roots( command_cwd: &Path, policy: &SandboxPolicy, policy_cwd: &Path, ) -> Vec { let mut roots: Vec = Vec::new(); for p in [ PathBuf::from(r"C:\Windows"), PathBuf::from(r"C:\Program Files"), PathBuf::from(r"C:\Program Files (x86)"), PathBuf::from(r"C:\ProgramData"), ] { roots.push(p); } if let Ok(up) = std::env::var("USERPROFILE") { roots.push(PathBuf::from(up)); } roots.push(command_cwd.to_path_buf()); if let SandboxPolicy::WorkspaceWrite { writable_roots, .. } = policy { for root in writable_roots { let candidate = if root.is_absolute() { root.clone() } else { policy_cwd.join(root) }; roots.push(candidate); } } canonical_existing(&roots) } fn gather_write_roots( policy: &SandboxPolicy, policy_cwd: &Path, command_cwd: &Path, env_map: &HashMap, ) -> Vec { let AllowDenyPaths { allow, .. } = compute_allow_paths(policy, policy_cwd, command_cwd, env_map); canonical_existing(&allow.into_iter().collect::>()) } #[derive(Serialize)] struct ElevationPayload { version: u32, offline_username: String, online_username: String, codex_home: PathBuf, read_roots: Vec, write_roots: Vec, real_user: String, } fn quote_arg(arg: &str) -> String { let needs = arg.is_empty() || arg .chars() .any(|c| matches!(c, ' ' | '\t' | '\n' | '\r' | '"')); if !needs { return arg.to_string(); } let mut out = String::from("\""); let mut bs = 0; for ch in arg.chars() { match ch { '\\' => { bs += 1; } '"' => { out.push_str(&"\\".repeat(bs * 2 + 1)); out.push('"'); bs = 0; } _ => { if bs > 0 { out.push_str(&"\\".repeat(bs)); bs = 0; } out.push(ch); } } } if bs > 0 { out.push_str(&"\\".repeat(bs * 2)); } out.push('"'); out } fn find_setup_exe() -> PathBuf { if let Ok(exe) = std::env::current_exe() { if let Some(dir) = exe.parent() { let candidate = dir.join("codex-windows-sandbox-setup.exe"); if candidate.exists() { return candidate; } } } PathBuf::from("codex-windows-sandbox-setup.exe") } fn run_setup_exe(payload: &ElevationPayload, needs_elevation: bool) -> Result<()> { use windows_sys::Win32::System::Threading::GetExitCodeProcess; use windows_sys::Win32::System::Threading::WaitForSingleObject; use windows_sys::Win32::System::Threading::INFINITE; use windows_sys::Win32::UI::Shell::ShellExecuteExW; use windows_sys::Win32::UI::Shell::SEE_MASK_NOCLOSEPROCESS; use windows_sys::Win32::UI::Shell::SHELLEXECUTEINFOW; let exe = find_setup_exe(); let payload_json = serde_json::to_string(payload)?; let payload_b64 = BASE64_STANDARD.encode(payload_json.as_bytes()); if !needs_elevation { let status = Command::new(&exe) .arg(&payload_b64) .status() .context("failed to launch setup helper")?; if !status.success() { return Err(anyhow!( "setup helper exited with status {:?}", status.code() )); } return Ok(()); } let exe_w = crate::winutil::to_wide(&exe); let params = quote_arg(&payload_b64); let params_w = crate::winutil::to_wide(params); let verb_w = crate::winutil::to_wide("runas"); let mut sei: SHELLEXECUTEINFOW = unsafe { std::mem::zeroed() }; sei.cbSize = std::mem::size_of::() as u32; sei.fMask = SEE_MASK_NOCLOSEPROCESS; sei.lpVerb = verb_w.as_ptr(); sei.lpFile = exe_w.as_ptr(); sei.lpParameters = params_w.as_ptr(); // Default show window. sei.nShow = 1; let ok = unsafe { ShellExecuteExW(&mut sei) }; if ok == 0 || sei.hProcess == 0 { return Err(anyhow!( "ShellExecuteExW failed to launch setup helper: {}", unsafe { GetLastError() } )); } unsafe { WaitForSingleObject(sei.hProcess, INFINITE); let mut code: u32 = 1; GetExitCodeProcess(sei.hProcess, &mut code); CloseHandle(sei.hProcess); if code != 0 { return Err(anyhow!("setup helper exited with status {}", code)); } } Ok(()) } pub fn run_elevated_setup( policy: &SandboxPolicy, policy_cwd: &Path, command_cwd: &Path, env_map: &HashMap, codex_home: &Path, ) -> Result<()> { let payload = ElevationPayload { version: SETUP_VERSION, offline_username: OFFLINE_USERNAME.to_string(), online_username: ONLINE_USERNAME.to_string(), codex_home: codex_home.to_path_buf(), read_roots: gather_read_roots(command_cwd, policy, policy_cwd), write_roots: gather_write_roots(policy, policy_cwd, command_cwd, env_map), real_user: std::env::var("USERNAME").unwrap_or_else(|_| "Administrators".to_string()), }; let needs_elevation = !is_elevated()?; run_setup_exe(&payload, needs_elevation) }