Add Windows sandbox unified exec runtime support (#15578)

## Summary

This is the runtime/foundation half of the Windows sandbox unified-exec
work.

- add Windows sandbox `unified_exec` session support in
`windows-sandbox-rs` for both:
  - the legacy restricted-token backend
  - the elevated runner backend
- extend the PTY/process runtime so driver-backed sessions can support:
  - stdin streaming
  - stdout/stderr separation
  - exit propagation
  - PTY resize hooks
- add Windows sandbox runtime coverage in `codex-windows-sandbox` /
`codex-utils-pty`

This PR does **not** enable Windows sandbox `UnifiedExec` for product
callers yet because hooking this up to app-server comes in the next PR.

Windows sandbox advertising is intentionally kept aligned with `main`,
so sandboxed Windows callers still fall back to `ShellCommand`.

This PR isolates the runtime/session layer so it can be reviewed
independently from product-surface enablement.

---------

Co-authored-by: jif-oai <jif@openai.com>
Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
authored and GitHub committed 2026-04-21 10:44:49 -07:00
1 parent 38ba876ea9
commit 8612714aa6
32 files changed
+2620 -210

No files matched your search

@@ -15,10 +15,12 @@ use anyhow::Result;
use codex_windows_sandbox::ErrorPayload;
use codex_windows_sandbox::ExitPayload;
use codex_windows_sandbox::FramedMessage;
use codex_windows_sandbox::LaunchDesktop;
use codex_windows_sandbox::Message;
use codex_windows_sandbox::OutputPayload;
use codex_windows_sandbox::OutputStream;
use codex_windows_sandbox::PipeSpawnHandles;
use codex_windows_sandbox::ResizePayload;
use codex_windows_sandbox::SandboxPolicy;
use codex_windows_sandbox::SpawnReady;
use codex_windows_sandbox::SpawnRequest;
@@ -56,7 +58,9 @@ 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::Console::COORD;
use windows_sys::Win32::System::Console::ClosePseudoConsole;
use windows_sys::Win32::System::Console::ResizePseudoConsole;
use windows_sys::Win32::System::JobObjects::AssignProcessToJobObject;
use windows_sys::Win32::System::JobObjects::CreateJobObjectW;
use windows_sys::Win32::System::JobObjects::JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
@@ -86,6 +90,8 @@ struct IpcSpawnedProcess {
stderr_handle: HANDLE,
stdin_handle: Option<HANDLE>,
hpc_handle: Option<HANDLE>,
_desktop_owner: Option<LaunchDesktop>,
_pipe_handles: Option<PipeSpawnHandles>,
}
unsafe fn create_job_kill_on_close() -> Result<HANDLE> {
@@ -166,7 +172,7 @@ fn effective_cwd(req_cwd: &Path, log_dir: Option<&Path>) -> PathBuf {
Err(err) => {
log_note(
&format!(
"junction: read_acl_mutex_exists failed: {err}; assuming read ACL helper is running"
"junction: failed to probe ACL mutex state: {err}; defaulting to junction cwd"
),
log_dir,
);
@@ -174,10 +180,6 @@ fn effective_cwd(req_cwd: &Path, log_dir: Option<&Path>) -> PathBuf {
}
};
if use_junction {
log_note(
"junction: read ACL helper running; using junction CWD",
log_dir,
);
cwd_junction::create_cwd_junction(req_cwd, log_dir).unwrap_or_else(|| req_cwd.to_path_buf())
} else {
req_cwd.to_path_buf()
@@ -187,16 +189,6 @@ fn effective_cwd(req_cwd: &Path, log_dir: Option<&Path>) -> PathBuf {
fn spawn_ipc_process(req: &SpawnRequest) -> Result<IpcSpawnedProcess> {
let log_dir = req.codex_home.clone();
hide_current_user_profile_dir(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 mut cap_psids: Vec<*mut c_void> = Vec::new();
for sid in &req.cap_sids {
@@ -240,16 +232,10 @@ fn spawn_ipc_process(req: &SpawnRequest) -> Result<IpcSpawnedProcess> {
}
let effective_cwd = effective_cwd(&req.cwd, Some(log_dir.as_path()));
log_note(
&format!(
"runner: effective cwd={} (requested {})",
effective_cwd.display(),
req.cwd.display()
),
Some(log_dir.as_path()),
);
let mut hpc_handle: Option<HANDLE> = None;
let mut desktop_owner = None;
let mut pipe_handles = None;
let (pi, stdout_handle, stderr_handle, stdin_handle) = if req.tty {
let (pi, conpty) = codex_windows_sandbox::spawn_conpty_process_as_user(
h_token,
@@ -259,8 +245,9 @@ fn spawn_ipc_process(req: &SpawnRequest) -> Result<IpcSpawnedProcess> {
req.use_private_desktop,
Some(log_dir.as_path()),
)?;
let (hpc, input_write, output_read) = conpty.into_raw();
let (hpc, input_write, output_read, desktop) = conpty.into_raw();
hpc_handle = Some(hpc);
desktop_owner = desktop;
let stdin_handle = if req.stdin_open {
Some(input_write)
} else {
@@ -281,29 +268,29 @@ fn spawn_ipc_process(req: &SpawnRequest) -> Result<IpcSpawnedProcess> {
} else {
StdinMode::Closed
};
let pipe_handles: PipeSpawnHandles = spawn_process_with_pipes(
let spawned_pipes: PipeSpawnHandles = spawn_process_with_pipes(
h_token,
&req.command,
&effective_cwd,
&req.env,
stdin_mode,
StderrMode::Separate,
/*use_private_desktop*/ false,
req.use_private_desktop,
Some(log_dir.as_path()),
)?;
(
pipe_handles.process,
pipe_handles.stdout_read,
pipe_handles
.stderr_read
.unwrap_or(windows_sys::Win32::Foundation::INVALID_HANDLE_VALUE),
pipe_handles.stdin_write,
)
let pi = spawned_pipes.process;
let stdout_handle = spawned_pipes.stdout_read;
let stderr_handle = spawned_pipes
.stderr_read
.unwrap_or(windows_sys::Win32::Foundation::INVALID_HANDLE_VALUE);
let stdin_handle = spawned_pipes.stdin_write;
pipe_handles = Some(spawned_pipes);
(pi, stdout_handle, stderr_handle, stdin_handle)
};
unsafe {
CloseHandle(h_token);
}
Ok(IpcSpawnedProcess {
log_dir,
pi,
@@ -311,6 +298,8 @@ fn spawn_ipc_process(req: &SpawnRequest) -> Result<IpcSpawnedProcess> {
stderr_handle,
stdin_handle,
hpc_handle,
_desktop_owner: desktop_owner,
_pipe_handles: pipe_handles,
})
}
@@ -346,21 +335,17 @@ fn spawn_output_reader(
fn spawn_input_loop(
mut reader: File,
stdin_handle: Option<HANDLE>,
hpc_handle: Arc<StdMutex<Option<HANDLE>>>,
process_handle: Arc<StdMutex<Option<HANDLE>>>,
log_dir: Option<PathBuf>,
_log_dir: Option<PathBuf>,
) -> std::thread::JoinHandle<()> {
std::thread::spawn(move || {
let mut stdin_handle = stdin_handle;
loop {
let msg = match read_frame(&mut reader) {
Ok(Some(v)) => v,
Ok(None) => break,
Err(err) => {
log_note(
&format!("runner input read failed: {err}"),
log_dir.as_deref(),
);
break;
}
Err(_) => break,
};
match msg.message {
Message::Stdin { payload } => {
@@ -380,6 +365,30 @@ fn spawn_input_loop(
}
}
}
Message::CloseStdin { .. } => {
if let Some(handle) = stdin_handle.take() {
unsafe {
CloseHandle(handle);
}
}
}
Message::Resize {
payload: ResizePayload { rows, cols },
} => {
if let Ok(guard) = hpc_handle.lock()
&& let Some(hpc) = guard.as_ref()
{
unsafe {
let _ = ResizePseudoConsole(
*hpc,
COORD {
X: cols as i16,
Y: rows as i16,
},
);
}
}
}
Message::Terminate { .. } => {
if let Ok(guard) = process_handle.lock()
&& let Some(handle) = guard.as_ref()
@@ -450,7 +459,7 @@ pub fn main() -> Result<()> {
let stdout_handle = ipc_spawn.stdout_handle;
let stderr_handle = ipc_spawn.stderr_handle;
let stdin_handle = ipc_spawn.stdin_handle;
let hpc_handle = ipc_spawn.hpc_handle;
let hpc_handle = Arc::new(StdMutex::new(ipc_spawn.hpc_handle));
let h_job = unsafe { create_job_kill_on_close().ok() };
if let Some(job) = h_job {
@@ -474,7 +483,6 @@ pub fn main() -> Result<()> {
} else {
anyhow::bail!("runner spawn_ready write failed: pipe_write lock poisoned");
} {
log_note(&format!("runner spawn_ready write failed: {err}"), log_dir);
let _ = send_error(&pipe_write, "spawn_failed", err.to_string());
return Err(err);
}
@@ -499,6 +507,7 @@ pub fn main() -> Result<()> {
let _input_thread = spawn_input_loop(
pipe_read,
stdin_handle,
Arc::clone(&hpc_handle),
Arc::clone(&process_handle),
log_dir_owned,
);
@@ -517,9 +526,6 @@ pub fn main() -> Result<()> {
GetExitCodeProcess(pi.hProcess, &mut raw_exit);
exit_code = raw_exit as i32;
}
if let Some(hpc) = hpc_handle {
ClosePseudoConsole(hpc);
}
if pi.hThread != 0 {
CloseHandle(pi.hThread);
}
@@ -530,10 +536,20 @@ pub fn main() -> Result<()> {
CloseHandle(job);
}
}
let _ = out_thread.join();
if let Some(err_thread) = err_thread {
let _ = err_thread.join();
if let Ok(mut guard) = hpc_handle.lock()
&& let Some(hpc) = guard.take()
{
unsafe {
ClosePseudoConsole(hpc);
}
}
let _ = out_thread.join();
if let Some(thread) = err_thread {
let _ = thread.join();
}
let exit_msg = FramedMessage {
version: 1,
message: Message::Exit {
@@ -33,8 +33,8 @@ pub struct FramedMessage {
/// IPC message variants exchanged between parent and runner.
///
/// `SpawnRequest`, `Stdin`, and `Terminate` are parent->runner commands. `SpawnReady`,
/// `Output`, `Exit`, and `Error` are runner->parent events/results.
/// `SpawnRequest`, `Stdin`, `CloseStdin`, `Resize`, and `Terminate` are parent->runner commands.
/// `SpawnReady`, `Output`, `Exit`, and `Error` are runner->parent events/results.
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum Message {
@@ -42,6 +42,8 @@ pub enum Message {
SpawnReady { payload: SpawnReady },
Output { payload: OutputPayload },
Stdin { payload: StdinPayload },
CloseStdin { payload: EmptyPayload },
Resize { payload: ResizePayload },
Exit { payload: ExitPayload },
Error { payload: ErrorPayload },
Terminate { payload: EmptyPayload },
@@ -93,6 +95,13 @@ pub struct StdinPayload {
pub data_b64: String,
}
/// PTY resize request sent from parent to runner.
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct ResizePayload {
pub rows: u16,
pub cols: u16,
}
/// Exit status sent from runner to parent.
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct ExitPayload {
@@ -0,0 +1,213 @@
use crate::identity::SandboxCreds;
use crate::ipc_framed::FramedMessage;
use crate::ipc_framed::Message;
use crate::ipc_framed::SpawnRequest;
use crate::ipc_framed::read_frame;
use crate::ipc_framed::write_frame;
use crate::runner_pipe::PIPE_ACCESS_INBOUND;
use crate::runner_pipe::PIPE_ACCESS_OUTBOUND;
use crate::runner_pipe::connect_pipe;
use crate::runner_pipe::create_named_pipe;
use crate::runner_pipe::find_runner_exe;
use crate::runner_pipe::pipe_pair;
use crate::winutil::quote_windows_arg;
use crate::winutil::to_wide;
use anyhow::Result;
use std::ffi::c_void;
use std::fs::File;
use std::os::windows::io::AsRawHandle;
use std::os::windows::io::FromRawHandle;
use std::path::Path;
use std::ptr;
use std::time::Duration;
use std::time::Instant;
use windows_sys::Win32::Foundation::CloseHandle;
use windows_sys::Win32::Foundation::GetLastError;
use windows_sys::Win32::Foundation::HANDLE;
use windows_sys::Win32::System::Diagnostics::Debug::SetErrorMode;
use windows_sys::Win32::System::Pipes::PeekNamedPipe;
use windows_sys::Win32::System::Threading::CreateProcessWithLogonW;
use windows_sys::Win32::System::Threading::LOGON_WITH_PROFILE;
use windows_sys::Win32::System::Threading::PROCESS_INFORMATION;
use windows_sys::Win32::System::Threading::STARTUPINFOW;
const RUNNER_SPAWN_READY_TIMEOUT: Duration = Duration::from_secs(15);
const RUNNER_SPAWN_READY_POLL_INTERVAL: Duration = Duration::from_millis(50);
const RUNNER_ERROR_MODE_FLAGS: u32 = 0x0001 | 0x0002;
pub(crate) struct RunnerTransport {
pipe_write: File,
pipe_read: File,
}
impl RunnerTransport {
pub(crate) fn send_spawn_request(&mut self, request: SpawnRequest) -> Result<()> {
let spawn_request = FramedMessage {
version: 1,
message: Message::SpawnRequest {
payload: Box::new(request),
},
};
write_frame(&mut self.pipe_write, &spawn_request)
}
pub(crate) fn read_spawn_ready(&mut self) -> Result<()> {
wait_for_complete_frame(&self.pipe_read, RUNNER_SPAWN_READY_TIMEOUT)?;
let msg = read_frame(&mut self.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:?}"
)),
}
}
pub(crate) fn into_files(self) -> (File, File) {
(self.pipe_write, self.pipe_read)
}
}
pub(crate) fn spawn_runner_transport(
codex_home: &Path,
cwd: &Path,
sandbox_creds: &SandboxCreds,
log_dir: Option<&Path>,
) -> Result<RunnerTransport> {
let (pipe_in_name, pipe_out_name) = pipe_pair();
let h_pipe_in =
create_named_pipe(&pipe_in_name, PIPE_ACCESS_OUTBOUND, &sandbox_creds.username)?;
let h_pipe_out =
create_named_pipe(&pipe_out_name, PIPE_ACCESS_INBOUND, &sandbox_creds.username)?;
let runner_exe = find_runner_exe(codex_home, log_dir);
let runner_cmdline = runner_exe
.to_str()
.map(str::to_owned)
.unwrap_or_else(|| "codex-command-runner.exe".to_string());
let runner_full_cmd = format!(
"{} {} {}",
quote_windows_arg(&runner_cmdline),
quote_windows_arg(&format!("--pipe-in={pipe_in_name}")),
quote_windows_arg(&format!("--pipe-out={pipe_out_name}"))
);
let mut cmdline_vec = to_wide(&runner_full_cmd);
let exe_w = to_wide(&runner_cmdline);
let cwd_w = to_wide(cwd);
let user_w = to_wide(&sandbox_creds.username);
let domain_w = to_wide(".");
let password_w = to_wide(&sandbox_creds.password);
let mut si: STARTUPINFOW = unsafe { std::mem::zeroed() };
si.cb = std::mem::size_of::<STARTUPINFOW>() as u32;
let mut pi: PROCESS_INFORMATION = unsafe { std::mem::zeroed() };
let env_block: Option<Vec<u16>> = None;
let previous_error_mode = unsafe { SetErrorMode(RUNNER_ERROR_MODE_FLAGS) };
let spawn_res = unsafe {
CreateProcessWithLogonW(
user_w.as_ptr(),
domain_w.as_ptr(),
password_w.as_ptr(),
LOGON_WITH_PROFILE,
exe_w.as_ptr(),
cmdline_vec.as_mut_ptr(),
windows_sys::Win32::System::Threading::CREATE_NO_WINDOW
| windows_sys::Win32::System::Threading::CREATE_UNICODE_ENVIRONMENT,
env_block
.as_ref()
.map(|block| block.as_ptr() as *const c_void)
.unwrap_or(ptr::null()),
cwd_w.as_ptr(),
&si,
&mut pi,
)
};
unsafe {
SetErrorMode(previous_error_mode);
}
if spawn_res == 0 {
let err = unsafe { GetLastError() } as i32;
unsafe {
CloseHandle(h_pipe_in);
CloseHandle(h_pipe_out);
}
return Err(anyhow::anyhow!("CreateProcessWithLogonW failed: {err}"));
}
let connect_result = (|| -> Result<()> {
connect_pipe(h_pipe_in)?;
connect_pipe(h_pipe_out)?;
Ok(())
})();
unsafe {
if pi.hThread != 0 {
CloseHandle(pi.hThread);
}
if pi.hProcess != 0 {
CloseHandle(pi.hProcess);
}
}
if let Err(err) = connect_result {
unsafe {
CloseHandle(h_pipe_in);
CloseHandle(h_pipe_out);
}
return Err(err);
}
let pipe_write = unsafe { File::from_raw_handle(h_pipe_in as _) };
let pipe_read = unsafe { File::from_raw_handle(h_pipe_out as _) };
Ok(RunnerTransport {
pipe_write,
pipe_read,
})
}
fn wait_for_complete_frame(pipe_read: &File, timeout: Duration) -> Result<()> {
let handle = pipe_read.as_raw_handle() as HANDLE;
let deadline = Instant::now() + timeout;
let mut len_buf = [0u8; 4];
loop {
let mut bytes_read = 0u32;
let mut total_available = 0u32;
let ok = unsafe {
PeekNamedPipe(
handle,
len_buf.as_mut_ptr() as *mut c_void,
len_buf.len() as u32,
&mut bytes_read,
&mut total_available,
ptr::null_mut(),
)
};
if ok == 0 {
let err = unsafe { GetLastError() } as i32;
return Err(anyhow::anyhow!(
"PeekNamedPipe failed while waiting for spawn_ready: {err}"
));
}
if bytes_read == len_buf.len() as u32 {
let frame_len = u32::from_le_bytes(len_buf) as usize;
let total_len = frame_len
.checked_add(len_buf.len())
.ok_or_else(|| anyhow::anyhow!("runner frame length overflow"))?;
if total_available as usize >= total_len {
return Ok(());
}
}
if Instant::now() >= deadline {
return Err(anyhow::anyhow!(
"timed out after {}ms waiting for runner spawn_ready",
timeout.as_millis()
));
}
std::thread::sleep(RUNNER_SPAWN_READY_POLL_INTERVAL);
}
}
@@ -20,6 +20,8 @@ use std::path::PathBuf;
use std::ptr;
use windows_sys::Win32::Foundation::GetLastError;
use windows_sys::Win32::Foundation::HANDLE;
use windows_sys::Win32::Foundation::HLOCAL;
use windows_sys::Win32::Foundation::LocalFree;
use windows_sys::Win32::Security::Authorization::ConvertStringSecurityDescriptorToSecurityDescriptorW;
use windows_sys::Win32::Security::PSECURITY_DESCRIPTOR;
use windows_sys::Win32::Security::SECURITY_ATTRIBUTES;
@@ -43,7 +45,8 @@ pub fn find_runner_exe(codex_home: &Path, log_dir: Option<&Path>) -> PathBuf {
/// Generates a unique named-pipe path used to communicate with the runner process.
pub fn pipe_pair() -> (String, String) {
let mut rng = SmallRng::from_entropy();
let base = format!(r"\\.\pipe\codex-runner-{:x}", rng.gen::<u128>());
let nonce: u128 = rng.r#gen();
let base = format!(r"\\.\pipe\codex-runner-{nonce:x}");
(format!("{base}-in"), format!("{base}-out"))
}
@@ -86,6 +89,9 @@ pub fn create_named_pipe(name: &str, access: u32, sandbox_username: &str) -> io:
&mut sa as *mut SECURITY_ATTRIBUTES,
)
};
unsafe {
LocalFree(sd as HLOCAL);
}
if h == 0 || h == windows_sys::Win32::Foundation::INVALID_HANDLE_VALUE {
return Err(io::Error::from_raw_os_error(unsafe {
GetLastError() as i32