mirror of
https://github.com/openai/codex.git
synced 2026-05-01 01:47:18 +00:00
use framed IPC for elevated command runner (#14846)
## Summary This is PR 2 of the Windows sandbox runner split. PR 1 introduced the framed IPC runner foundation and related Windows sandbox infrastructure without changing the active elevated one-shot execution path. This PR switches that elevated one-shot path over to the new runner IPC transport and removes the old request-file bootstrap that PR 1 intentionally left in place. After this change, ordinary elevated Windows sandbox commands still behave as one-shot executions, but they now run as the simple case of the same helper/IPC transport that later unified_exec work will build on. ## Why this is needed for unified_exec Windows elevated sandboxed execution crosses a user boundary: the CLI launches a helper as the sandbox user and has to manage command execution from outside that security context. For one-shot commands, the old request-file/bootstrap flow was sufficient. For unified_exec, it is not. Unified_exec needs a long-lived bidirectional channel so the parent can: - send a spawn request - receive structured spawn success/failure - stream stdout and stderr incrementally - eventually support stdin writes, termination, and other session lifecycle events This PR does not add long-lived sessions yet. It converts the existing elevated one-shot path to use the same framed IPC transport so that PR 3 can add unified_exec session semantics on top of a transport that is already exercised by normal elevated command execution. ## Scope This PR: - updates `windows-sandbox-rs/src/elevated_impl.rs` to launch the runner with named pipes, send a framed `SpawnRequest`, wait for `SpawnReady`, and collect framed `Output`/`Exit` messages - removes the old `--request-file=...` execution path from `windows-sandbox-rs/src/elevated/command_runner_win.rs` - keeps the public behavior one-shot: no session reuse or interactive unified_exec behavior is introduced here This PR does not: - add Windows unified_exec session support - add background terminal reuse - add PTY session lifecycle management ## Why Windows needs this and Linux/macOS do not On Linux and macOS, the existing sandbox/process model composes much more directly with long-lived process control. The parent can generally spawn and own the child process (or PTY) directly inside the sandbox model we already use. Windows elevated sandboxing is different. The parent is not directly managing the sandboxed process in the same way; it launches across a different user/security context. That means long-lived control requires an explicit helper process plus IPC for spawn, output, exit, and later stdin/session control. So the extra machinery here is not because unified_exec is conceptually different on Windows. It is because the elevated Windows sandbox boundary requires a helper-mediated transport to support it cleanly. ## Validation - `cargo test -p codex-windows-sandbox`
This commit is contained in:
@@ -9,6 +9,13 @@ mod windows_impl {
|
||||
use crate::helper_materialization::resolve_helper_for_launch;
|
||||
use crate::helper_materialization::HelperExecutable;
|
||||
use crate::identity::require_logon_sandbox_creds;
|
||||
use crate::ipc_framed::decode_bytes;
|
||||
use crate::ipc_framed::read_frame;
|
||||
use crate::ipc_framed::write_frame;
|
||||
use crate::ipc_framed::FramedMessage;
|
||||
use crate::ipc_framed::Message;
|
||||
use crate::ipc_framed::OutputStream;
|
||||
use crate::ipc_framed::SpawnRequest;
|
||||
use crate::logging::log_failure;
|
||||
use crate::logging::log_note;
|
||||
use crate::logging::log_start;
|
||||
@@ -26,8 +33,9 @@ mod windows_impl {
|
||||
use rand::SeedableRng;
|
||||
use std::collections::HashMap;
|
||||
use std::ffi::c_void;
|
||||
use std::fs;
|
||||
use std::fs::File;
|
||||
use std::io;
|
||||
use std::os::windows::io::FromRawHandle;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::ptr;
|
||||
@@ -40,15 +48,12 @@ mod windows_impl {
|
||||
use windows_sys::Win32::System::Diagnostics::Debug::SetErrorMode;
|
||||
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;
|
||||
const PIPE_ACCESS_INBOUND: u32 = 0x0000_0001;
|
||||
const PIPE_ACCESS_OUTBOUND: u32 = 0x0000_0002;
|
||||
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::INFINITE;
|
||||
use windows_sys::Win32::System::Threading::LOGON_WITH_PROFILE;
|
||||
use windows_sys::Win32::System::Threading::PROCESS_INFORMATION;
|
||||
use windows_sys::Win32::System::Threading::STARTUPINFOW;
|
||||
@@ -183,24 +188,16 @@ mod windows_impl {
|
||||
|
||||
pub use crate::windows_impl::CaptureResult;
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
struct RunnerPayload {
|
||||
policy_json_or_preset: String,
|
||||
sandbox_policy_cwd: PathBuf,
|
||||
// Writable log dir for sandbox user (.codex in sandbox profile).
|
||||
codex_home: PathBuf,
|
||||
// Real user's CODEX_HOME for shared data (caps, config).
|
||||
real_codex_home: PathBuf,
|
||||
cap_sids: Vec<String>,
|
||||
request_file: Option<PathBuf>,
|
||||
command: Vec<String>,
|
||||
cwd: PathBuf,
|
||||
env_map: HashMap<String, String>,
|
||||
timeout_ms: Option<u64>,
|
||||
use_private_desktop: bool,
|
||||
stdin_pipe: String,
|
||||
stdout_pipe: String,
|
||||
stderr_pipe: String,
|
||||
fn read_spawn_ready(pipe_read: &mut File) -> Result<()> {
|
||||
let msg = read_frame(pipe_read)?
|
||||
.ok_or_else(|| anyhow::anyhow!("runner pipe closed before spawn_ready"))?;
|
||||
match msg.message {
|
||||
Message::SpawnReady { .. } => Ok(()),
|
||||
Message::Error { payload } => Err(anyhow::anyhow!("runner error: {}", payload.message)),
|
||||
other => Err(anyhow::anyhow!(
|
||||
"expected spawn_ready from runner, got {other:?}"
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Launches the command runner under the sandbox user and captures its output.
|
||||
@@ -271,25 +268,10 @@ mod windows_impl {
|
||||
allow_null_device(psid_to_use);
|
||||
}
|
||||
|
||||
// Prepare named pipes for runner.
|
||||
let stdin_name = pipe_name("stdin");
|
||||
let stdout_name = pipe_name("stdout");
|
||||
let stderr_name = pipe_name("stderr");
|
||||
let h_stdin_pipe = create_named_pipe(
|
||||
&stdin_name,
|
||||
PIPE_ACCESS_DUPLEX | PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT,
|
||||
&sandbox_sid,
|
||||
)?;
|
||||
let h_stdout_pipe = create_named_pipe(
|
||||
&stdout_name,
|
||||
PIPE_ACCESS_DUPLEX | PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT,
|
||||
&sandbox_sid,
|
||||
)?;
|
||||
let h_stderr_pipe = create_named_pipe(
|
||||
&stderr_name,
|
||||
PIPE_ACCESS_DUPLEX | PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT,
|
||||
&sandbox_sid,
|
||||
)?;
|
||||
let pipe_in_name = pipe_name("in");
|
||||
let pipe_out_name = pipe_name("out");
|
||||
let h_pipe_in = create_named_pipe(&pipe_in_name, PIPE_ACCESS_OUTBOUND, &sandbox_sid)?;
|
||||
let h_pipe_out = create_named_pipe(&pipe_out_name, PIPE_ACCESS_INBOUND, &sandbox_sid)?;
|
||||
|
||||
// Launch runner as sandbox user via CreateProcessWithLogonW.
|
||||
let runner_exe = find_runner_exe(codex_home, logs_base_dir);
|
||||
@@ -297,40 +279,11 @@ mod windows_impl {
|
||||
.to_str()
|
||||
.map(|s| s.to_string())
|
||||
.unwrap_or_else(|| "codex-command-runner.exe".to_string());
|
||||
// Write request to a file under the sandbox base dir for the runner to read.
|
||||
// TODO(iceweasel) - use a different mechanism for invoking the runner.
|
||||
let base_tmp = sandbox_base.join("requests");
|
||||
std::fs::create_dir_all(&base_tmp)?;
|
||||
let mut rng = SmallRng::from_entropy();
|
||||
let req_file = base_tmp.join(format!("request-{:x}.json", rng.gen::<u128>()));
|
||||
let payload = RunnerPayload {
|
||||
policy_json_or_preset: policy_json_or_preset.to_string(),
|
||||
sandbox_policy_cwd: sandbox_policy_cwd.to_path_buf(),
|
||||
codex_home: sandbox_base.clone(),
|
||||
real_codex_home: codex_home.to_path_buf(),
|
||||
cap_sids: cap_sids.clone(),
|
||||
request_file: Some(req_file.clone()),
|
||||
command: command.clone(),
|
||||
cwd: cwd.to_path_buf(),
|
||||
env_map: env_map.clone(),
|
||||
timeout_ms,
|
||||
use_private_desktop,
|
||||
stdin_pipe: stdin_name.clone(),
|
||||
stdout_pipe: stdout_name.clone(),
|
||||
stderr_pipe: stderr_name.clone(),
|
||||
};
|
||||
let payload_json = serde_json::to_string(&payload)?;
|
||||
if let Err(e) = fs::write(&req_file, &payload_json) {
|
||||
log_note(
|
||||
&format!("error writing request file {}: {}", req_file.display(), e),
|
||||
logs_base_dir,
|
||||
);
|
||||
return Err(e.into());
|
||||
}
|
||||
let runner_full_cmd = format!(
|
||||
"{} {}",
|
||||
"{} {} {}",
|
||||
quote_windows_arg(&runner_cmdline),
|
||||
quote_windows_arg(&format!("--request-file={}", req_file.display()))
|
||||
quote_windows_arg(&format!("--pipe-in={pipe_in_name}")),
|
||||
quote_windows_arg(&format!("--pipe-out={pipe_out_name}"))
|
||||
);
|
||||
let mut cmdline_vec: Vec<u16> = to_wide(&runner_full_cmd);
|
||||
let exe_w: Vec<u16> = to_wide(&runner_cmdline);
|
||||
@@ -390,73 +343,99 @@ mod windows_impl {
|
||||
return Err(anyhow::anyhow!("CreateProcessWithLogonW failed: {}", err));
|
||||
}
|
||||
|
||||
// Pipes are no longer passed as std handles; no stdin payload is sent.
|
||||
connect_pipe(h_stdin_pipe)?;
|
||||
connect_pipe(h_stdout_pipe)?;
|
||||
connect_pipe(h_stderr_pipe)?;
|
||||
unsafe {
|
||||
CloseHandle(h_stdin_pipe);
|
||||
if let Err(err) = connect_pipe(h_pipe_in) {
|
||||
unsafe {
|
||||
CloseHandle(h_pipe_in);
|
||||
CloseHandle(h_pipe_out);
|
||||
if pi.hThread != 0 {
|
||||
CloseHandle(pi.hThread);
|
||||
}
|
||||
if pi.hProcess != 0 {
|
||||
CloseHandle(pi.hProcess);
|
||||
}
|
||||
}
|
||||
return Err(err.into());
|
||||
}
|
||||
if let Err(err) = connect_pipe(h_pipe_out) {
|
||||
unsafe {
|
||||
CloseHandle(h_pipe_in);
|
||||
CloseHandle(h_pipe_out);
|
||||
if pi.hThread != 0 {
|
||||
CloseHandle(pi.hThread);
|
||||
}
|
||||
if pi.hProcess != 0 {
|
||||
CloseHandle(pi.hProcess);
|
||||
}
|
||||
}
|
||||
return Err(err.into());
|
||||
}
|
||||
|
||||
// Read stdout/stderr.
|
||||
let (tx_out, rx_out) = std::sync::mpsc::channel::<Vec<u8>>();
|
||||
let (tx_err, rx_err) = std::sync::mpsc::channel::<Vec<u8>>();
|
||||
let t_out = std::thread::spawn(move || {
|
||||
let mut buf = Vec::new();
|
||||
let mut tmp = [0u8; 8192];
|
||||
loop {
|
||||
let mut read_bytes: u32 = 0;
|
||||
let ok = unsafe {
|
||||
windows_sys::Win32::Storage::FileSystem::ReadFile(
|
||||
h_stdout_pipe,
|
||||
tmp.as_mut_ptr(),
|
||||
tmp.len() as u32,
|
||||
&mut read_bytes,
|
||||
std::ptr::null_mut(),
|
||||
)
|
||||
};
|
||||
if ok == 0 || read_bytes == 0 {
|
||||
break;
|
||||
}
|
||||
buf.extend_from_slice(&tmp[..read_bytes as usize]);
|
||||
}
|
||||
let _ = tx_out.send(buf);
|
||||
});
|
||||
let t_err = std::thread::spawn(move || {
|
||||
let mut buf = Vec::new();
|
||||
let mut tmp = [0u8; 8192];
|
||||
loop {
|
||||
let mut read_bytes: u32 = 0;
|
||||
let ok = unsafe {
|
||||
windows_sys::Win32::Storage::FileSystem::ReadFile(
|
||||
h_stderr_pipe,
|
||||
tmp.as_mut_ptr(),
|
||||
tmp.len() as u32,
|
||||
&mut read_bytes,
|
||||
std::ptr::null_mut(),
|
||||
)
|
||||
};
|
||||
if ok == 0 || read_bytes == 0 {
|
||||
break;
|
||||
}
|
||||
buf.extend_from_slice(&tmp[..read_bytes as usize]);
|
||||
}
|
||||
let _ = tx_err.send(buf);
|
||||
});
|
||||
let result = (|| -> Result<CaptureResult> {
|
||||
let mut pipe_write = unsafe { File::from_raw_handle(h_pipe_in as _) };
|
||||
let mut pipe_read = unsafe { File::from_raw_handle(h_pipe_out as _) };
|
||||
|
||||
let timeout = timeout_ms.map(|ms| ms as u32).unwrap_or(INFINITE);
|
||||
let res = unsafe { WaitForSingleObject(pi.hProcess, timeout) };
|
||||
let timed_out = res == 0x0000_0102;
|
||||
let mut exit_code_u32: u32 = 1;
|
||||
if !timed_out {
|
||||
unsafe {
|
||||
GetExitCodeProcess(pi.hProcess, &mut exit_code_u32);
|
||||
let spawn_request = FramedMessage {
|
||||
version: 1,
|
||||
message: Message::SpawnRequest {
|
||||
payload: Box::new(SpawnRequest {
|
||||
command: command.clone(),
|
||||
cwd: cwd.to_path_buf(),
|
||||
env: env_map.clone(),
|
||||
policy_json_or_preset: policy_json_or_preset.to_string(),
|
||||
sandbox_policy_cwd: sandbox_policy_cwd.to_path_buf(),
|
||||
codex_home: sandbox_base.clone(),
|
||||
real_codex_home: codex_home.to_path_buf(),
|
||||
cap_sids,
|
||||
timeout_ms,
|
||||
tty: false,
|
||||
stdin_open: false,
|
||||
use_private_desktop,
|
||||
}),
|
||||
},
|
||||
};
|
||||
write_frame(&mut pipe_write, &spawn_request)?;
|
||||
read_spawn_ready(&mut pipe_read)?;
|
||||
drop(pipe_write);
|
||||
|
||||
let mut stdout = Vec::new();
|
||||
let mut stderr = Vec::new();
|
||||
let (exit_code, timed_out) = loop {
|
||||
let msg = read_frame(&mut pipe_read)?
|
||||
.ok_or_else(|| anyhow::anyhow!("runner pipe closed before exit"))?;
|
||||
match msg.message {
|
||||
Message::SpawnReady { .. } => {}
|
||||
Message::Output { payload } => {
|
||||
let bytes = decode_bytes(&payload.data_b64)?;
|
||||
match payload.stream {
|
||||
OutputStream::Stdout => stdout.extend_from_slice(&bytes),
|
||||
OutputStream::Stderr => stderr.extend_from_slice(&bytes),
|
||||
}
|
||||
}
|
||||
Message::Exit { payload } => break (payload.exit_code, payload.timed_out),
|
||||
Message::Error { payload } => {
|
||||
return Err(anyhow::anyhow!("runner error: {}", payload.message));
|
||||
}
|
||||
other => {
|
||||
return Err(anyhow::anyhow!(
|
||||
"unexpected runner message during capture: {other:?}"
|
||||
));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if exit_code == 0 {
|
||||
log_success(&command, logs_base_dir);
|
||||
} else {
|
||||
log_failure(&command, &format!("exit code {}", exit_code), logs_base_dir);
|
||||
}
|
||||
} else {
|
||||
unsafe {
|
||||
windows_sys::Win32::System::Threading::TerminateProcess(pi.hProcess, 1);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(CaptureResult {
|
||||
exit_code,
|
||||
stdout,
|
||||
stderr,
|
||||
timed_out,
|
||||
})
|
||||
})();
|
||||
|
||||
unsafe {
|
||||
if pi.hThread != 0 {
|
||||
@@ -465,31 +444,9 @@ mod windows_impl {
|
||||
if pi.hProcess != 0 {
|
||||
CloseHandle(pi.hProcess);
|
||||
}
|
||||
CloseHandle(h_stdout_pipe);
|
||||
CloseHandle(h_stderr_pipe);
|
||||
}
|
||||
let _ = t_out.join();
|
||||
let _ = t_err.join();
|
||||
let stdout = rx_out.recv().unwrap_or_default();
|
||||
let stderr = rx_err.recv().unwrap_or_default();
|
||||
let exit_code = if timed_out {
|
||||
128 + 64
|
||||
} else {
|
||||
exit_code_u32 as i32
|
||||
};
|
||||
|
||||
if exit_code == 0 {
|
||||
log_success(&command, logs_base_dir);
|
||||
} else {
|
||||
log_failure(&command, &format!("exit code {}", exit_code), logs_base_dir);
|
||||
}
|
||||
|
||||
Ok(CaptureResult {
|
||||
exit_code,
|
||||
stdout,
|
||||
stderr,
|
||||
timed_out,
|
||||
})
|
||||
result
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
Reference in New Issue
Block a user