From 3e81ed4b918680519c620cc103c865b7be41bbfe Mon Sep 17 00:00:00 2001 From: iceweasel-oai Date: Thu, 11 Dec 2025 13:51:27 -0800 Subject: [PATCH] Elevated Sandbox 3 (#7809) dedicated sandbox command runner exe. --- codex-rs/windows-sandbox-rs/Cargo.toml | 4 + .../src/bin/command_runner.rs | 12 + .../src/command_runner_win.rs | 225 ++++++++++++++++++ codex-rs/windows-sandbox-rs/src/lib.rs | 16 ++ 4 files changed, 257 insertions(+) create mode 100644 codex-rs/windows-sandbox-rs/src/bin/command_runner.rs create mode 100644 codex-rs/windows-sandbox-rs/src/command_runner_win.rs diff --git a/codex-rs/windows-sandbox-rs/Cargo.toml b/codex-rs/windows-sandbox-rs/Cargo.toml index ac290101c..4e7542160 100644 --- a/codex-rs/windows-sandbox-rs/Cargo.toml +++ b/codex-rs/windows-sandbox-rs/Cargo.toml @@ -13,6 +13,10 @@ path = "src/lib.rs" name = "codex-windows-sandbox-setup" path = "src/bin/setup_main.rs" +[[bin]] +name = "codex-command-runner" +path = "src/bin/command_runner.rs" + [dependencies] anyhow = "1.0" base64 = { workspace = true } 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 000000000..e84a15354 --- /dev/null +++ b/codex-rs/windows-sandbox-rs/src/bin/command_runner.rs @@ -0,0 +1,12 @@ +#[path = "../command_runner_win.rs"] +mod win; + +#[cfg(target_os = "windows")] +fn main() -> anyhow::Result<()> { + win::main() +} + +#[cfg(not(target_os = "windows"))] +fn main() { + panic!("codex-command-runner is Windows-only"); +} diff --git a/codex-rs/windows-sandbox-rs/src/command_runner_win.rs b/codex-rs/windows-sandbox-rs/src/command_runner_win.rs new file mode 100644 index 000000000..806a8777d --- /dev/null +++ b/codex-rs/windows-sandbox-rs/src/command_runner_win.rs @@ -0,0 +1,225 @@ +#![cfg(target_os = "windows")] + +use anyhow::Context; +use anyhow::Result; +use codex_windows_sandbox::allow_null_device; +use codex_windows_sandbox::convert_string_sid_to_sid; +use codex_windows_sandbox::create_process_as_user; +use codex_windows_sandbox::create_readonly_token_with_cap_from; +use codex_windows_sandbox::create_workspace_write_token_with_cap_from; +use codex_windows_sandbox::get_current_token_for_restriction; +use codex_windows_sandbox::log_note; +use codex_windows_sandbox::parse_policy; +use codex_windows_sandbox::to_wide; +use codex_windows_sandbox::SandboxPolicy; +use serde::Deserialize; +use std::collections::HashMap; +use std::ffi::c_void; +use std::path::PathBuf; +use windows_sys::Win32::Foundation::CloseHandle; +use windows_sys::Win32::Foundation::GetLastError; +use windows_sys::Win32::Foundation::HANDLE; +use windows_sys::Win32::Storage::FileSystem::CreateFileW; +use windows_sys::Win32::Storage::FileSystem::FILE_GENERIC_READ; +use windows_sys::Win32::Storage::FileSystem::FILE_GENERIC_WRITE; +use windows_sys::Win32::Storage::FileSystem::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::TerminateProcess; +use windows_sys::Win32::System::Threading::WaitForSingleObject; +use windows_sys::Win32::System::Threading::INFINITE; + +#[derive(Debug, Deserialize)] +struct RunnerRequest { + policy_json_or_preset: String, + // Writable location for logs (sandbox user's .codex). + codex_home: PathBuf, + // Real user's CODEX_HOME for shared data (caps, config). + real_codex_home: PathBuf, + cap_sid: String, + command: Vec, + cwd: PathBuf, + env_map: HashMap, + timeout_ms: Option, + stdin_pipe: String, + stdout_pipe: String, + stderr_pipe: String, +} + +const WAIT_TIMEOUT: u32 = 0x0000_0102; + +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) +} + +pub fn main() -> Result<()> { + let mut input = String::new(); + let mut args = std::env::args().skip(1); + if let Some(first) = args.next() { + if let Some(rest) = first.strip_prefix("--request-file=") { + let req_path = PathBuf::from(rest); + input = std::fs::read_to_string(&req_path).context("read request file")?; + } + } + if input.is_empty() { + anyhow::bail!("runner: no request-file provided"); + } + let req: RunnerRequest = serde_json::from_str(&input).context("parse runner request json")?; + let log_dir = Some(req.codex_home.as_path()); + log_note( + &format!( + "runner start cwd={} cmd={:?} real_codex_home={}", + req.cwd.display(), + req.command, + req.real_codex_home.display() + ), + Some(&req.codex_home), + ); + + let policy = parse_policy(&req.policy_json_or_preset).context("parse policy_json_or_preset")?; + let psid_cap: *mut c_void = unsafe { convert_string_sid_to_sid(&req.cap_sid).unwrap() }; + + // 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)?; + + // Build command and env, spawn with CreateProcessAsUserW. + let spawn_result = 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)), + ) + }; + let (proc_info, _si) = match spawn_result { + Ok(v) => v, + Err(e) => { + log_note(&format!("runner: spawn failed: {e:?}"), log_dir); + unsafe { + CloseHandle(h_stdin); + CloseHandle(h_stdout); + CloseHandle(h_stderr); + CloseHandle(h_token); + } + return Err(e); + } + }; + + // 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 wait_res = unsafe { + WaitForSingleObject( + proc_info.hProcess, + req.timeout_ms.map(|ms| ms as u32).unwrap_or(INFINITE), + ) + }; + let timed_out = wait_res == WAIT_TIMEOUT; + + let exit_code: i32; + unsafe { + if timed_out { + let _ = TerminateProcess(proc_info.hProcess, 1); + exit_code = 128 + 64; + } else { + let mut raw_exit: u32 = 1; + windows_sys::Win32::System::Threading::GetExitCodeProcess( + proc_info.hProcess, + &mut raw_exit, + ); + exit_code = raw_exit as i32; + } + if proc_info.hThread != 0 { + CloseHandle(proc_info.hThread); + } + if proc_info.hProcess != 0 { + CloseHandle(proc_info.hProcess); + } + CloseHandle(h_stdin); + CloseHandle(h_stdout); + CloseHandle(h_stderr); + CloseHandle(h_token); + if let Some(job) = h_job { + CloseHandle(job); + } + } + if exit_code != 0 { + eprintln!("runner child exited with code {}", exit_code); + } + log_note( + &format!("runner exit pid={} code={}", proc_info.hProcess, exit_code), + log_dir, + ); + std::process::exit(exit_code); +} diff --git a/codex-rs/windows-sandbox-rs/src/lib.rs b/codex-rs/windows-sandbox-rs/src/lib.rs index 56b59e387..c4367e612 100644 --- a/codex-rs/windows-sandbox-rs/src/lib.rs +++ b/codex-rs/windows-sandbox-rs/src/lib.rs @@ -12,6 +12,8 @@ windows_modules!( #[path = "setup_orchestrator.rs"] mod setup; +#[cfg(target_os = "windows")] +pub use acl::allow_null_device; #[cfg(target_os = "windows")] pub use acl::ensure_allow_write_aces; #[cfg(target_os = "windows")] @@ -33,6 +35,12 @@ pub use logging::log_note; #[cfg(target_os = "windows")] pub use logging::LOG_FILE_NAME; #[cfg(target_os = "windows")] +pub use policy::parse_policy; +#[cfg(target_os = "windows")] +pub use 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::run_setup_refresh; @@ -43,11 +51,19 @@ pub use setup::SETUP_VERSION; #[cfg(target_os = "windows")] pub use token::convert_string_sid_to_sid; #[cfg(target_os = "windows")] +pub use token::create_readonly_token_with_cap_from; +#[cfg(target_os = "windows")] +pub use token::create_workspace_write_token_with_cap_from; +#[cfg(target_os = "windows")] +pub use token::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;