diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index adbb2fa775..2f12e0ae99 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -1684,6 +1684,8 @@ name = "codex-windows-sandbox" version = "0.0.0" dependencies = [ "anyhow", + "base64", + "chrono", "codex-protocol", "dirs-next", "dunce", @@ -1691,6 +1693,7 @@ dependencies = [ "serde", "serde_json", "tempfile", + "windows 0.58.0", "windows-sys 0.52.0", ] @@ -3111,7 +3114,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core", + "windows-core 0.61.2", ] [[package]] @@ -4746,7 +4749,7 @@ dependencies = [ "nix 0.30.1", "tokio", "tracing", - "windows", + "windows 0.61.3", ] [[package]] @@ -7405,6 +7408,16 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6" +dependencies = [ + "windows-core 0.58.0", + "windows-targets 0.52.6", +] + [[package]] name = "windows" version = "0.61.3" @@ -7412,7 +7425,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" dependencies = [ "windows-collections", - "windows-core", + "windows-core 0.61.2", "windows-future", "windows-link 0.1.3", "windows-numerics", @@ -7424,7 +7437,20 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" dependencies = [ - "windows-core", + "windows-core 0.61.2", +] + +[[package]] +name = "windows-core" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" +dependencies = [ + "windows-implement 0.58.0", + "windows-interface 0.58.0", + "windows-result 0.2.0", + "windows-strings 0.1.0", + "windows-targets 0.52.6", ] [[package]] @@ -7433,11 +7459,11 @@ version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" dependencies = [ - "windows-implement", - "windows-interface", + "windows-implement 0.60.0", + "windows-interface 0.59.1", "windows-link 0.1.3", - "windows-result", - "windows-strings", + "windows-result 0.3.4", + "windows-strings 0.4.2", ] [[package]] @@ -7446,11 +7472,22 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" dependencies = [ - "windows-core", + "windows-core 0.61.2", "windows-link 0.1.3", "windows-threading", ] +[[package]] +name = "windows-implement" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] + [[package]] name = "windows-implement" version = "0.60.0" @@ -7462,6 +7499,17 @@ dependencies = [ "syn 2.0.104", ] +[[package]] +name = "windows-interface" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] + [[package]] name = "windows-interface" version = "0.59.1" @@ -7491,7 +7539,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" dependencies = [ - "windows-core", + "windows-core 0.61.2", "windows-link 0.1.3", ] @@ -7502,8 +7550,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" dependencies = [ "windows-link 0.1.3", - "windows-result", - "windows-strings", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + +[[package]] +name = "windows-result" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets 0.52.6", ] [[package]] @@ -7515,6 +7572,16 @@ dependencies = [ "windows-link 0.1.3", ] +[[package]] +name = "windows-strings" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +dependencies = [ + "windows-result 0.2.0", + "windows-targets 0.52.6", +] + [[package]] name = "windows-strings" version = "0.4.2" diff --git a/codex-rs/windows-sandbox-rs/Cargo.toml b/codex-rs/windows-sandbox-rs/Cargo.toml index 29306371b2..845eda9bd9 100644 --- a/codex-rs/windows-sandbox-rs/Cargo.toml +++ b/codex-rs/windows-sandbox-rs/Cargo.toml @@ -8,11 +8,35 @@ version.workspace = true name = "codex_windows_sandbox" path = "src/lib.rs" +[[bin]] +name = "codex-windows-sandbox-setup" +path = "src/bin/setup.rs" + +[[bin]] +name = "codex-command-runner" +path = "src/bin/command_runner.rs" + +[[bin]] +name = "runner-smoke" +path = "src/bin/runner_smoke.rs" + +[[bin]] +name = "runner-stub" +path = "src/bin/runner_stub.rs" + [dependencies] anyhow = "1.0" +base64 = { workspace = true } dunce = "1.0" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" +chrono = { version = "0.4", default-features = false, features = ["clock", "std"] } +windows = { version = "0.58", features = [ + "Win32_Foundation", + "Win32_NetworkManagement_WindowsFirewall", + "Win32_System_Com", + "Win32_System_Variant", +] } [dependencies.codex-protocol] package = "codex-protocol" path = "../protocol" @@ -40,10 +64,14 @@ features = [ "Win32_System_Console", "Win32_Storage_FileSystem", "Win32_System_Diagnostics_ToolHelp", + "Win32_NetworkManagement_NetManagement", "Win32_Networking_WinSock", "Win32_System_LibraryLoader", "Win32_System_Com", + "Win32_Security_Cryptography", "Win32_Security_Authentication_Identity", + "Win32_UI_Shell", + "Win32_System_Registry", ] version = "0.52" [dev-dependencies] diff --git a/codex-rs/windows-sandbox-rs/src/acl.rs b/codex-rs/windows-sandbox-rs/src/acl.rs index f2e1e09480..254a42e4f4 100644 --- a/codex-rs/windows-sandbox-rs/src/acl.rs +++ b/codex-rs/windows-sandbox-rs/src/acl.rs @@ -160,6 +160,7 @@ pub unsafe fn dacl_effective_allows_write(p_dacl: *mut ACL, psid: *mut c_void) - // Fallback: simple allow ACE scan (already ignores inherit-only) dacl_has_write_allow_for_sid(p_dacl, psid) } +#[allow(clippy::missing_safety_doc)] pub unsafe fn add_allow_ace(path: &Path, psid: *mut c_void) -> Result { let mut p_sd: *mut c_void = std::ptr::null_mut(); let mut p_dacl: *mut ACL = std::ptr::null_mut(); @@ -217,6 +218,10 @@ pub unsafe fn add_allow_ace(path: &Path, psid: *mut c_void) -> Result { Ok(added) } +/// Adds a deny ACE to prevent write/append/delete for the given SID on the target path. +/// +/// # Safety +/// Caller must ensure `psid` points to a valid SID and `path` refers to an existing file or directory. pub unsafe fn add_deny_write_ace(path: &Path, psid: *mut c_void) -> Result { let mut p_sd: *mut c_void = std::ptr::null_mut(); let mut p_dacl: *mut ACL = std::ptr::null_mut(); @@ -330,6 +335,10 @@ pub unsafe fn revoke_ace(path: &Path, psid: *mut c_void) { } } +/// Grants RX to the null device for the given SID to support stdout/stderr redirection. +/// +/// # Safety +/// Caller must ensure `psid` is a valid SID pointer. pub unsafe fn allow_null_device(psid: *mut c_void) { let desired = 0x00020000 | 0x00040000; // READ_CONTROL | WRITE_DAC let h = CreateFileW( diff --git a/codex-rs/windows-sandbox-rs/src/bin/command_runner.rs b/codex-rs/windows-sandbox-rs/src/bin/command_runner.rs new file mode 100644 index 0000000000..9b99f08417 --- /dev/null +++ b/codex-rs/windows-sandbox-rs/src/bin/command_runner.rs @@ -0,0 +1,219 @@ +use anyhow::{Context, Result}; +use codex_windows_sandbox::{ + allow_null_device, cap_sid_file, convert_string_sid_to_sid, create_process_as_user, + create_readonly_token_with_cap_from, create_workspace_write_token_with_cap_from, + get_current_token_for_restriction, load_or_create_cap_sids, log_note, parse_policy, to_wide, + SandboxPolicy, +}; +use serde::Deserialize; +use std::collections::HashMap; +use std::ffi::c_void; +use std::fs; +use std::io::Read; +use std::path::PathBuf; +use windows_sys::Win32::Foundation::{CloseHandle, GetLastError, HANDLE}; +use windows_sys::Win32::Storage::FileSystem::{ + CreateFileW, FILE_GENERIC_READ, FILE_GENERIC_WRITE, OPEN_EXISTING, +}; +use windows_sys::Win32::System::JobObjects::AssignProcessToJobObject; +use windows_sys::Win32::System::JobObjects::CreateJobObjectW; +use windows_sys::Win32::System::JobObjects::JobObjectExtendedLimitInformation; +use windows_sys::Win32::System::JobObjects::SetInformationJobObject; +use windows_sys::Win32::System::JobObjects::JOBOBJECT_EXTENDED_LIMIT_INFORMATION; +use windows_sys::Win32::System::JobObjects::JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE; +use windows_sys::Win32::System::Threading::WaitForSingleObject; +use windows_sys::Win32::System::Threading::INFINITE; + +#[derive(Debug, Deserialize)] +struct RunnerRequest { + policy_json_or_preset: String, + #[allow(dead_code)] + sandbox_policy_cwd: PathBuf, + codex_home: PathBuf, + command: Vec, + cwd: PathBuf, + env_map: HashMap, + timeout_ms: Option, + stdin_pipe: String, + stdout_pipe: String, + stderr_pipe: String, +} + +// Best-effort early marker to detect image load before main. +#[used] +#[allow(dead_code)] +static LOAD_MARKER: fn() = load_marker; + +#[allow(dead_code)] +const fn load_marker() { + // const fn placeholder; actual work is in write_load_marker, invoked at start of main. +} + +fn write_load_marker() { + if let Some(mut p) = dirs_next::home_dir() { + p.push(".codex"); + let _ = std::fs::create_dir_all(&p); + p.push("runner_load_marker.txt"); + let _ = std::fs::write(&p, "loaded"); + } +} + +unsafe fn create_job_kill_on_close() -> Result { + let h = CreateJobObjectW(std::ptr::null_mut(), std::ptr::null()); + if h == 0 { + return Err(anyhow::anyhow!("CreateJobObjectW failed")); + } + let mut limits: JOBOBJECT_EXTENDED_LIMIT_INFORMATION = std::mem::zeroed(); + limits.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE; + let ok = SetInformationJobObject( + h, + JobObjectExtendedLimitInformation, + &mut limits as *mut _ as *mut _, + std::mem::size_of::() as u32, + ); + if ok == 0 { + return Err(anyhow::anyhow!("SetInformationJobObject failed")); + } + Ok(h) +} + +fn main() -> Result<()> { + write_load_marker(); + let mut input = String::new(); + std::io::stdin() + .read_to_string(&mut input) + .context("read request")?; + let req: RunnerRequest = + serde_json::from_str(&input).context("parse runner request json from stdin")?; + log_note( + &format!( + "runner start cwd={} cmd={:?}", + req.cwd.display(), + req.command + ), + Some(&req.codex_home), + ); + log_note( + &format!( + "stdin_pipe={} stdout_pipe={} stderr_pipe={}", + req.stdin_pipe, req.stdout_pipe, req.stderr_pipe + ), + Some(&req.codex_home), + ); + + let policy = parse_policy(&req.policy_json_or_preset).context("parse policy_json_or_preset")?; + // Ensure cap SIDs exist. + let caps = load_or_create_cap_sids(&req.codex_home); + let cap_sid_path = cap_sid_file(&req.codex_home); + fs::write(&cap_sid_path, serde_json::to_string(&caps)?).context("write cap sid file")?; + + let psid_cap: *mut c_void = match &policy { + SandboxPolicy::ReadOnly => unsafe { convert_string_sid_to_sid(&caps.readonly).unwrap() }, + SandboxPolicy::WorkspaceWrite { .. } => unsafe { + convert_string_sid_to_sid(&caps.workspace).unwrap() + }, + SandboxPolicy::DangerFullAccess => { + anyhow::bail!("DangerFullAccess is not supported for runner") + } + }; + + // Create restricted token from current process token. + let base = unsafe { get_current_token_for_restriction()? }; + let token_res: Result<(HANDLE, *mut c_void)> = unsafe { + match &policy { + SandboxPolicy::ReadOnly => create_readonly_token_with_cap_from(base, psid_cap), + SandboxPolicy::WorkspaceWrite { .. } => { + create_workspace_write_token_with_cap_from(base, psid_cap) + } + SandboxPolicy::DangerFullAccess => unreachable!(), + } + }; + let (h_token, psid_to_use) = token_res?; + unsafe { + CloseHandle(base); + } + + unsafe { + allow_null_device(psid_to_use); + } + + // Open named pipes for stdio. + let open_pipe = |name: &str, access: u32| -> Result { + let path = to_wide(name); + let handle = unsafe { + CreateFileW( + path.as_ptr(), + access, + 0, + std::ptr::null_mut(), + OPEN_EXISTING, + 0, + 0, + ) + }; + if handle == windows_sys::Win32::Foundation::INVALID_HANDLE_VALUE { + let err = unsafe { GetLastError() }; + log_note( + &format!("CreateFileW failed for pipe {name}: {err}"), + Some(&req.codex_home), + ); + return Err(anyhow::anyhow!("CreateFileW failed for pipe {name}: {err}")); + } + Ok(handle) + }; + let h_stdin = open_pipe(&req.stdin_pipe, FILE_GENERIC_READ)?; + let h_stdout = open_pipe(&req.stdout_pipe, FILE_GENERIC_WRITE)?; + let h_stderr = open_pipe(&req.stderr_pipe, FILE_GENERIC_WRITE)?; + log_note("pipes opened", Some(&req.codex_home)); + + // Build command and env, spawn with CreateProcessWithTokenW. + let (proc_info, _si) = unsafe { + create_process_as_user( + h_token, + &req.command, + &req.cwd, + &req.env_map, + Some(&req.codex_home), + Some((h_stdin, h_stdout, h_stderr)), + )? + }; + log_note("spawned child process", Some(&req.codex_home)); + + // Optional job kill on close. + let h_job = unsafe { create_job_kill_on_close().ok() }; + if let Some(job) = h_job { + unsafe { + let _ = AssignProcessToJobObject(job, proc_info.hProcess); + } + } + + // Wait for process. + let _ = unsafe { + WaitForSingleObject( + proc_info.hProcess, + req.timeout_ms.map(|ms| ms as u32).unwrap_or(INFINITE), + ) + }; + let mut exit_code: u32 = 1; + unsafe { + windows_sys::Win32::System::Threading::GetExitCodeProcess( + proc_info.hProcess, + &mut exit_code, + ); + if proc_info.hThread != 0 { + CloseHandle(proc_info.hThread); + } + if proc_info.hProcess != 0 { + CloseHandle(proc_info.hProcess); + } + CloseHandle(h_token); + if let Some(job) = h_job { + CloseHandle(job); + } + } + log_note( + &format!("runner exiting with code {}", exit_code), + Some(&req.codex_home), + ); + std::process::exit(exit_code as i32); +} diff --git a/codex-rs/windows-sandbox-rs/src/bin/logon_smoke.rs b/codex-rs/windows-sandbox-rs/src/bin/logon_smoke.rs new file mode 100644 index 0000000000..855ad1d47b --- /dev/null +++ b/codex-rs/windows-sandbox-rs/src/bin/logon_smoke.rs @@ -0,0 +1,70 @@ +use anyhow::Result; +use codex_windows_sandbox::to_wide; +use codex_windows_sandbox::{require_logon_sandbox_creds, SandboxPolicy}; +use std::collections::HashMap; +use windows_sys::Win32::Foundation::GetLastError; +use windows_sys::Win32::System::Threading::CreateProcessWithLogonW; +use windows_sys::Win32::System::Threading::LOGON_WITH_PROFILE; +use windows_sys::Win32::System::Threading::{PROCESS_INFORMATION, STARTUPINFOW}; + +fn main() -> Result<()> { + let cwd = std::env::current_dir()?; + let codex_home = dirs_next::home_dir().unwrap_or(cwd.clone()).join(".codex"); + let policy = SandboxPolicy::ReadOnly; + let _policy_json = serde_json::to_string(&policy)?; + let env_map: HashMap = HashMap::new(); + + // Fetch sandbox creds (will prompt setup if missing). + let creds = require_logon_sandbox_creds(&policy, &cwd, &cwd, &env_map, &codex_home)?; + + // Optional target override: + // - "stub" to launch runner-stub.exe + // - any other argument list is treated as the full command line to run. + let args: Vec = std::env::args().skip(1).collect(); + let target = args.first().cloned().unwrap_or_else(|| "cmd".to_string()); + 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() }; + + let user_w = to_wide(&creds.username); + let domain_w = to_wide("."); + let password_w = to_wide(&creds.password); + let cmdline = if target == "stub" { + std::env::current_exe() + .ok() + .and_then(|p| p.parent().map(|d| d.join("runner-stub.exe"))) + .and_then(|p| p.to_str().map(|s| s.to_string())) + .unwrap_or_else(|| "runner-stub.exe".to_string()) + } else if !args.is_empty() { + args.join(" ") + } else { + "cmd /c whoami".to_string() + }; + let cmd_w = to_wide(&cmdline); + let cwd_w = to_wide(&cwd); + let ok = unsafe { + CreateProcessWithLogonW( + user_w.as_ptr(), + domain_w.as_ptr(), + password_w.as_ptr(), + LOGON_WITH_PROFILE, + std::ptr::null(), + cmd_w.as_ptr() as *mut _, + 0, + std::ptr::null(), + cwd_w.as_ptr(), + &si, + &mut pi, + ) + }; + if ok == 0 { + let err = unsafe { GetLastError() }; + println!("CreateProcessWithLogonW failed: {}", err); + return Ok(()); + } + println!( + "CreateProcessWithLogonW succeeded pid={} (target={})", + pi.dwProcessId, target + ); + Ok(()) +} diff --git a/codex-rs/windows-sandbox-rs/src/bin/runner_smoke.rs b/codex-rs/windows-sandbox-rs/src/bin/runner_smoke.rs new file mode 100644 index 0000000000..598c1cfe1e --- /dev/null +++ b/codex-rs/windows-sandbox-rs/src/bin/runner_smoke.rs @@ -0,0 +1,33 @@ +use anyhow::Result; +use codex_windows_sandbox::{run_windows_sandbox_capture, SandboxPolicy}; +use std::collections::HashMap; + +fn main() -> Result<()> { + let cwd = std::env::current_dir()?; + let codex_home = dirs_next::home_dir().unwrap_or(cwd.clone()).join(".codex"); + let policy = SandboxPolicy::ReadOnly; + let policy_json = serde_json::to_string(&policy)?; + + let mut env_map = HashMap::new(); + env_map.insert("SBX_DEBUG".to_string(), "1".to_string()); + + let res = run_windows_sandbox_capture( + &policy_json, + &cwd, + &codex_home, + vec![ + "cmd".to_string(), + "/c".to_string(), + "echo smoke-runner".to_string(), + ], + &cwd, + env_map, + Some(10_000), + )?; + + println!("exit_code={}", res.exit_code); + println!("stdout={}", String::from_utf8_lossy(&res.stdout)); + println!("stderr={}", String::from_utf8_lossy(&res.stderr)); + println!("timed_out={}", res.timed_out); + Ok(()) +} diff --git a/codex-rs/windows-sandbox-rs/src/bin/runner_stub.rs b/codex-rs/windows-sandbox-rs/src/bin/runner_stub.rs new file mode 100644 index 0000000000..2d6ab52e05 --- /dev/null +++ b/codex-rs/windows-sandbox-rs/src/bin/runner_stub.rs @@ -0,0 +1,105 @@ +use anyhow::Result; +use codex_windows_sandbox::{ + convert_string_sid_to_sid, create_readonly_token_with_cap_from, + get_current_token_for_restriction, load_or_create_cap_sids, to_wide, +}; +use std::collections::HashMap; +use windows_sys::Win32::Foundation::{CloseHandle, GetLastError}; +use windows_sys::Win32::System::Threading::{ + CreateProcessAsUserW, WaitForSingleObject, CREATE_UNICODE_ENVIRONMENT, INFINITE, + PROCESS_INFORMATION, STARTUPINFOW, +}; + +fn main() -> Result<()> { + // Log current environment for diagnostics to a file under the sandbox user's profile. + let env_dump = std::env::vars() + .map(|(k, v)| format!("{k}={v}")) + .collect::>() + .join("\n"); + // Attempt multiple destinations; log errors to stderr. + if let Some(mut p) = dirs_next::home_dir() { + p.push(".codex"); + if let Err(e) = std::fs::create_dir_all(&p) { + eprintln!("failed to create {:?}: {e}", p); + } + p.push("runner_stub_env.txt"); + if let Err(e) = std::fs::write(&p, &env_dump) { + eprintln!("failed to write {:?}: {e}", p); + } + } else { + eprintln!("home_dir not available"); + } + let public_path = std::path::Path::new(r"C:\Users\Public\runner_stub_env.txt"); + if let Err(e) = std::fs::write(public_path, &env_dump) { + eprintln!("failed to write {:?}: {e}", public_path); + } + let cwd_path = std::env::current_dir() + .unwrap_or_else(|_| std::path::PathBuf::from(".")) + .join("runner_stub_env.txt"); + if let Err(e) = std::fs::write(&cwd_path, &env_dump) { + eprintln!("failed to write {:?}: {e}", cwd_path); + } + + // Create restricted token with readonly capability. + let codex_home = dirs_next::home_dir() + .unwrap_or_else(std::env::temp_dir) + .join(".codex"); + let caps = load_or_create_cap_sids(&codex_home); + let psid_cap = unsafe { convert_string_sid_to_sid(&caps.readonly).unwrap() }; + + let base = unsafe { get_current_token_for_restriction()? }; + let (restricted, _psid_used) = unsafe { create_readonly_token_with_cap_from(base, psid_cap)? }; + unsafe { + CloseHandle(base); + } + + // Launch a trivial command with the restricted token. + let cmd = "cmd"; + let args = "/c echo restricted-stub"; + 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() }; + let mut cmdline = to_wide(format!("{cmd} {args}")); + let cwd = std::env::current_dir()?; + let mut env_block: Vec = Vec::new(); + let env_map: HashMap = std::env::vars().collect(); + for (k, v) in env_map { + let mut w = to_wide(format!("{k}={v}")); + w.pop(); + env_block.extend_from_slice(&w); + env_block.push(0); + } + env_block.push(0); + let ok = unsafe { + CreateProcessAsUserW( + restricted, + std::ptr::null(), + cmdline.as_mut_ptr(), + std::ptr::null_mut(), + std::ptr::null_mut(), + 0, + CREATE_UNICODE_ENVIRONMENT, + env_block.as_ptr() as *const _, + to_wide(&cwd).as_ptr(), + &mut si, + &mut pi, + ) + }; + if ok == 0 { + eprintln!("CreateProcessAsUserW failed: {}", unsafe { GetLastError() }); + } else { + unsafe { + WaitForSingleObject(pi.hProcess, INFINITE); + if pi.hThread != 0 { + CloseHandle(pi.hThread); + } + if pi.hProcess != 0 { + CloseHandle(pi.hProcess); + } + } + } + unsafe { + CloseHandle(restricted); + } + Ok(()) +} diff --git a/codex-rs/windows-sandbox-rs/src/bin/setup.rs b/codex-rs/windows-sandbox-rs/src/bin/setup.rs new file mode 100644 index 0000000000..083129d9f6 --- /dev/null +++ b/codex-rs/windows-sandbox-rs/src/bin/setup.rs @@ -0,0 +1,784 @@ +use anyhow::Context; +use anyhow::Result; +use base64::engine::general_purpose::STANDARD as BASE64; +use base64::Engine; +use codex_windows_sandbox::add_allow_ace; +use codex_windows_sandbox::dpapi_protect; +use codex_windows_sandbox::sandbox_dir; +use codex_windows_sandbox::string_from_sid_bytes; +use codex_windows_sandbox::SETUP_VERSION; +use rand::rngs::SmallRng; +use rand::RngCore; +use rand::SeedableRng; +use serde::Deserialize; +use serde::Serialize; +use std::ffi::c_void; +use std::ffi::OsStr; +use std::fs::File; +use std::io::Write; +use std::os::windows::ffi::OsStrExt; +use std::path::Path; +use std::path::PathBuf; +use std::sync::mpsc; +use std::time::Duration; +use windows::core::Interface; +use windows::core::BSTR; +use windows::Win32::Foundation::VARIANT_TRUE; +use windows::Win32::NetworkManagement::WindowsFirewall::INetFwPolicy2; +use windows::Win32::NetworkManagement::WindowsFirewall::INetFwRule3; +use windows::Win32::NetworkManagement::WindowsFirewall::NetFwPolicy2; +use windows::Win32::NetworkManagement::WindowsFirewall::NetFwRule; +use windows::Win32::NetworkManagement::WindowsFirewall::NET_FW_ACTION_BLOCK; +use windows::Win32::NetworkManagement::WindowsFirewall::NET_FW_IP_PROTOCOL_ANY; +use windows::Win32::NetworkManagement::WindowsFirewall::NET_FW_PROFILE2_ALL; +use windows::Win32::NetworkManagement::WindowsFirewall::NET_FW_RULE_DIR_OUT; +use windows::Win32::System::Com::CoCreateInstance; +use windows::Win32::System::Com::CoInitializeEx; +use windows::Win32::System::Com::CoUninitialize; +use windows::Win32::System::Com::CLSCTX_INPROC_SERVER; +use windows::Win32::System::Com::COINIT_APARTMENTTHREADED; +use windows_sys::Win32::Foundation::GetLastError; +use windows_sys::Win32::Foundation::LocalFree; +use windows_sys::Win32::Foundation::ERROR_INSUFFICIENT_BUFFER; +use windows_sys::Win32::Foundation::HLOCAL; +use windows_sys::Win32::NetworkManagement::NetManagement::NERR_Success; +use windows_sys::Win32::NetworkManagement::NetManagement::NetLocalGroupAddMembers; +use windows_sys::Win32::NetworkManagement::NetManagement::NetUserAdd; +use windows_sys::Win32::NetworkManagement::NetManagement::NetUserSetInfo; +use windows_sys::Win32::NetworkManagement::NetManagement::LOCALGROUP_MEMBERS_INFO_3; +use windows_sys::Win32::NetworkManagement::NetManagement::UF_DONT_EXPIRE_PASSWD; +use windows_sys::Win32::NetworkManagement::NetManagement::UF_SCRIPT; +use windows_sys::Win32::NetworkManagement::NetManagement::USER_INFO_1; +use windows_sys::Win32::NetworkManagement::NetManagement::USER_INFO_1003; +use windows_sys::Win32::NetworkManagement::NetManagement::USER_PRIV_USER; +use windows_sys::Win32::Security::Authorization::ConvertStringSidToSidW; +use windows_sys::Win32::Security::Authorization::GetEffectiveRightsFromAclW; +use windows_sys::Win32::Security::Authorization::GetNamedSecurityInfoW; +use windows_sys::Win32::Security::Authorization::SetEntriesInAclW; +use windows_sys::Win32::Security::Authorization::SetNamedSecurityInfoW; +use windows_sys::Win32::Security::Authorization::EXPLICIT_ACCESS_W; +use windows_sys::Win32::Security::Authorization::GRANT_ACCESS; +use windows_sys::Win32::Security::Authorization::SE_FILE_OBJECT; +use windows_sys::Win32::Security::Authorization::TRUSTEE_IS_SID; +use windows_sys::Win32::Security::Authorization::TRUSTEE_W; +use windows_sys::Win32::Security::LookupAccountNameW; +use windows_sys::Win32::Security::ACL; +use windows_sys::Win32::Security::CONTAINER_INHERIT_ACE; +use windows_sys::Win32::Security::DACL_SECURITY_INFORMATION; +use windows_sys::Win32::Security::OBJECT_INHERIT_ACE; +use windows_sys::Win32::Security::SID_NAME_USE; +use windows_sys::Win32::Storage::FileSystem::DELETE; +use windows_sys::Win32::Storage::FileSystem::FILE_GENERIC_EXECUTE; +use windows_sys::Win32::Storage::FileSystem::FILE_GENERIC_READ; +use windows_sys::Win32::Storage::FileSystem::FILE_GENERIC_WRITE; + +#[derive(Debug, Deserialize)] +struct Payload { + version: u32, + offline_username: String, + online_username: String, + codex_home: PathBuf, + read_roots: Vec, + write_roots: Vec, + real_user: String, +} + +#[derive(Serialize)] +struct SandboxUserRecord { + username: String, + password: String, +} + +#[derive(Serialize)] +struct SandboxUsersFile { + version: u32, + offline: SandboxUserRecord, + online: SandboxUserRecord, +} + +#[derive(Serialize)] +struct SetupMarker { + version: u32, + offline_username: String, + online_username: String, + created_at: String, +} + +fn log_line(log: &mut File, msg: &str) -> Result<()> { + let ts = chrono::Utc::now().to_rfc3339(); + writeln!(log, "[{ts}] {msg}")?; + Ok(()) +} + +fn to_wide(s: &OsStr) -> Vec { + let mut v: Vec = s.encode_wide().collect(); + v.push(0); + v +} + +fn random_password() -> String { + const CHARS: &[u8] = + b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()-_=+"; + let mut rng = SmallRng::from_entropy(); + let mut buf = [0u8; 24]; + rng.fill_bytes(&mut buf); + buf.iter() + .map(|b| { + let idx = (*b as usize) % CHARS.len(); + CHARS[idx] as char + }) + .collect() +} + +fn sid_to_string(sid: &[u8]) -> Result { + string_from_sid_bytes(sid).map_err(anyhow::Error::msg) +} + +fn sid_bytes_to_psid(sid: &[u8]) -> Result<*mut c_void> { + let sid_str = sid_to_string(sid)?; + let sid_w = to_wide(OsStr::new(&sid_str)); + let mut psid: *mut c_void = std::ptr::null_mut(); + if unsafe { ConvertStringSidToSidW(sid_w.as_ptr(), &mut psid) } == 0 { + return Err(anyhow::anyhow!( + "ConvertStringSidToSidW failed: {}", + unsafe { GetLastError() } + )); + } + Ok(psid) +} + +fn ensure_local_user(name: &str, password: &str, log: &mut File) -> Result<()> { + let name_w = to_wide(OsStr::new(name)); + let pwd_w = to_wide(OsStr::new(password)); + unsafe { + let info = USER_INFO_1 { + usri1_name: name_w.as_ptr() as *mut u16, + usri1_password: pwd_w.as_ptr() as *mut u16, + usri1_password_age: 0, + usri1_priv: USER_PRIV_USER, + usri1_home_dir: std::ptr::null_mut(), + usri1_comment: std::ptr::null_mut(), + usri1_flags: UF_SCRIPT | UF_DONT_EXPIRE_PASSWD, + usri1_script_path: std::ptr::null_mut(), + }; + let status = NetUserAdd( + std::ptr::null(), + 1, + &info as *const _ as *mut u8, + std::ptr::null_mut(), + ); + if status != NERR_Success { + // Try update password via level 1003. + let pw_info = USER_INFO_1003 { + usri1003_password: pwd_w.as_ptr() as *mut u16, + }; + let upd = NetUserSetInfo( + std::ptr::null(), + name_w.as_ptr(), + 1003, + &pw_info as *const _ as *mut u8, + std::ptr::null_mut(), + ); + if upd != NERR_Success { + log_line(log, &format!("NetUserSetInfo failed for {name} code {upd}"))?; + return Err(anyhow::anyhow!( + "failed to create/update user {name}, code {status}/{upd}" + )); + } + } + let group = to_wide(OsStr::new("Users")); + let member = LOCALGROUP_MEMBERS_INFO_3 { + lgrmi3_domainandname: name_w.as_ptr() as *mut u16, + }; + let _ = NetLocalGroupAddMembers( + std::ptr::null(), + group.as_ptr(), + 3, + &member as *const _ as *mut u8, + 1, + ); + } + Ok(()) +} + +fn resolve_sid(name: &str) -> Result> { + let name_w = to_wide(OsStr::new(name)); + let mut sid_buffer = vec![0u8; 68]; + let mut sid_len: u32 = sid_buffer.len() as u32; + let mut domain: Vec = Vec::new(); + let mut domain_len: u32 = 0; + let mut use_type: SID_NAME_USE = 0; + loop { + let ok = unsafe { + LookupAccountNameW( + std::ptr::null(), + name_w.as_ptr(), + sid_buffer.as_mut_ptr() as *mut c_void, + &mut sid_len, + domain.as_mut_ptr(), + &mut domain_len, + &mut use_type, + ) + }; + if ok != 0 { + sid_buffer.truncate(sid_len as usize); + return Ok(sid_buffer); + } + let err = unsafe { GetLastError() }; + if err == ERROR_INSUFFICIENT_BUFFER { + sid_buffer.resize(sid_len as usize, 0); + domain.resize(domain_len as usize, 0); + continue; + } + return Err(anyhow::anyhow!( + "LookupAccountNameW failed for {name}: {}", + err + )); + } +} + +fn trustee_has_rx(path: &Path, trustee: &str) -> Result { + let sid = resolve_sid(trustee)?; + unsafe { + let sid_str = sid_to_string(&sid)?; + let sid_w = to_wide(OsStr::new(&sid_str)); + let mut psid: *mut c_void = std::ptr::null_mut(); + if ConvertStringSidToSidW(sid_w.as_ptr(), &mut psid) == 0 { + return Err(anyhow::anyhow!( + "ConvertStringSidToSidW failed: {}", + GetLastError() + )); + } + let path_w = to_wide(path.as_os_str()); + let mut existing_dacl: *mut ACL = std::ptr::null_mut(); + let mut sd: *mut c_void = std::ptr::null_mut(); + let get_res = GetNamedSecurityInfoW( + path_w.as_ptr() as *mut u16, + SE_FILE_OBJECT, + DACL_SECURITY_INFORMATION, + std::ptr::null_mut(), + std::ptr::null_mut(), + &mut existing_dacl, + std::ptr::null_mut(), + &mut sd, + ); + if get_res != 0 { + return Err(anyhow::anyhow!( + "GetNamedSecurityInfoW failed for {}: {}", + path.display(), + get_res + )); + } + let trustee = TRUSTEE_W { + pMultipleTrustee: std::ptr::null_mut(), + MultipleTrusteeOperation: 0, + TrusteeForm: TRUSTEE_IS_SID, + TrusteeType: TRUSTEE_IS_SID, + ptstrName: psid as *mut u16, + }; + let mut mask: u32 = 0; + let eff = GetEffectiveRightsFromAclW(existing_dacl, &trustee, &mut mask); + if eff != 0 { + return Err(anyhow::anyhow!( + "GetEffectiveRightsFromAclW failed for {}: {}", + path.display(), + eff + )); + } + if !sd.is_null() { + LocalFree(sd as HLOCAL); + } + if !psid.is_null() { + LocalFree(psid as HLOCAL); + } + let needed = FILE_GENERIC_READ | FILE_GENERIC_EXECUTE; + Ok((mask & needed) == needed) + } +} + +fn collect_system_roots() -> Vec { + let mut roots = Vec::new(); + if let Ok(sr) = std::env::var("SystemRoot") { + roots.push(PathBuf::from(sr)); + } else { + roots.push(PathBuf::from(r"C:\Windows")); + } + if let Ok(pf) = std::env::var("ProgramFiles") { + roots.push(PathBuf::from(pf)); + } else { + roots.push(PathBuf::from(r"C:\Program Files")); + } + if let Ok(pf86) = std::env::var("ProgramFiles(x86)") { + roots.push(PathBuf::from(pf86)); + } else { + roots.push(PathBuf::from(r"C:\Program Files (x86)")); + } + if let Ok(pd) = std::env::var("ProgramData") { + roots.push(PathBuf::from(pd)); + } else { + roots.push(PathBuf::from(r"C:\ProgramData")); + } + roots +} + +fn add_inheritable_allow_no_log(path: &Path, sid: &[u8], mask: u32) -> Result<()> { + unsafe { + let mut psid: *mut c_void = std::ptr::null_mut(); + let sid_str = sid_to_string(sid)?; + let sid_w = to_wide(OsStr::new(&sid_str)); + if ConvertStringSidToSidW(sid_w.as_ptr(), &mut psid) == 0 { + return Err(anyhow::anyhow!( + "ConvertStringSidToSidW failed: {}", + GetLastError() + )); + } + let path_w = to_wide(path.as_os_str()); + + let mut existing_dacl: *mut ACL = std::ptr::null_mut(); + let mut sd: *mut c_void = std::ptr::null_mut(); + let get_res = GetNamedSecurityInfoW( + path_w.as_ptr() as *mut u16, + SE_FILE_OBJECT, + DACL_SECURITY_INFORMATION, + std::ptr::null_mut(), + std::ptr::null_mut(), + &mut existing_dacl, + std::ptr::null_mut(), + &mut sd, + ); + if get_res != 0 { + return Err(anyhow::anyhow!( + "GetNamedSecurityInfoW failed for {}: {}", + path.display(), + get_res + )); + } + let trustee = TRUSTEE_W { + pMultipleTrustee: std::ptr::null_mut(), + MultipleTrusteeOperation: 0, + TrusteeForm: TRUSTEE_IS_SID, + TrusteeType: TRUSTEE_IS_SID, + ptstrName: psid as *mut u16, + }; + let ea = EXPLICIT_ACCESS_W { + grfAccessPermissions: mask, + grfAccessMode: GRANT_ACCESS, + grfInheritance: OBJECT_INHERIT_ACE | CONTAINER_INHERIT_ACE, + Trustee: trustee, + }; + let mut new_dacl: *mut ACL = std::ptr::null_mut(); + let set = SetEntriesInAclW(1, &ea, existing_dacl, &mut new_dacl); + if set != 0 { + return Err(anyhow::anyhow!("SetEntriesInAclW failed: {}", set)); + } + let res = SetNamedSecurityInfoW( + path_w.as_ptr() as *mut u16, + SE_FILE_OBJECT, + DACL_SECURITY_INFORMATION, + std::ptr::null_mut(), + std::ptr::null_mut(), + new_dacl, + std::ptr::null_mut(), + ); + if res != 0 { + return Err(anyhow::anyhow!( + "SetNamedSecurityInfoW failed for {}: {}", + path.display(), + res + )); + } + if !new_dacl.is_null() { + LocalFree(new_dacl as HLOCAL); + } + if !sd.is_null() { + LocalFree(sd as HLOCAL); + } + if !psid.is_null() { + LocalFree(psid as HLOCAL); + } + } + Ok(()) +} + +fn try_add_inheritable_allow_with_timeout( + path: &Path, + sid: &[u8], + mask: u32, + _log: &mut File, + timeout: Duration, +) -> Result<()> { + let (tx, rx) = mpsc::channel::>(); + let path_buf = path.to_path_buf(); + let sid_vec = sid.to_vec(); + std::thread::spawn(move || { + let res = add_inheritable_allow_no_log(&path_buf, &sid_vec, mask); + let _ = tx.send(res); + }); + match rx.recv_timeout(timeout) { + Ok(res) => res, + Err(mpsc::RecvTimeoutError::Timeout) => Err(anyhow::anyhow!( + "ACL grant timed out on {} after {:?}", + path.display(), + timeout + )), + Err(e) => Err(anyhow::anyhow!( + "ACL grant channel error on {}: {e}", + path.display() + )), + } +} + +fn run_netsh_firewall(sid: &str, log: &mut File) -> Result<()> { + let local_user_spec = format!("O:LSD:(A;;CC;;;{sid})"); + let hr = unsafe { CoInitializeEx(None, COINIT_APARTMENTTHREADED) }; + if hr.is_err() { + return Err(anyhow::anyhow!("CoInitializeEx failed: {hr:?}")); + } + let result = unsafe { + (|| -> Result<()> { + let policy: INetFwPolicy2 = CoCreateInstance(&NetFwPolicy2, None, CLSCTX_INPROC_SERVER) + .map_err(|e| anyhow::anyhow!("CoCreateInstance NetFwPolicy2: {e:?}"))?; + let rules = policy + .Rules() + .map_err(|e| anyhow::anyhow!("INetFwPolicy2::Rules: {e:?}"))?; + let name = BSTR::from("Codex Sandbox Offline - Block Outbound"); + let rule: INetFwRule3 = match rules.Item(&name) { + Ok(existing) => existing.cast().map_err(|e| { + anyhow::anyhow!("cast existing firewall rule to INetFwRule3: {e:?}") + })?, + Err(_) => { + let new_rule: INetFwRule3 = + CoCreateInstance(&NetFwRule, None, CLSCTX_INPROC_SERVER) + .map_err(|e| anyhow::anyhow!("CoCreateInstance NetFwRule: {e:?}"))?; + new_rule + .SetName(&name) + .map_err(|e| anyhow::anyhow!("SetName: {e:?}"))?; + new_rule + .SetDirection(NET_FW_RULE_DIR_OUT) + .map_err(|e| anyhow::anyhow!("SetDirection: {e:?}"))?; + new_rule + .SetAction(NET_FW_ACTION_BLOCK) + .map_err(|e| anyhow::anyhow!("SetAction: {e:?}"))?; + new_rule + .SetEnabled(VARIANT_TRUE) + .map_err(|e| anyhow::anyhow!("SetEnabled: {e:?}"))?; + new_rule + .SetProfiles(NET_FW_PROFILE2_ALL.0) + .map_err(|e| anyhow::anyhow!("SetProfiles: {e:?}"))?; + new_rule + .SetProtocol(NET_FW_IP_PROTOCOL_ANY.0) + .map_err(|e| anyhow::anyhow!("SetProtocol: {e:?}"))?; + rules + .Add(&new_rule) + .map_err(|e| anyhow::anyhow!("Rules::Add: {e:?}"))?; + new_rule + } + }; + rule.SetLocalUserAuthorizedList(&BSTR::from(local_user_spec.as_str())) + .map_err(|e| anyhow::anyhow!("SetLocalUserAuthorizedList: {e:?}"))?; + rule.SetEnabled(VARIANT_TRUE) + .map_err(|e| anyhow::anyhow!("SetEnabled: {e:?}"))?; + rule.SetProfiles(NET_FW_PROFILE2_ALL.0) + .map_err(|e| anyhow::anyhow!("SetProfiles: {e:?}"))?; + rule.SetAction(NET_FW_ACTION_BLOCK) + .map_err(|e| anyhow::anyhow!("SetAction: {e:?}"))?; + rule.SetDirection(NET_FW_RULE_DIR_OUT) + .map_err(|e| anyhow::anyhow!("SetDirection: {e:?}"))?; + rule.SetProtocol(NET_FW_IP_PROTOCOL_ANY.0) + .map_err(|e| anyhow::anyhow!("SetProtocol: {e:?}"))?; + log_line( + log, + &format!( + "firewall rule configured via COM with LocalUserAuthorizedList={local_user_spec}" + ), + )?; + Ok(()) + })() + }; + unsafe { + CoUninitialize(); + } + result +} + +fn lock_sandbox_dir(dir: &Path, real_user: &str, log: &mut File) -> Result<()> { + std::fs::create_dir_all(dir)?; + let system_sid = resolve_sid("SYSTEM")?; + let admins_sid = resolve_sid("Administrators")?; + let real_sid = resolve_sid(real_user)?; + let entries = [ + ( + system_sid, + FILE_GENERIC_READ | FILE_GENERIC_WRITE | FILE_GENERIC_EXECUTE | DELETE, + ), + ( + admins_sid, + FILE_GENERIC_READ | FILE_GENERIC_WRITE | FILE_GENERIC_EXECUTE | DELETE, + ), + ( + real_sid, + FILE_GENERIC_READ | FILE_GENERIC_WRITE | FILE_GENERIC_EXECUTE, + ), + ]; + unsafe { + let mut eas: Vec = Vec::new(); + let mut sids: Vec<*mut c_void> = Vec::new(); + for (sid_bytes, mask) in entries { + let sid_str = sid_to_string(&sid_bytes)?; + let sid_w = to_wide(OsStr::new(&sid_str)); + let mut psid: *mut c_void = std::ptr::null_mut(); + if ConvertStringSidToSidW(sid_w.as_ptr(), &mut psid) == 0 { + return Err(anyhow::anyhow!( + "ConvertStringSidToSidW failed: {}", + GetLastError() + )); + } + sids.push(psid); + eas.push(EXPLICIT_ACCESS_W { + grfAccessPermissions: mask, + grfAccessMode: GRANT_ACCESS, + grfInheritance: OBJECT_INHERIT_ACE | CONTAINER_INHERIT_ACE, + Trustee: TRUSTEE_W { + pMultipleTrustee: std::ptr::null_mut(), + MultipleTrusteeOperation: 0, + TrusteeForm: TRUSTEE_IS_SID, + TrusteeType: TRUSTEE_IS_SID, + ptstrName: psid as *mut u16, + }, + }); + } + let mut new_dacl: *mut ACL = std::ptr::null_mut(); + let set = SetEntriesInAclW( + eas.len() as u32, + eas.as_ptr(), + std::ptr::null_mut(), + &mut new_dacl, + ); + if set != 0 { + return Err(anyhow::anyhow!( + "SetEntriesInAclW sandbox dir failed: {}", + set + )); + } + let path_w = to_wide(dir.as_os_str()); + let res = SetNamedSecurityInfoW( + path_w.as_ptr() as *mut u16, + SE_FILE_OBJECT, + DACL_SECURITY_INFORMATION, + std::ptr::null_mut(), + std::ptr::null_mut(), + new_dacl, + std::ptr::null_mut(), + ); + if res != 0 { + return Err(anyhow::anyhow!( + "SetNamedSecurityInfoW sandbox dir failed: {}", + res + )); + } + if !new_dacl.is_null() { + LocalFree(new_dacl as HLOCAL); + } + for sid in sids { + if !sid.is_null() { + LocalFree(sid as HLOCAL); + } + } + } + log_line( + log, + &format!("sandbox dir ACL applied at {}", dir.display()), + )?; + Ok(()) +} + +fn write_secrets( + codex_home: &Path, + offline_user: &str, + offline_pwd: &str, + online_user: &str, + online_pwd: &str, +) -> Result<()> { + let sandbox_dir = sandbox_dir(codex_home); + std::fs::create_dir_all(&sandbox_dir)?; + let offline_blob = dpapi_protect(offline_pwd.as_bytes())?; + let online_blob = dpapi_protect(online_pwd.as_bytes())?; + let users = SandboxUsersFile { + version: SETUP_VERSION, + offline: SandboxUserRecord { + username: offline_user.to_string(), + password: BASE64.encode(offline_blob), + }, + online: SandboxUserRecord { + username: online_user.to_string(), + password: BASE64.encode(online_blob), + }, + }; + let marker = SetupMarker { + version: SETUP_VERSION, + offline_username: offline_user.to_string(), + online_username: online_user.to_string(), + created_at: chrono::Utc::now().to_rfc3339(), + }; + let users_path = sandbox_dir.join("sandbox_users.json"); + let marker_path = sandbox_dir.join("setup_marker.json"); + std::fs::write(users_path, serde_json::to_vec_pretty(&users)?)?; + std::fs::write(marker_path, serde_json::to_vec_pretty(&marker)?)?; + Ok(()) +} + +fn main() -> Result<()> { + let mut args = std::env::args().collect::>(); + if args.len() != 2 { + anyhow::bail!("expected payload argument"); + } + let payload_b64 = args.remove(1); + let payload_json = BASE64 + .decode(payload_b64) + .context("failed to decode payload b64")?; + let payload: Payload = + serde_json::from_slice(&payload_json).context("failed to parse payload json")?; + if payload.version != SETUP_VERSION { + anyhow::bail!("setup version mismatch"); + } + let log_path = payload.codex_home.join("codex_sbx_setup.log"); + std::fs::create_dir_all(&payload.codex_home)?; + let mut log = File::options() + .create(true) + .append(true) + .open(&log_path) + .context("open log")?; + log_line(&mut log, "setup binary started")?; + let offline_pwd = random_password(); + let online_pwd = random_password(); + log_line( + &mut log, + &format!( + "ensuring sandbox users offline={} online={}", + payload.offline_username, payload.online_username + ), + )?; + ensure_local_user(&payload.offline_username, &offline_pwd, &mut log)?; + ensure_local_user(&payload.online_username, &online_pwd, &mut log)?; + let offline_sid = resolve_sid(&payload.offline_username)?; + let online_sid = resolve_sid(&payload.online_username)?; + let offline_psid = sid_bytes_to_psid(&offline_sid)?; + let online_psid = sid_bytes_to_psid(&online_sid)?; + let system_roots = collect_system_roots(); + let offline_sid_str = sid_to_string(&offline_sid)?; + log_line( + &mut log, + &format!( + "resolved SIDs offline={} online={}", + offline_sid_str, + sid_to_string(&online_sid)? + ), + )?; + run_netsh_firewall(&offline_sid_str, &mut log)?; + + for root in &payload.read_roots { + if !root.exists() { + continue; + } + let mut skipped = false; + for trustee in ["Users", "Authenticated Users", "Everyone"] { + if trustee_has_rx(root, trustee).unwrap_or(false) { + log_line( + &mut log, + &format!("{trustee} already has RX on {}; skipping", root.display()), + )?; + skipped = true; + break; + } + } + if skipped { + continue; + } + if system_roots.contains(root) { + log_line( + &mut log, + &format!( + "system root {} missing RX for Users/AU/Everyone; skipping to avoid hang", + root.display() + ), + )?; + continue; + } + log_line( + &mut log, + &format!("granting read ACE to {} for sandbox users", root.display()), + )?; + let read_mask = FILE_GENERIC_READ | FILE_GENERIC_EXECUTE; + for (label, sid_bytes) in [("offline", &offline_sid), ("online", &online_sid)] { + match try_add_inheritable_allow_with_timeout( + root, + sid_bytes, + read_mask, + &mut log, + Duration::from_millis(25), + ) { + Ok(_) => {} + Err(e) => { + log_line( + &mut log, + &format!( + "grant read ACE timed out/failed on {} for {label}: {e}", + root.display() + ), + )?; + // Best-effort: skip to next root. + continue; + } + } + } + log_line(&mut log, &format!("granted read ACE to {}", root.display()))?; + } + + for root in &payload.write_roots { + if !root.exists() { + continue; + } + log_line( + &mut log, + &format!("granting write ACE to {} for sandbox users", root.display()), + )?; + unsafe { + add_allow_ace(root, offline_psid) + .with_context(|| format!("failed to grant write ACE on {}", root.display()))?; + add_allow_ace(root, online_psid) + .with_context(|| format!("failed to grant write ACE on {}", root.display()))?; + } + log_line( + &mut log, + &format!("granted write ACE to {}", root.display()), + )?; + } + + lock_sandbox_dir( + &sandbox_dir(&payload.codex_home), + &payload.real_user, + &mut log, + )?; + log_line(&mut log, "sandbox dir ACL applied")?; + write_secrets( + &payload.codex_home, + &payload.offline_username, + &offline_pwd, + &payload.online_username, + &online_pwd, + )?; + log_line( + &mut log, + "sandbox users and marker written (sandbox_users.json, setup_marker.json)", + )?; + unsafe { + if !offline_psid.is_null() { + LocalFree(offline_psid as HLOCAL); + } + if !online_psid.is_null() { + LocalFree(online_psid as HLOCAL); + } + } + log_line(&mut log, "setup binary completed")?; + Ok(()) +} diff --git a/codex-rs/windows-sandbox-rs/src/dpapi.rs b/codex-rs/windows-sandbox-rs/src/dpapi.rs new file mode 100644 index 0000000000..d254fa1169 --- /dev/null +++ b/codex-rs/windows-sandbox-rs/src/dpapi.rs @@ -0,0 +1,81 @@ +use anyhow::anyhow; +use anyhow::Result; +use windows_sys::Win32::Foundation::GetLastError; +use windows_sys::Win32::Foundation::HLOCAL; +use windows_sys::Win32::Foundation::LocalFree; +use windows_sys::Win32::Security::Cryptography::CryptProtectData; +use windows_sys::Win32::Security::Cryptography::CryptUnprotectData; +use windows_sys::Win32::Security::Cryptography::CRYPT_INTEGER_BLOB; +use windows_sys::Win32::Security::Cryptography::CRYPTPROTECT_UI_FORBIDDEN; + +fn make_blob(data: &[u8]) -> CRYPT_INTEGER_BLOB { + CRYPT_INTEGER_BLOB { + cbData: data.len() as u32, + pbData: data.as_ptr() as *mut u8, + } +} + +#[allow(clippy::unnecessary_mut_passed)] +pub fn protect(data: &[u8]) -> Result> { + let mut in_blob = make_blob(data); + let mut out_blob = CRYPT_INTEGER_BLOB { + cbData: 0, + pbData: std::ptr::null_mut(), + }; + let ok = unsafe { + CryptProtectData( + &mut in_blob, + std::ptr::null(), + std::ptr::null(), + std::ptr::null_mut(), + std::ptr::null_mut(), + CRYPTPROTECT_UI_FORBIDDEN, + &mut out_blob, + ) + }; + if ok == 0 { + return Err(anyhow!("CryptProtectData failed: {}", unsafe { GetLastError() })); + } + let slice = + unsafe { std::slice::from_raw_parts(out_blob.pbData, out_blob.cbData as usize) }.to_vec(); + unsafe { + if !out_blob.pbData.is_null() { + LocalFree(out_blob.pbData as HLOCAL); + } + } + Ok(slice) +} + +#[allow(clippy::unnecessary_mut_passed)] +pub fn unprotect(blob: &[u8]) -> Result> { + let mut in_blob = make_blob(blob); + let mut out_blob = CRYPT_INTEGER_BLOB { + cbData: 0, + pbData: std::ptr::null_mut(), + }; + let ok = unsafe { + CryptUnprotectData( + &mut in_blob, + std::ptr::null_mut(), + std::ptr::null(), + std::ptr::null_mut(), + std::ptr::null_mut(), + CRYPTPROTECT_UI_FORBIDDEN, + &mut out_blob, + ) + }; + if ok == 0 { + return Err(anyhow!( + "CryptUnprotectData failed: {}", + unsafe { GetLastError() } + )); + } + let slice = + unsafe { std::slice::from_raw_parts(out_blob.pbData, out_blob.cbData as usize) }.to_vec(); + unsafe { + if !out_blob.pbData.is_null() { + LocalFree(out_blob.pbData as HLOCAL); + } + } + Ok(slice) +} diff --git a/codex-rs/windows-sandbox-rs/src/identity.rs b/codex-rs/windows-sandbox-rs/src/identity.rs new file mode 100644 index 0000000000..6cfbb7748c --- /dev/null +++ b/codex-rs/windows-sandbox-rs/src/identity.rs @@ -0,0 +1,136 @@ +use crate::dpapi; +use crate::logging::debug_log; +use crate::policy::SandboxPolicy; +use crate::setup::run_elevated_setup; +use crate::setup::sandbox_users_path; +use crate::setup::setup_marker_path; +use crate::setup::SandboxUserRecord; +use crate::setup::SandboxUsersFile; +use crate::setup::SetupMarker; +use anyhow::anyhow; +use anyhow::Context; +use anyhow::Result; +use base64::engine::general_purpose::STANDARD as BASE64_STANDARD; +use base64::Engine; +use std::collections::HashMap; +use std::fs; +use std::path::Path; + +#[derive(Debug, Clone)] +struct SandboxIdentity { + username: String, + password: String, + #[allow(dead_code)] + offline: bool, +} + +#[derive(Debug, Clone)] +pub struct SandboxCreds { + pub username: String, + pub password: String, +} + +fn load_marker(codex_home: &Path) -> Result> { + let path = setup_marker_path(codex_home); + let marker = match fs::read_to_string(&path) { + Ok(contents) => match serde_json::from_str::(&contents) { + Ok(m) => Some(m), + Err(err) => { + debug_log( + &format!("sandbox setup marker parse failed: {}", err), + Some(codex_home), + ); + None + } + }, + Err(err) if err.kind() == std::io::ErrorKind::NotFound => None, + Err(err) => { + debug_log( + &format!("sandbox setup marker read failed: {}", err), + Some(codex_home), + ); + None + } + }; + Ok(marker) +} + +fn load_users(codex_home: &Path) -> Result> { + let path = sandbox_users_path(codex_home); + let file = match fs::read_to_string(&path) { + Ok(contents) => contents, + Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(None), + Err(err) => { + debug_log( + &format!("sandbox users read failed: {}", err), + Some(codex_home), + ); + return Ok(None); + } + }; + match serde_json::from_str::(&file) { + Ok(users) => Ok(Some(users)), + Err(err) => { + debug_log( + &format!("sandbox users parse failed: {}", err), + Some(codex_home), + ); + Ok(None) + } + } +} + +fn decode_password(record: &SandboxUserRecord) -> Result { + let blob = BASE64_STANDARD + .decode(record.password.as_bytes()) + .context("base64 decode password")?; + let decrypted = dpapi::unprotect(&blob)?; + let pwd = String::from_utf8(decrypted).context("sandbox password not utf-8")?; + Ok(pwd) +} + +fn select_identity(policy: &SandboxPolicy, codex_home: &Path) -> Result> { + let _marker = match load_marker(codex_home)? { + Some(m) if m.version_matches() => m, + _ => return Ok(None), + }; + let users = match load_users(codex_home)? { + Some(u) if u.version_matches() => u, + _ => return Ok(None), + }; + let offline = !policy.has_full_network_access(); + let chosen = if offline { + users.offline + } else { + users.online + }; + let password = decode_password(&chosen)?; + Ok(Some(SandboxIdentity { + username: chosen.username.clone(), + password, + offline, + })) +} + +pub fn require_logon_sandbox_creds( + policy: &SandboxPolicy, + policy_cwd: &Path, + command_cwd: &Path, + env_map: &HashMap, + codex_home: &Path, +) -> Result { + let mut identity = select_identity(policy, codex_home)?; + if identity.is_none() { + run_elevated_setup(policy, policy_cwd, command_cwd, env_map, codex_home)?; + identity = select_identity(policy, codex_home)?; + } + let identity = identity.ok_or_else(|| { + anyhow!( + "Windows sandbox setup is missing or out of date; rerun the sandbox setup with elevation" + ) + })?; + Ok(SandboxCreds { + username: identity.username, + password: identity.password, + }) +} diff --git a/codex-rs/windows-sandbox-rs/src/lib.rs b/codex-rs/windows-sandbox-rs/src/lib.rs index 45595052ab..8985b84d48 100644 --- a/codex-rs/windows-sandbox-rs/src/lib.rs +++ b/codex-rs/windows-sandbox-rs/src/lib.rs @@ -4,18 +4,55 @@ macro_rules! windows_modules { }; } -windows_modules!(acl, allow, audit, cap, env, logging, policy, token, winutil); +windows_modules!( + acl, allow, audit, cap, dpapi, env, identity, logging, policy, process, setup, token, winutil +); +#[cfg(target_os = "windows")] +pub use acl::{add_allow_ace, add_deny_write_ace, allow_null_device}; +#[cfg(target_os = "windows")] +pub use allow::compute_allow_paths; #[cfg(target_os = "windows")] pub use audit::apply_world_writable_scan_and_denies; #[cfg(target_os = "windows")] +pub use cap::{cap_sid_file, load_or_create_cap_sids}; +#[cfg(target_os = "windows")] +pub use dpapi::protect as dpapi_protect; +#[cfg(target_os = "windows")] +pub use dpapi::unprotect as dpapi_unprotect; +#[cfg(target_os = "windows")] +pub use identity::require_logon_sandbox_creds; +#[cfg(target_os = "windows")] +pub use logging::log_note; +#[cfg(target_os = "windows")] +pub use policy::{parse_policy, SandboxPolicy}; +#[cfg(target_os = "windows")] +pub use process::create_process_as_user; +#[cfg(target_os = "windows")] +pub use setup::run_elevated_setup; +#[cfg(target_os = "windows")] +pub use setup::sandbox_dir; +#[cfg(target_os = "windows")] +pub use setup::SETUP_VERSION; +#[cfg(target_os = "windows")] +pub use token::{ + convert_string_sid_to_sid, create_readonly_token_with_cap_from, + create_workspace_write_token_with_cap_from, get_current_token_for_restriction, +}; +#[cfg(target_os = "windows")] pub use windows_impl::run_windows_sandbox_capture; #[cfg(target_os = "windows")] pub use windows_impl::CaptureResult; +#[cfg(target_os = "windows")] +pub use winutil::string_from_sid_bytes; +#[cfg(target_os = "windows")] +pub use winutil::to_wide; #[cfg(not(target_os = "windows"))] pub use stub::apply_world_writable_scan_and_denies; #[cfg(not(target_os = "windows"))] +pub use stub::run_elevated_setup; +#[cfg(not(target_os = "windows"))] pub use stub::run_windows_sandbox_capture; #[cfg(not(target_os = "windows"))] pub use stub::CaptureResult; @@ -33,8 +70,10 @@ mod windows_impl { use super::env::apply_no_network_to_env; use super::env::ensure_non_interactive_pager; use super::env::normalize_null_device_env; + use super::identity::require_logon_sandbox_creds; use super::logging::debug_log; use super::logging::log_failure; + use super::logging::log_note; use super::logging::log_start; use super::logging::log_success; use super::policy::parse_policy; @@ -43,29 +82,45 @@ mod windows_impl { use super::winutil::format_last_error; use super::winutil::to_wide; use anyhow::Result; + use rand::rngs::SmallRng; + use rand::Rng; + use rand::SeedableRng; use std::collections::HashMap; use std::ffi::c_void; use std::fs; use std::io; + use std::os::windows::io::FromRawHandle; use std::path::Path; use std::path::PathBuf; use std::ptr; use windows_sys::Win32::Foundation::CloseHandle; use windows_sys::Win32::Foundation::GetLastError; - use windows_sys::Win32::Foundation::SetHandleInformation; use windows_sys::Win32::Foundation::HANDLE; - use windows_sys::Win32::Foundation::HANDLE_FLAG_INHERIT; - use windows_sys::Win32::System::Pipes::CreatePipe; - use windows_sys::Win32::System::Threading::CreateProcessAsUserW; + use windows_sys::Win32::System::Pipes::ConnectNamedPipe; + use windows_sys::Win32::System::Pipes::CreateNamedPipeW; + // PIPE_ACCESS_DUPLEX is 0x00000003; not exposed in windows-sys 0.52, so use the value directly. + const PIPE_ACCESS_DUPLEX: u32 = 0x0000_0003; + use windows_sys::Win32::Security::Authorization::ConvertStringSecurityDescriptorToSecurityDescriptorW; + use windows_sys::Win32::Security::LogonUserW; + use windows_sys::Win32::Security::LOGON32_LOGON_INTERACTIVE; + use windows_sys::Win32::Security::LOGON32_PROVIDER_DEFAULT; + use windows_sys::Win32::Security::{PSECURITY_DESCRIPTOR, SECURITY_ATTRIBUTES}; + use windows_sys::Win32::System::Environment::CreateEnvironmentBlock; + use windows_sys::Win32::System::Environment::DestroyEnvironmentBlock; + use windows_sys::Win32::System::Pipes::PIPE_READMODE_BYTE; + use windows_sys::Win32::System::Pipes::PIPE_TYPE_BYTE; + use windows_sys::Win32::System::Pipes::PIPE_WAIT; + use windows_sys::Win32::System::Threading::CreateProcessWithLogonW; use windows_sys::Win32::System::Threading::GetExitCodeProcess; use windows_sys::Win32::System::Threading::WaitForSingleObject; use windows_sys::Win32::System::Threading::CREATE_UNICODE_ENVIRONMENT; 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::STARTF_USESTDHANDLES; use windows_sys::Win32::System::Threading::STARTUPINFOW; - - type PipeHandles = ((HANDLE, HANDLE), (HANDLE, HANDLE), (HANDLE, HANDLE)); + use windows_sys::Win32::UI::Shell::LoadUserProfileA; + use windows_sys::Win32::UI::Shell::UnloadUserProfile; + use windows_sys::Win32::UI::Shell::PROFILEINFOA; fn should_apply_network_block(policy: &SandboxPolicy) -> bool { !policy.has_full_network_access() @@ -83,6 +138,26 @@ mod windows_impl { Ok(()) } + fn find_runner_exe() -> PathBuf { + if let Ok(exe) = std::env::current_exe() { + if let Some(dir) = exe.parent() { + let candidate = dir.join("codex-command-runner.exe"); + if candidate.exists() { + return candidate; + } + let release_candidate = dir + .parent() + .map(|p| p.join("release").join("codex-command-runner.exe")); + if let Some(rel) = release_candidate { + if rel.exists() { + return rel; + } + } + } + } + PathBuf::from("codex-command-runner.exe") + } + fn make_env_block(env: &HashMap) -> Vec { let mut items: Vec<(String, String)> = env.iter().map(|(k, v)| (k.clone(), v.clone())).collect(); @@ -143,32 +218,64 @@ mod windows_impl { quoted } - unsafe fn setup_stdio_pipes() -> io::Result { - let mut in_r: HANDLE = 0; - let mut in_w: HANDLE = 0; - let mut out_r: HANDLE = 0; - let mut out_w: HANDLE = 0; - let mut err_r: HANDLE = 0; - let mut err_w: HANDLE = 0; - if CreatePipe(&mut in_r, &mut in_w, ptr::null_mut(), 0) == 0 { - return Err(io::Error::from_raw_os_error(GetLastError() as i32)); + fn pipe_name(suffix: &str) -> String { + let mut rng = SmallRng::from_entropy(); + format!(r"\\.\pipe\codex-runner-{:x}-{}", rng.gen::(), suffix) + } + + fn create_named_pipe(name: &str, access: u32) -> io::Result { + // Allow sandbox users to connect by granting Everyone full access on the pipe. + let sddl = to_wide("D:(A;;GA;;;WD)"); + let mut sd: PSECURITY_DESCRIPTOR = ptr::null_mut(); + let ok = unsafe { + ConvertStringSecurityDescriptorToSecurityDescriptorW( + sddl.as_ptr(), + 1, // SDDL_REVISION_1 + &mut sd, + ptr::null_mut(), + ) + }; + if ok == 0 { + return Err(io::Error::from_raw_os_error(unsafe { + GetLastError() as i32 + })); } - if CreatePipe(&mut out_r, &mut out_w, ptr::null_mut(), 0) == 0 { - return Err(io::Error::from_raw_os_error(GetLastError() as i32)); + let mut sa = SECURITY_ATTRIBUTES { + nLength: std::mem::size_of::() as u32, + lpSecurityDescriptor: sd, + bInheritHandle: 0, + }; + let wide = to_wide(name); + let h = unsafe { + CreateNamedPipeW( + wide.as_ptr(), + access, + PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT, + 1, + 65536, + 65536, + 0, + &mut sa as *mut SECURITY_ATTRIBUTES, + ) + }; + if h == 0 || h == windows_sys::Win32::Foundation::INVALID_HANDLE_VALUE { + return Err(io::Error::from_raw_os_error(unsafe { + GetLastError() as i32 + })); } - if CreatePipe(&mut err_r, &mut err_w, ptr::null_mut(), 0) == 0 { - return Err(io::Error::from_raw_os_error(GetLastError() as i32)); + Ok(h) + } + + fn connect_pipe(h: HANDLE) -> io::Result<()> { + let ok = unsafe { ConnectNamedPipe(h, ptr::null_mut()) }; + if ok == 0 { + let err = unsafe { GetLastError() }; + const ERROR_PIPE_CONNECTED: u32 = 535; + if err != ERROR_PIPE_CONNECTED { + return Err(io::Error::from_raw_os_error(err as i32)); + } } - if SetHandleInformation(in_r, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT) == 0 { - return Err(io::Error::from_raw_os_error(GetLastError() as i32)); - } - if SetHandleInformation(out_w, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT) == 0 { - return Err(io::Error::from_raw_os_error(GetLastError() as i32)); - } - if SetHandleInformation(err_w, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT) == 0 { - return Err(io::Error::from_raw_os_error(GetLastError() as i32)); - } - Ok(((in_r, in_w), (out_r, out_w), (err_r, err_w))) + Ok(()) } pub struct CaptureResult { @@ -178,6 +285,20 @@ mod windows_impl { pub timed_out: bool, } + #[derive(serde::Serialize)] + struct RunnerPayload { + policy_json_or_preset: String, + sandbox_policy_cwd: PathBuf, + codex_home: PathBuf, + command: Vec, + cwd: PathBuf, + env_map: HashMap, + timeout_ms: Option, + stdin_pipe: String, + stdout_pipe: String, + stderr_pipe: String, + } + pub fn run_windows_sandbox_capture( policy_json_or_preset: &str, sandbox_policy_cwd: &Path, @@ -201,99 +322,270 @@ mod windows_impl { log_start(&command, logs_base_dir); let cap_sid_path = cap_sid_file(codex_home); let is_workspace_write = matches!(&policy, SandboxPolicy::WorkspaceWrite { .. }); + let sandbox_creds = + require_logon_sandbox_creds(&policy, sandbox_policy_cwd, cwd, &env_map, codex_home)?; - let (h_token, psid_to_use): (HANDLE, *mut c_void) = unsafe { - match &policy { - SandboxPolicy::ReadOnly => { - let caps = load_or_create_cap_sids(codex_home); - ensure_dir(&cap_sid_path)?; - fs::write(&cap_sid_path, serde_json::to_string(&caps)?)?; - let psid = convert_string_sid_to_sid(&caps.readonly).unwrap(); - super::token::create_readonly_token_with_cap(psid)? - } - SandboxPolicy::WorkspaceWrite { .. } => { - let caps = load_or_create_cap_sids(codex_home); - ensure_dir(&cap_sid_path)?; - fs::write(&cap_sid_path, serde_json::to_string(&caps)?)?; - let psid = convert_string_sid_to_sid(&caps.workspace).unwrap(); - super::token::create_workspace_write_token_with_cap(psid)? - } - SandboxPolicy::DangerFullAccess => { - anyhow::bail!("DangerFullAccess is not supported for sandboxing") - } + // Build capability SID for ACL grants. + let psid_to_use = match &policy { + SandboxPolicy::ReadOnly => { + let caps = load_or_create_cap_sids(codex_home); + ensure_dir(&cap_sid_path)?; + fs::write(&cap_sid_path, serde_json::to_string(&caps)?)?; + unsafe { convert_string_sid_to_sid(&caps.readonly).unwrap() } + } + SandboxPolicy::WorkspaceWrite { .. } => { + let caps = load_or_create_cap_sids(codex_home); + ensure_dir(&cap_sid_path)?; + fs::write(&cap_sid_path, serde_json::to_string(&caps)?)?; + unsafe { convert_string_sid_to_sid(&caps.workspace).unwrap() } + } + SandboxPolicy::DangerFullAccess => { + anyhow::bail!("DangerFullAccess is not supported for sandboxing") } }; - unsafe { - if is_workspace_write { - if let Ok(base) = super::token::get_current_token_for_restriction() { - if let Ok(bytes) = super::token::get_logon_sid_bytes(base) { - let mut tmp = bytes.clone(); - let psid2 = tmp.as_mut_ptr() as *mut c_void; - allow_null_device(psid2); - } - windows_sys::Win32::Foundation::CloseHandle(base); - } - } - } - let persist_aces = is_workspace_write; let AllowDenyPaths { allow, deny } = compute_allow_paths(&policy, sandbox_policy_cwd, ¤t_dir, &env_map); let mut guards: Vec<(PathBuf, *mut c_void)> = Vec::new(); - unsafe { - for p in &allow { - if let Ok(added) = add_allow_ace(p, psid_to_use) { - if added { - if persist_aces { - if p.is_dir() { - // best-effort seeding omitted intentionally - } - } else { - guards.push((p.clone(), psid_to_use)); - } - } - } - } - for p in &deny { + for p in &deny { + unsafe { if let Ok(added) = add_deny_write_ace(p, psid_to_use) { if added && !persist_aces { guards.push((p.clone(), psid_to_use)); } } } + } + if is_workspace_write { + for p in &allow { + unsafe { + if let Ok(added) = add_allow_ace(p, psid_to_use) { + if added && !persist_aces { + guards.push((p.clone(), psid_to_use)); + } + } + } + } + } + unsafe { allow_null_device(psid_to_use); } - 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); + // Prepare named pipes for runner. + let stdin_name = pipe_name("stdin"); + let stdout_name = pipe_name("stdout"); + let stderr_name = pipe_name("stderr"); + log_note( + &format!( + "preparing pipes stdin={} stdout={} stderr={}", + stdin_name, stdout_name, stderr_name + ), + logs_base_dir, + ); + let h_stdin_pipe = create_named_pipe( + &stdin_name, + PIPE_ACCESS_DUPLEX | PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT, + )?; + let h_stdout_pipe = create_named_pipe( + &stdout_name, + PIPE_ACCESS_DUPLEX | PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT, + )?; + let h_stderr_pipe = create_named_pipe( + &stderr_name, + PIPE_ACCESS_DUPLEX | PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT, + )?; + + // Build runner payload. + let payload = RunnerPayload { + policy_json_or_preset: policy_json_or_preset.to_string(), + sandbox_policy_cwd: sandbox_policy_cwd.to_path_buf(), + codex_home: codex_home.to_path_buf(), + command: command.clone(), + cwd: cwd.to_path_buf(), + env_map: env_map.clone(), + timeout_ms, + stdin_pipe: stdin_name.clone(), + stdout_pipe: stdout_name.clone(), + stderr_pipe: stderr_name.clone(), + }; + let payload_json = serde_json::to_string(&payload)?; + + // Launch runner as sandbox user via CreateProcessWithLogonW. + let runner_exe = find_runner_exe(); + let runner_cmdline = runner_exe + .to_str() + .map(|s| s.to_string()) + .unwrap_or_else(|| "codex-command-runner.exe".to_string()); + log_note( + &format!( + "launching runner exe={} as user={} cwd={}", + runner_cmdline, + sandbox_creds.username, + cwd.display() + ), + logs_base_dir, + ); + let cmdline_str = quote_windows_arg(&runner_cmdline); + let mut cmdline: Vec = to_wide(&cmdline_str); + + fn build_sandbox_env_block( + username: &str, + password: &str, + logs_base_dir: Option<&Path>, + ) -> Option> { + unsafe { + let user_w = to_wide(username); + let domain_w = to_wide("."); + let password_w = to_wide(password); + let mut h_tok: HANDLE = 0; + let ok = LogonUserW( + user_w.as_ptr(), + domain_w.as_ptr(), + password_w.as_ptr(), + LOGON32_LOGON_INTERACTIVE, + LOGON32_PROVIDER_DEFAULT, + &mut h_tok, + ); + if ok == 0 || h_tok == 0 { + log_note( + &format!( + "build_sandbox_env_block: LogonUserW failed for {} err={}", + username, + GetLastError() + ), + logs_base_dir, + ); + return None; + } + + let mut profile: PROFILEINFOA = std::mem::zeroed(); + profile.dwSize = std::mem::size_of::() as u32; + profile.lpUserName = user_w.as_ptr() as *mut _; + let profile_loaded = LoadUserProfileA(h_tok, &mut profile as *mut _); + if profile_loaded == 0 { + log_note( + &format!( + "build_sandbox_env_block: LoadUserProfile failed err={}", + GetLastError() + ), + logs_base_dir, + ); + } + + let mut env_block_ptr: *mut std::ffi::c_void = std::ptr::null_mut(); + let env_ok = CreateEnvironmentBlock(&mut env_block_ptr, h_tok, 0); + if env_ok == 0 || env_block_ptr.is_null() { + log_note( + &format!( + "build_sandbox_env_block: CreateEnvironmentBlock failed err={}", + GetLastError() + ), + logs_base_dir, + ); + if profile_loaded != 0 { + let _ = UnloadUserProfile(h_tok, profile.hProfile); + } + CloseHandle(h_tok); + return None; + } + + // Convert env block to map for patch/logging. + let mut map = HashMap::new(); + let mut ptr_u16 = env_block_ptr as *const u16; + loop { + // find len to null + let mut len = 0; + while *ptr_u16.add(len) != 0 { + len += 1; + } + if len == 0 { + break; + } + let slice = std::slice::from_raw_parts(ptr_u16, len); + if let Ok(s) = String::from_utf16(slice) { + if let Some((k, v)) = s.split_once('=') { + map.insert(k.to_string(), v.to_string()); + } + } + ptr_u16 = ptr_u16.add(len + 1); + } + + // Patch critical vars to the sandbox profile. + let profile_dir = format!(r"C:\Users\{}", username); + map.insert("USERPROFILE".to_string(), profile_dir.clone()); + map.insert("HOMEDRIVE".to_string(), "C:".to_string()); + map.insert("HOMEPATH".to_string(), format!(r"\Users\{}", username)); + map.entry("SystemRoot".to_string()) + .or_insert_with(|| "C:\\Windows".to_string()); + map.entry("WINDIR".to_string()) + .or_insert_with(|| "C:\\Windows".to_string()); + let local_app = format!(r"{}\AppData\Local", profile_dir); + let appdata = format!(r"{}\AppData\Roaming", profile_dir); + map.insert("LOCALAPPDATA".to_string(), local_app.clone()); + map.insert("APPDATA".to_string(), appdata); + let temp = format!(r"{}\Temp", local_app); + map.insert("TEMP".to_string(), temp.clone()); + map.insert("TMP".to_string(), temp); + + // Log env + let mut vars: Vec = + map.iter().map(|(k, v)| format!("{}={}", k, v)).collect(); + vars.sort(); + log_note( + &format!( + "build_sandbox_env_block for {}:\n{}", + username, + vars.join("\n") + ), + logs_base_dir, + ); + + // Rebuild env block + let env_block = make_env_block(&map); + + DestroyEnvironmentBlock(env_block_ptr); + if profile_loaded != 0 { + let _ = UnloadUserProfile(h_tok, profile.hProfile); + } + CloseHandle(h_tok); + + Some(env_block) + } + } + + let env_block = build_sandbox_env_block( + &sandbox_creds.username, + &sandbox_creds.password, + logs_base_dir, + ); + let env_log = if env_block.is_some() { + "runner env_block: custom sandbox profile env" + } else { + "runner env_block: inherit (sandbox user profile defaults)" + }; + log_note(env_log, logs_base_dir); + let desktop = to_wide("Winsta0\\Default"); let mut si: STARTUPINFOW = unsafe { std::mem::zeroed() }; si.cb = std::mem::size_of::() as u32; - si.dwFlags |= STARTF_USESTDHANDLES; - si.hStdInput = in_r; - si.hStdOutput = out_w; - si.hStdError = err_w; - - let mut pi: PROCESS_INFORMATION = unsafe { std::mem::zeroed() }; - let cmdline_str = command - .iter() - .map(|a| quote_windows_arg(a)) - .collect::>() - .join(" "); - let mut cmdline: Vec = to_wide(&cmdline_str); - let env_block = make_env_block(&env_map); - let desktop = to_wide("Winsta0\\Default"); si.lpDesktop = desktop.as_ptr() as *mut u16; + let mut pi: PROCESS_INFORMATION = unsafe { std::mem::zeroed() }; + let user_w = to_wide(&sandbox_creds.username); + let domain_w = to_wide("."); + let password_w = to_wide(&sandbox_creds.password); let spawn_res = unsafe { - CreateProcessAsUserW( - h_token, + CreateProcessWithLogonW( + user_w.as_ptr(), + domain_w.as_ptr(), + password_w.as_ptr(), + LOGON_WITH_PROFILE, ptr::null(), cmdline.as_mut_ptr(), - ptr::null_mut(), - ptr::null_mut(), - 1, CREATE_UNICODE_ENVIRONMENT, - env_block.as_ptr() as *mut c_void, + env_block + .as_ref() + .map(|b| b.as_ptr() as *const c_void) + .unwrap_or(ptr::null()), to_wide(cwd).as_ptr(), &si, &mut pi, @@ -302,35 +594,33 @@ mod windows_impl { if spawn_res == 0 { let err = unsafe { GetLastError() } as i32; let dbg = format!( - "CreateProcessAsUserW failed: {} ({}) | cwd={} | cmd={} | env_u16_len={} | si_flags={}", + "CreateProcessWithLogonW failed: {} ({}) | cwd={} | cmd={} | env=inherit | si_flags={}", err, format_last_error(err), cwd.display(), cmdline_str, - env_block.len(), si.dwFlags, ); debug_log(&dbg, logs_base_dir); - unsafe { - CloseHandle(in_r); - CloseHandle(in_w); - CloseHandle(out_r); - CloseHandle(out_w); - CloseHandle(err_r); - CloseHandle(err_w); - CloseHandle(h_token); - } - return Err(anyhow::anyhow!("CreateProcessAsUserW failed: {}", err)); + log_note(&dbg, logs_base_dir); + return Err(anyhow::anyhow!("CreateProcessWithLogonW failed: {}", err)); } + log_note("runner process launched", logs_base_dir); + // Connect pipes and send payload. + connect_pipe(h_stdin_pipe)?; + connect_pipe(h_stdout_pipe)?; + connect_pipe(h_stderr_pipe)?; + { + use std::io::Write; + let mut writer = unsafe { std::fs::File::from_raw_handle(h_stdin_pipe as _) }; + writer.write_all(payload_json.as_bytes())?; + } unsafe { - CloseHandle(in_r); - // Close the parent's stdin write end so the child sees EOF immediately. - CloseHandle(in_w); - CloseHandle(out_w); - CloseHandle(err_w); + CloseHandle(h_stdin_pipe); } + // Read stdout/stderr. let (tx_out, rx_out) = std::sync::mpsc::channel::>(); let (tx_err, rx_err) = std::sync::mpsc::channel::>(); let t_out = std::thread::spawn(move || { @@ -340,7 +630,7 @@ mod windows_impl { let mut read_bytes: u32 = 0; let ok = unsafe { windows_sys::Win32::Storage::FileSystem::ReadFile( - out_r, + h_stdout_pipe, tmp.as_mut_ptr(), tmp.len() as u32, &mut read_bytes, @@ -361,7 +651,7 @@ mod windows_impl { let mut read_bytes: u32 = 0; let ok = unsafe { windows_sys::Win32::Storage::FileSystem::ReadFile( - err_r, + h_stderr_pipe, tmp.as_mut_ptr(), tmp.len() as u32, &mut read_bytes, @@ -389,6 +679,13 @@ mod windows_impl { windows_sys::Win32::System::Threading::TerminateProcess(pi.hProcess, 1); } } + log_note( + &format!( + "runner exited timed_out={} code={}", + timed_out, exit_code_u32 + ), + logs_base_dir, + ); unsafe { if pi.hThread != 0 { @@ -397,7 +694,8 @@ mod windows_impl { if pi.hProcess != 0 { CloseHandle(pi.hProcess); } - CloseHandle(h_token); + CloseHandle(h_stdout_pipe); + CloseHandle(h_stderr_pipe); } let _ = t_out.join(); let _ = t_err.join(); @@ -499,4 +797,14 @@ mod stub { ) -> Result<()> { bail!("Windows sandbox is only available on Windows") } + + pub fn run_elevated_setup( + _policy: &SandboxPolicy, + _policy_cwd: &Path, + _command_cwd: &Path, + _env_map: &HashMap, + _codex_home: &Path, + ) -> Result<()> { + bail!("Windows sandbox is only available on Windows") + } } diff --git a/codex-rs/windows-sandbox-rs/src/process.rs b/codex-rs/windows-sandbox-rs/src/process.rs index 095dcf8b98..ba2313ac10 100644 --- a/codex-rs/windows-sandbox-rs/src/process.rs +++ b/codex-rs/windows-sandbox-rs/src/process.rs @@ -21,11 +21,12 @@ use windows_sys::Win32::System::JobObjects::JobObjectExtendedLimitInformation; use windows_sys::Win32::System::JobObjects::SetInformationJobObject; use windows_sys::Win32::System::JobObjects::JOBOBJECT_EXTENDED_LIMIT_INFORMATION; use windows_sys::Win32::System::JobObjects::JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE; -use windows_sys::Win32::System::Threading::CreateProcessAsUserW; use windows_sys::Win32::System::Threading::GetExitCodeProcess; +use windows_sys::Win32::System::Threading::CreateProcessWithTokenW; use windows_sys::Win32::System::Threading::WaitForSingleObject; use windows_sys::Win32::System::Threading::CREATE_UNICODE_ENVIRONMENT; 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::STARTF_USESTDHANDLES; use windows_sys::Win32::System::Threading::STARTUPINFOW; @@ -79,6 +80,7 @@ fn quote_arg(a: &str) -> String { out.push('"'); out } +#[allow(dead_code)] unsafe fn ensure_inheritable_stdio(si: &mut STARTUPINFOW) -> Result<()> { for kind in [STD_INPUT_HANDLE, STD_OUTPUT_HANDLE, STD_ERROR_HANDLE] { let h = GetStdHandle(kind); @@ -96,12 +98,16 @@ unsafe fn ensure_inheritable_stdio(si: &mut STARTUPINFOW) -> Result<()> { Ok(()) } +/// # Safety +/// Caller must provide a valid primary token handle (`h_token`) with appropriate access, +/// and the `argv`, `cwd`, and `env_map` must remain valid for the duration of the call. pub unsafe fn create_process_as_user( h_token: HANDLE, argv: &[String], cwd: &Path, env_map: &HashMap, logs_base_dir: Option<&Path>, + stdio: Option<(HANDLE, HANDLE, HANDLE)>, ) -> Result<(PROCESS_INFORMATION, STARTUPINFOW)> { let cmdline_str = argv .iter() @@ -117,17 +123,22 @@ pub unsafe fn create_process_as_user( // Point explicitly at the interactive desktop. let desktop = to_wide("Winsta0\\Default"); si.lpDesktop = desktop.as_ptr() as *mut u16; - ensure_inheritable_stdio(&mut si)?; + if let Some((stdin_h, stdout_h, stderr_h)) = stdio { + si.dwFlags |= STARTF_USESTDHANDLES; + si.hStdInput = stdin_h; + si.hStdOutput = stdout_h; + si.hStdError = stderr_h; + } else { + ensure_inheritable_stdio(&mut si)?; + } let mut pi: PROCESS_INFORMATION = std::mem::zeroed(); - let ok = CreateProcessAsUserW( + let ok = CreateProcessWithTokenW( h_token, + LOGON_WITH_PROFILE, std::ptr::null(), cmdline.as_mut_ptr(), - std::ptr::null_mut(), - std::ptr::null_mut(), - 1, CREATE_UNICODE_ENVIRONMENT, - env_block.as_ptr() as *mut c_void, + env_block.as_ptr() as *const c_void, to_wide(cwd).as_ptr(), &si, &mut pi, @@ -135,7 +146,7 @@ pub unsafe fn create_process_as_user( if ok == 0 { let err = GetLastError() as i32; let msg = format!( - "CreateProcessAsUserW failed: {} ({}) | cwd={} | cmd={} | env_u16_len={} | si_flags={}", + "CreateProcessWithTokenW failed: {} ({}) | cwd={} | cmd={} | env_u16_len={} | si_flags={}", err, format_last_error(err), cwd.display(), @@ -144,11 +155,14 @@ pub unsafe fn create_process_as_user( si.dwFlags, ); logging::debug_log(&msg, logs_base_dir); - return Err(anyhow!("CreateProcessAsUserW failed: {}", err)); + return Err(anyhow!("CreateProcessWithTokenW failed: {}", err)); } Ok((pi, si)) } +/// # Safety +/// Caller must provide valid process information handles. +#[allow(dead_code)] pub unsafe fn wait_process_and_exitcode(pi: &PROCESS_INFORMATION) -> Result { let res = WaitForSingleObject(pi.hProcess, INFINITE); if res != 0 { @@ -161,6 +175,9 @@ pub unsafe fn wait_process_and_exitcode(pi: &PROCESS_INFORMATION) -> Result Ok(code as i32) } +/// # Safety +/// Caller must close the returned job handle. +#[allow(dead_code)] pub unsafe fn create_job_kill_on_close() -> Result { let h = CreateJobObjectW(std::ptr::null_mut(), std::ptr::null()); if h == 0 { @@ -183,6 +200,9 @@ pub unsafe fn create_job_kill_on_close() -> Result { Ok(h) } +/// # Safety +/// Caller must pass valid handles for a job object and a process. +#[allow(dead_code)] pub unsafe fn assign_to_job(h_job: HANDLE, h_process: HANDLE) -> Result<()> { if AssignProcessToJobObject(h_job, h_process) == 0 { return Err(anyhow!( diff --git a/codex-rs/windows-sandbox-rs/src/setup.rs b/codex-rs/windows-sandbox-rs/src/setup.rs new file mode 100644 index 0000000000..ce78a6c0d6 --- /dev/null +++ b/codex-rs/windows-sandbox-rs/src/setup.rs @@ -0,0 +1,298 @@ +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) +} diff --git a/codex-rs/windows-sandbox-rs/src/token.rs b/codex-rs/windows-sandbox-rs/src/token.rs index 60eae9377f..7e565bc67a 100644 --- a/codex-rs/windows-sandbox-rs/src/token.rs +++ b/codex-rs/windows-sandbox-rs/src/token.rs @@ -24,6 +24,7 @@ use windows_sys::Win32::Security::TOKEN_DUPLICATE; use windows_sys::Win32::Security::TOKEN_PRIVILEGES; use windows_sys::Win32::Security::TOKEN_QUERY; use windows_sys::Win32::System::Threading::GetCurrentProcess; +use windows_sys::Win32::System::Threading::OpenProcessToken; const DISABLE_MAX_PRIVILEGE: u32 = 0x01; const LUA_TOKEN: u32 = 0x04; @@ -52,6 +53,8 @@ pub unsafe fn world_sid() -> Result> { Ok(buf) } +/// # Safety +/// Caller is responsible for freeing the returned SID with `LocalFree`. pub unsafe fn convert_string_sid_to_sid(s: &str) -> Option<*mut c_void> { #[link(name = "advapi32")] extern "system" { @@ -66,6 +69,9 @@ pub unsafe fn convert_string_sid_to_sid(s: &str) -> Option<*mut c_void> { } } +/// # Safety +/// Caller must close the returned token handle. +#[allow(dead_code)] pub unsafe fn get_current_token_for_restriction() -> Result { let desired = TOKEN_DUPLICATE | TOKEN_QUERY @@ -197,13 +203,55 @@ unsafe fn enable_single_privilege(h_token: HANDLE, name: &str) -> Result<()> { Ok(()) } -// removed unused create_write_restricted_token_strict +/// # Safety +/// Opens the current process token and adjusts privileges; caller should ensure this is needed in the current context. +#[allow(dead_code)] +pub unsafe fn enable_privilege_on_current(name: &str) -> Result<()> { + let mut h: HANDLE = 0; + let ok = OpenProcessToken( + GetCurrentProcess(), + TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, + &mut h, + ); + if ok == 0 { + return Err(anyhow!("OpenProcessToken failed: {}", GetLastError())); + } + let res = enable_single_privilege(h, name); + CloseHandle(h); + res +} +/// # Safety +/// Caller must close the returned token handle. +#[allow(dead_code)] pub unsafe fn create_workspace_write_token_with_cap( psid_capability: *mut c_void, ) -> Result<(HANDLE, *mut c_void)> { let base = get_current_token_for_restriction()?; - let mut logon_sid_bytes = get_logon_sid_bytes(base)?; + let res = create_workspace_write_token_with_cap_from(base, psid_capability); + CloseHandle(base); + res +} + +/// # Safety +/// Caller must close the returned token handle. +#[allow(dead_code)] +pub unsafe fn create_readonly_token_with_cap( + psid_capability: *mut c_void, +) -> Result<(HANDLE, *mut c_void)> { + let base = get_current_token_for_restriction()?; + let res = create_readonly_token_with_cap_from(base, psid_capability); + CloseHandle(base); + res +} + +/// # Safety +/// Caller must close the returned token handle; base_token must be a valid primary token. +pub unsafe fn create_workspace_write_token_with_cap_from( + base_token: HANDLE, + psid_capability: *mut c_void, +) -> Result<(HANDLE, *mut c_void)> { + let mut logon_sid_bytes = get_logon_sid_bytes(base_token)?; let psid_logon = logon_sid_bytes.as_mut_ptr() as *mut c_void; let mut everyone = world_sid()?; let psid_everyone = everyone.as_mut_ptr() as *mut c_void; @@ -218,7 +266,7 @@ pub unsafe fn create_workspace_write_token_with_cap( let mut new_token: HANDLE = 0; let flags = DISABLE_MAX_PRIVILEGE | LUA_TOKEN | WRITE_RESTRICTED; let ok = CreateRestrictedToken( - base, + base_token, flags, 0, std::ptr::null(), @@ -235,11 +283,13 @@ pub unsafe fn create_workspace_write_token_with_cap( Ok((new_token, psid_capability)) } -pub unsafe fn create_readonly_token_with_cap( +/// # Safety +/// Caller must close the returned token handle; base_token must be a valid primary token. +pub unsafe fn create_readonly_token_with_cap_from( + base_token: HANDLE, psid_capability: *mut c_void, ) -> Result<(HANDLE, *mut c_void)> { - let base = get_current_token_for_restriction()?; - let mut logon_sid_bytes = get_logon_sid_bytes(base)?; + let mut logon_sid_bytes = get_logon_sid_bytes(base_token)?; let psid_logon = logon_sid_bytes.as_mut_ptr() as *mut c_void; let mut everyone = world_sid()?; let psid_everyone = everyone.as_mut_ptr() as *mut c_void; @@ -254,7 +304,7 @@ pub unsafe fn create_readonly_token_with_cap( let mut new_token: HANDLE = 0; let flags = DISABLE_MAX_PRIVILEGE | LUA_TOKEN | WRITE_RESTRICTED; let ok = CreateRestrictedToken( - base, + base_token, flags, 0, std::ptr::null(), diff --git a/codex-rs/windows-sandbox-rs/src/winutil.rs b/codex-rs/windows-sandbox-rs/src/winutil.rs index 5e74ce072e..b0f0f336a8 100644 --- a/codex-rs/windows-sandbox-rs/src/winutil.rs +++ b/codex-rs/windows-sandbox-rs/src/winutil.rs @@ -6,6 +6,7 @@ use windows_sys::Win32::System::Diagnostics::Debug::FormatMessageW; use windows_sys::Win32::System::Diagnostics::Debug::FORMAT_MESSAGE_ALLOCATE_BUFFER; use windows_sys::Win32::System::Diagnostics::Debug::FORMAT_MESSAGE_FROM_SYSTEM; use windows_sys::Win32::System::Diagnostics::Debug::FORMAT_MESSAGE_IGNORE_INSERTS; +use windows_sys::Win32::Security::Authorization::ConvertSidToStringSidW; pub fn to_wide>(s: S) -> Vec { let mut v: Vec = s.as_ref().encode_wide().collect(); @@ -41,3 +42,21 @@ pub fn format_last_error(err: i32) -> String { s } } + +pub fn string_from_sid_bytes(sid: &[u8]) -> Result { + unsafe { + let mut str_ptr: *mut u16 = std::ptr::null_mut(); + let ok = ConvertSidToStringSidW(sid.as_ptr() as *mut std::ffi::c_void, &mut str_ptr); + if ok == 0 || str_ptr.is_null() { + return Err(format!("ConvertSidToStringSidW failed: {}", std::io::Error::last_os_error())); + } + let mut len = 0; + while *str_ptr.add(len) != 0 { + len += 1; + } + let slice = std::slice::from_raw_parts(str_ptr, len); + let out = String::from_utf16_lossy(slice); + let _ = LocalFree(str_ptr as HLOCAL); + Ok(out) + } +}