|
| 1 | +#![cfg(target_os = "windows")] |
| 2 | + |
| 3 | +use anyhow::Context; |
| 4 | +use anyhow::Result; |
| 5 | +use codex_windows_sandbox::allow_null_device; |
| 6 | +use codex_windows_sandbox::convert_string_sid_to_sid; |
| 7 | +use codex_windows_sandbox::create_process_as_user; |
| 8 | +use codex_windows_sandbox::create_readonly_token_with_cap_from; |
| 9 | +use codex_windows_sandbox::create_workspace_write_token_with_cap_from; |
| 10 | +use codex_windows_sandbox::get_current_token_for_restriction; |
| 11 | +use codex_windows_sandbox::log_note; |
| 12 | +use codex_windows_sandbox::parse_policy; |
| 13 | +use codex_windows_sandbox::to_wide; |
| 14 | +use codex_windows_sandbox::SandboxPolicy; |
| 15 | +use serde::Deserialize; |
| 16 | +use std::collections::HashMap; |
| 17 | +use std::ffi::c_void; |
| 18 | +use std::path::PathBuf; |
| 19 | +use windows_sys::Win32::Foundation::CloseHandle; |
| 20 | +use windows_sys::Win32::Foundation::GetLastError; |
| 21 | +use windows_sys::Win32::Foundation::HANDLE; |
| 22 | +use windows_sys::Win32::Storage::FileSystem::CreateFileW; |
| 23 | +use windows_sys::Win32::Storage::FileSystem::FILE_GENERIC_READ; |
| 24 | +use windows_sys::Win32::Storage::FileSystem::FILE_GENERIC_WRITE; |
| 25 | +use windows_sys::Win32::Storage::FileSystem::OPEN_EXISTING; |
| 26 | +use windows_sys::Win32::System::JobObjects::AssignProcessToJobObject; |
| 27 | +use windows_sys::Win32::System::JobObjects::CreateJobObjectW; |
| 28 | +use windows_sys::Win32::System::JobObjects::JobObjectExtendedLimitInformation; |
| 29 | +use windows_sys::Win32::System::JobObjects::SetInformationJobObject; |
| 30 | +use windows_sys::Win32::System::JobObjects::JOBOBJECT_EXTENDED_LIMIT_INFORMATION; |
| 31 | +use windows_sys::Win32::System::JobObjects::JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE; |
| 32 | +use windows_sys::Win32::System::Threading::TerminateProcess; |
| 33 | +use windows_sys::Win32::System::Threading::WaitForSingleObject; |
| 34 | +use windows_sys::Win32::System::Threading::INFINITE; |
| 35 | + |
| 36 | +#[derive(Debug, Deserialize)] |
| 37 | +struct RunnerRequest { |
| 38 | + policy_json_or_preset: String, |
| 39 | + // Writable location for logs (sandbox user's .codex). |
| 40 | + codex_home: PathBuf, |
| 41 | + // Real user's CODEX_HOME for shared data (caps, config). |
| 42 | + real_codex_home: PathBuf, |
| 43 | + cap_sid: String, |
| 44 | + command: Vec<String>, |
| 45 | + cwd: PathBuf, |
| 46 | + env_map: HashMap<String, String>, |
| 47 | + timeout_ms: Option<u64>, |
| 48 | + stdin_pipe: String, |
| 49 | + stdout_pipe: String, |
| 50 | + stderr_pipe: String, |
| 51 | +} |
| 52 | + |
| 53 | +const WAIT_TIMEOUT: u32 = 0x0000_0102; |
| 54 | + |
| 55 | +unsafe fn create_job_kill_on_close() -> Result<HANDLE> { |
| 56 | + let h = CreateJobObjectW(std::ptr::null_mut(), std::ptr::null()); |
| 57 | + if h == 0 { |
| 58 | + return Err(anyhow::anyhow!("CreateJobObjectW failed")); |
| 59 | + } |
| 60 | + let mut limits: JOBOBJECT_EXTENDED_LIMIT_INFORMATION = std::mem::zeroed(); |
| 61 | + limits.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE; |
| 62 | + let ok = SetInformationJobObject( |
| 63 | + h, |
| 64 | + JobObjectExtendedLimitInformation, |
| 65 | + &mut limits as *mut _ as *mut _, |
| 66 | + std::mem::size_of::<JOBOBJECT_EXTENDED_LIMIT_INFORMATION>() as u32, |
| 67 | + ); |
| 68 | + if ok == 0 { |
| 69 | + return Err(anyhow::anyhow!("SetInformationJobObject failed")); |
| 70 | + } |
| 71 | + Ok(h) |
| 72 | +} |
| 73 | + |
| 74 | +pub fn main() -> Result<()> { |
| 75 | + let mut input = String::new(); |
| 76 | + let mut args = std::env::args().skip(1); |
| 77 | + if let Some(first) = args.next() { |
| 78 | + if let Some(rest) = first.strip_prefix("--request-file=") { |
| 79 | + let req_path = PathBuf::from(rest); |
| 80 | + input = std::fs::read_to_string(&req_path).context("read request file")?; |
| 81 | + } |
| 82 | + } |
| 83 | + if input.is_empty() { |
| 84 | + anyhow::bail!("runner: no request-file provided"); |
| 85 | + } |
| 86 | + let req: RunnerRequest = serde_json::from_str(&input).context("parse runner request json")?; |
| 87 | + let log_dir = Some(req.codex_home.as_path()); |
| 88 | + log_note( |
| 89 | + &format!( |
| 90 | + "runner start cwd={} cmd={:?} real_codex_home={}", |
| 91 | + req.cwd.display(), |
| 92 | + req.command, |
| 93 | + req.real_codex_home.display() |
| 94 | + ), |
| 95 | + Some(&req.codex_home), |
| 96 | + ); |
| 97 | + |
| 98 | + let policy = parse_policy(&req.policy_json_or_preset).context("parse policy_json_or_preset")?; |
| 99 | + let psid_cap: *mut c_void = unsafe { convert_string_sid_to_sid(&req.cap_sid).unwrap() }; |
| 100 | + |
| 101 | + // Create restricted token from current process token. |
| 102 | + let base = unsafe { get_current_token_for_restriction()? }; |
| 103 | + let token_res: Result<(HANDLE, *mut c_void)> = unsafe { |
| 104 | + match &policy { |
| 105 | + SandboxPolicy::ReadOnly => create_readonly_token_with_cap_from(base, psid_cap), |
| 106 | + SandboxPolicy::WorkspaceWrite { .. } => { |
| 107 | + create_workspace_write_token_with_cap_from(base, psid_cap) |
| 108 | + } |
| 109 | + SandboxPolicy::DangerFullAccess => unreachable!(), |
| 110 | + } |
| 111 | + }; |
| 112 | + let (h_token, psid_to_use) = token_res?; |
| 113 | + unsafe { |
| 114 | + CloseHandle(base); |
| 115 | + } |
| 116 | + unsafe { |
| 117 | + allow_null_device(psid_to_use); |
| 118 | + } |
| 119 | + |
| 120 | + // Open named pipes for stdio. |
| 121 | + let open_pipe = |name: &str, access: u32| -> Result<HANDLE> { |
| 122 | + let path = to_wide(name); |
| 123 | + let handle = unsafe { |
| 124 | + CreateFileW( |
| 125 | + path.as_ptr(), |
| 126 | + access, |
| 127 | + 0, |
| 128 | + std::ptr::null_mut(), |
| 129 | + OPEN_EXISTING, |
| 130 | + 0, |
| 131 | + 0, |
| 132 | + ) |
| 133 | + }; |
| 134 | + if handle == windows_sys::Win32::Foundation::INVALID_HANDLE_VALUE { |
| 135 | + let err = unsafe { GetLastError() }; |
| 136 | + log_note( |
| 137 | + &format!("CreateFileW failed for pipe {name}: {err}"), |
| 138 | + Some(&req.codex_home), |
| 139 | + ); |
| 140 | + return Err(anyhow::anyhow!("CreateFileW failed for pipe {name}: {err}")); |
| 141 | + } |
| 142 | + Ok(handle) |
| 143 | + }; |
| 144 | + let h_stdin = open_pipe(&req.stdin_pipe, FILE_GENERIC_READ)?; |
| 145 | + let h_stdout = open_pipe(&req.stdout_pipe, FILE_GENERIC_WRITE)?; |
| 146 | + let h_stderr = open_pipe(&req.stderr_pipe, FILE_GENERIC_WRITE)?; |
| 147 | + |
| 148 | + // Build command and env, spawn with CreateProcessAsUserW. |
| 149 | + let spawn_result = unsafe { |
| 150 | + create_process_as_user( |
| 151 | + h_token, |
| 152 | + &req.command, |
| 153 | + &req.cwd, |
| 154 | + &req.env_map, |
| 155 | + Some(&req.codex_home), |
| 156 | + Some((h_stdin, h_stdout, h_stderr)), |
| 157 | + ) |
| 158 | + }; |
| 159 | + let (proc_info, _si) = match spawn_result { |
| 160 | + Ok(v) => v, |
| 161 | + Err(e) => { |
| 162 | + log_note(&format!("runner: spawn failed: {e:?}"), log_dir); |
| 163 | + unsafe { |
| 164 | + CloseHandle(h_stdin); |
| 165 | + CloseHandle(h_stdout); |
| 166 | + CloseHandle(h_stderr); |
| 167 | + CloseHandle(h_token); |
| 168 | + } |
| 169 | + return Err(e); |
| 170 | + } |
| 171 | + }; |
| 172 | + |
| 173 | + // Optional job kill on close. |
| 174 | + let h_job = unsafe { create_job_kill_on_close().ok() }; |
| 175 | + if let Some(job) = h_job { |
| 176 | + unsafe { |
| 177 | + let _ = AssignProcessToJobObject(job, proc_info.hProcess); |
| 178 | + } |
| 179 | + } |
| 180 | + |
| 181 | + // Wait for process. |
| 182 | + let wait_res = unsafe { |
| 183 | + WaitForSingleObject( |
| 184 | + proc_info.hProcess, |
| 185 | + req.timeout_ms.map(|ms| ms as u32).unwrap_or(INFINITE), |
| 186 | + ) |
| 187 | + }; |
| 188 | + let timed_out = wait_res == WAIT_TIMEOUT; |
| 189 | + |
| 190 | + let exit_code: i32; |
| 191 | + unsafe { |
| 192 | + if timed_out { |
| 193 | + let _ = TerminateProcess(proc_info.hProcess, 1); |
| 194 | + exit_code = 128 + 64; |
| 195 | + } else { |
| 196 | + let mut raw_exit: u32 = 1; |
| 197 | + windows_sys::Win32::System::Threading::GetExitCodeProcess( |
| 198 | + proc_info.hProcess, |
| 199 | + &mut raw_exit, |
| 200 | + ); |
| 201 | + exit_code = raw_exit as i32; |
| 202 | + } |
| 203 | + if proc_info.hThread != 0 { |
| 204 | + CloseHandle(proc_info.hThread); |
| 205 | + } |
| 206 | + if proc_info.hProcess != 0 { |
| 207 | + CloseHandle(proc_info.hProcess); |
| 208 | + } |
| 209 | + CloseHandle(h_stdin); |
| 210 | + CloseHandle(h_stdout); |
| 211 | + CloseHandle(h_stderr); |
| 212 | + CloseHandle(h_token); |
| 213 | + if let Some(job) = h_job { |
| 214 | + CloseHandle(job); |
| 215 | + } |
| 216 | + } |
| 217 | + if exit_code != 0 { |
| 218 | + eprintln!("runner child exited with code {}", exit_code); |
| 219 | + } |
| 220 | + log_note( |
| 221 | + &format!("runner exit pid={} code={}", proc_info.hProcess, exit_code), |
| 222 | + log_dir, |
| 223 | + ); |
| 224 | + std::process::exit(exit_code); |
| 225 | +} |
0 commit comments