mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
## 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`
308 lines
9.7 KiB
Rust
308 lines
9.7 KiB
Rust
use crate::desktop::LaunchDesktop;
|
|
use crate::logging;
|
|
use crate::winutil::format_last_error;
|
|
use crate::winutil::quote_windows_arg;
|
|
use crate::winutil::to_wide;
|
|
use anyhow::anyhow;
|
|
use anyhow::Result;
|
|
use std::collections::HashMap;
|
|
use std::ffi::c_void;
|
|
use std::path::Path;
|
|
use std::ptr;
|
|
use windows_sys::Win32::Foundation::GetLastError;
|
|
use windows_sys::Win32::Foundation::CloseHandle;
|
|
use windows_sys::Win32::Foundation::SetHandleInformation;
|
|
use windows_sys::Win32::Foundation::HANDLE;
|
|
use windows_sys::Win32::Foundation::HANDLE_FLAG_INHERIT;
|
|
use windows_sys::Win32::Foundation::INVALID_HANDLE_VALUE;
|
|
use windows_sys::Win32::Storage::FileSystem::ReadFile;
|
|
use windows_sys::Win32::System::Console::GetStdHandle;
|
|
use windows_sys::Win32::System::Console::STD_ERROR_HANDLE;
|
|
use windows_sys::Win32::System::Console::STD_INPUT_HANDLE;
|
|
use windows_sys::Win32::System::Console::STD_OUTPUT_HANDLE;
|
|
use windows_sys::Win32::System::Pipes::CreatePipe;
|
|
use windows_sys::Win32::System::Threading::CreateProcessAsUserW;
|
|
use windows_sys::Win32::System::Threading::CREATE_UNICODE_ENVIRONMENT;
|
|
use windows_sys::Win32::System::Threading::PROCESS_INFORMATION;
|
|
use windows_sys::Win32::System::Threading::STARTF_USESTDHANDLES;
|
|
use windows_sys::Win32::System::Threading::STARTUPINFOW;
|
|
|
|
pub struct CreatedProcess {
|
|
pub process_info: PROCESS_INFORMATION,
|
|
pub startup_info: STARTUPINFOW,
|
|
_desktop: LaunchDesktop,
|
|
}
|
|
|
|
pub fn make_env_block(env: &HashMap<String, String>) -> Vec<u16> {
|
|
let mut items: Vec<(String, String)> =
|
|
env.iter().map(|(k, v)| (k.clone(), v.clone())).collect();
|
|
items.sort_by(|a, b| {
|
|
a.0.to_uppercase()
|
|
.cmp(&b.0.to_uppercase())
|
|
.then(a.0.cmp(&b.0))
|
|
});
|
|
let mut w: Vec<u16> = Vec::new();
|
|
for (k, v) in items {
|
|
let mut s = to_wide(format!("{}={}", k, v));
|
|
s.pop();
|
|
w.extend_from_slice(&s);
|
|
w.push(0);
|
|
}
|
|
w.push(0);
|
|
w
|
|
}
|
|
|
|
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);
|
|
if h == 0 || h == INVALID_HANDLE_VALUE {
|
|
return Err(anyhow!("GetStdHandle failed: {}", GetLastError()));
|
|
}
|
|
if SetHandleInformation(h, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT) == 0 {
|
|
return Err(anyhow!("SetHandleInformation failed: {}", GetLastError()));
|
|
}
|
|
}
|
|
si.dwFlags |= STARTF_USESTDHANDLES;
|
|
si.hStdInput = GetStdHandle(STD_INPUT_HANDLE);
|
|
si.hStdOutput = GetStdHandle(STD_OUTPUT_HANDLE);
|
|
si.hStdError = GetStdHandle(STD_ERROR_HANDLE);
|
|
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<String, String>,
|
|
logs_base_dir: Option<&Path>,
|
|
stdio: Option<(HANDLE, HANDLE, HANDLE)>,
|
|
use_private_desktop: bool,
|
|
) -> Result<CreatedProcess> {
|
|
let cmdline_str = argv
|
|
.iter()
|
|
.map(|a| quote_windows_arg(a))
|
|
.collect::<Vec<_>>()
|
|
.join(" ");
|
|
let mut cmdline: Vec<u16> = to_wide(&cmdline_str);
|
|
let env_block = make_env_block(env_map);
|
|
let mut si: STARTUPINFOW = std::mem::zeroed();
|
|
si.cb = std::mem::size_of::<STARTUPINFOW>() as u32;
|
|
// Some processes (e.g., PowerShell) can fail with STATUS_DLL_INIT_FAILED
|
|
// if lpDesktop is not set when launching with a restricted token.
|
|
// Point explicitly at the interactive desktop or a private desktop.
|
|
let desktop = LaunchDesktop::prepare(use_private_desktop, logs_base_dir)?;
|
|
si.lpDesktop = desktop.startup_info_desktop();
|
|
let mut pi: PROCESS_INFORMATION = std::mem::zeroed();
|
|
// Ensure handles are inheritable when custom stdio is supplied.
|
|
let inherit_handles = match stdio {
|
|
Some((stdin_h, stdout_h, stderr_h)) => {
|
|
si.dwFlags |= STARTF_USESTDHANDLES;
|
|
si.hStdInput = stdin_h;
|
|
si.hStdOutput = stdout_h;
|
|
si.hStdError = stderr_h;
|
|
for h in [stdin_h, stdout_h, stderr_h] {
|
|
if SetHandleInformation(h, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT) == 0 {
|
|
return Err(anyhow!(
|
|
"SetHandleInformation failed for stdio handle: {}",
|
|
GetLastError()
|
|
));
|
|
}
|
|
}
|
|
true
|
|
}
|
|
None => {
|
|
ensure_inheritable_stdio(&mut si)?;
|
|
true
|
|
}
|
|
};
|
|
|
|
let creation_flags = CREATE_UNICODE_ENVIRONMENT;
|
|
let cwd_wide = to_wide(cwd);
|
|
let env_block_len = env_block.len();
|
|
|
|
let ok = CreateProcessAsUserW(
|
|
h_token,
|
|
std::ptr::null(),
|
|
cmdline.as_mut_ptr(),
|
|
std::ptr::null_mut(),
|
|
std::ptr::null_mut(),
|
|
inherit_handles as i32,
|
|
creation_flags,
|
|
env_block.as_ptr() as *mut c_void,
|
|
cwd_wide.as_ptr(),
|
|
&si,
|
|
&mut pi,
|
|
);
|
|
if ok == 0 {
|
|
let err = GetLastError() as i32;
|
|
let msg = format!(
|
|
"CreateProcessAsUserW failed: {} ({}) | cwd={} | cmd={} | env_u16_len={} | si_flags={} | creation_flags={}",
|
|
err,
|
|
format_last_error(err),
|
|
cwd.display(),
|
|
cmdline_str,
|
|
env_block_len,
|
|
si.dwFlags,
|
|
creation_flags,
|
|
);
|
|
logging::debug_log(&msg, logs_base_dir);
|
|
return Err(anyhow!("CreateProcessAsUserW failed: {}", err));
|
|
}
|
|
Ok(CreatedProcess {
|
|
process_info: pi,
|
|
startup_info: si,
|
|
_desktop: desktop,
|
|
})
|
|
}
|
|
|
|
/// Controls whether the child's stdin handle is kept open for writing.
|
|
#[allow(dead_code)]
|
|
pub enum StdinMode {
|
|
Closed,
|
|
Open,
|
|
}
|
|
|
|
/// Controls how stderr is wired for a pipe-spawned process.
|
|
#[allow(dead_code)]
|
|
pub enum StderrMode {
|
|
MergeStdout,
|
|
Separate,
|
|
}
|
|
|
|
/// Handles returned by `spawn_process_with_pipes`.
|
|
#[allow(dead_code)]
|
|
pub struct PipeSpawnHandles {
|
|
pub process: PROCESS_INFORMATION,
|
|
pub stdin_write: Option<HANDLE>,
|
|
pub stdout_read: HANDLE,
|
|
pub stderr_read: Option<HANDLE>,
|
|
}
|
|
|
|
/// Spawns a process with anonymous pipes and returns the relevant handles.
|
|
pub fn spawn_process_with_pipes(
|
|
h_token: HANDLE,
|
|
argv: &[String],
|
|
cwd: &Path,
|
|
env_map: &HashMap<String, String>,
|
|
stdin_mode: StdinMode,
|
|
stderr_mode: StderrMode,
|
|
use_private_desktop: bool,
|
|
) -> Result<PipeSpawnHandles> {
|
|
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;
|
|
unsafe {
|
|
if CreatePipe(&mut in_r, &mut in_w, ptr::null_mut(), 0) == 0 {
|
|
return Err(anyhow!("CreatePipe stdin failed: {}", GetLastError()));
|
|
}
|
|
if CreatePipe(&mut out_r, &mut out_w, ptr::null_mut(), 0) == 0 {
|
|
CloseHandle(in_r);
|
|
CloseHandle(in_w);
|
|
return Err(anyhow!("CreatePipe stdout failed: {}", GetLastError()));
|
|
}
|
|
if matches!(stderr_mode, StderrMode::Separate)
|
|
&& CreatePipe(&mut err_r, &mut err_w, ptr::null_mut(), 0) == 0
|
|
{
|
|
CloseHandle(in_r);
|
|
CloseHandle(in_w);
|
|
CloseHandle(out_r);
|
|
CloseHandle(out_w);
|
|
return Err(anyhow!("CreatePipe stderr failed: {}", GetLastError()));
|
|
}
|
|
}
|
|
|
|
let stderr_handle = match stderr_mode {
|
|
StderrMode::MergeStdout => out_w,
|
|
StderrMode::Separate => err_w,
|
|
};
|
|
|
|
let stdio = Some((in_r, out_w, stderr_handle));
|
|
let spawn_result = unsafe {
|
|
create_process_as_user(
|
|
h_token,
|
|
argv,
|
|
cwd,
|
|
env_map,
|
|
None,
|
|
stdio,
|
|
use_private_desktop,
|
|
)
|
|
};
|
|
let created = match spawn_result {
|
|
Ok(v) => v,
|
|
Err(err) => {
|
|
unsafe {
|
|
CloseHandle(in_r);
|
|
CloseHandle(in_w);
|
|
CloseHandle(out_r);
|
|
CloseHandle(out_w);
|
|
if matches!(stderr_mode, StderrMode::Separate) {
|
|
CloseHandle(err_r);
|
|
CloseHandle(err_w);
|
|
}
|
|
}
|
|
return Err(err);
|
|
}
|
|
};
|
|
let pi = created.process_info;
|
|
|
|
unsafe {
|
|
CloseHandle(in_r);
|
|
CloseHandle(out_w);
|
|
if matches!(stderr_mode, StderrMode::Separate) {
|
|
CloseHandle(err_w);
|
|
}
|
|
if matches!(stdin_mode, StdinMode::Closed) {
|
|
CloseHandle(in_w);
|
|
}
|
|
}
|
|
|
|
Ok(PipeSpawnHandles {
|
|
process: pi,
|
|
stdin_write: match stdin_mode {
|
|
StdinMode::Open => Some(in_w),
|
|
StdinMode::Closed => None,
|
|
},
|
|
stdout_read: out_r,
|
|
stderr_read: match stderr_mode {
|
|
StderrMode::Separate => Some(err_r),
|
|
StderrMode::MergeStdout => None,
|
|
},
|
|
})
|
|
}
|
|
|
|
/// Reads a HANDLE until EOF and invokes `on_chunk` for each read.
|
|
pub fn read_handle_loop<F>(handle: HANDLE, mut on_chunk: F) -> std::thread::JoinHandle<()>
|
|
where
|
|
F: FnMut(&[u8]) + Send + 'static,
|
|
{
|
|
std::thread::spawn(move || {
|
|
let mut buf = [0u8; 8192];
|
|
loop {
|
|
let mut read_bytes: u32 = 0;
|
|
let ok = unsafe {
|
|
ReadFile(
|
|
handle,
|
|
buf.as_mut_ptr(),
|
|
buf.len() as u32,
|
|
&mut read_bytes,
|
|
ptr::null_mut(),
|
|
)
|
|
};
|
|
if ok == 0 || read_bytes == 0 {
|
|
break;
|
|
}
|
|
on_chunk(&buf[..read_bytes as usize]);
|
|
}
|
|
unsafe {
|
|
CloseHandle(handle);
|
|
}
|
|
})
|
|
}
|