mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
## Why The elevated Windows command runner currently trusts the first process that connects to its parent-created named pipes. Tightening the pipe ACL already narrows who can reach that boundary, but verifying the connected client PID gives the parent one more fail-closed check: it only accepts the exact runner process it just spawned. ## What changed - validate `GetNamedPipeClientProcessId` after `ConnectNamedPipe` and reject clients whose PID does not match the spawned runner - also did some code de-duplication to route the one-shot elevated capture flow in `windows-sandbox-rs/src/elevated_impl.rs` through `spawn_runner_transport()` so both elevated codepaths use the same pipe bootstrap and PID validation Using the transport unification here also reduces duplication in the elevated Windows IPC bootstrap, so future hardening to the runner handshake only needs to land in one place. ## Validation - `cargo test -p codex-windows-sandbox` - manual testing: one-shot elevated path via `target/debug/codex.exe exec` running a randomized shell command and confirming captured output - manual testing: elevated session path via `target/debug/codex.exe -c 'windows.sandbox="elevated"' sandbox windows -- python -u -c ...` with stdin/stdout round-trips (`READY`, then `GOT:...` for two input lines) --------- Co-authored-by: viyatb-oai <viyatb@openai.com>
136 lines
5.2 KiB
Rust
136 lines
5.2 KiB
Rust
//! Named pipe helpers for the elevated Windows sandbox runner.
|
||
//!
|
||
//! This module generates paired pipe names, creates server‑side pipes with
|
||
//! sandbox-user-scoped ACLs, and waits for the runner to connect. It is
|
||
//! **elevated-path only** and is
|
||
//! used by the parent to establish the IPC channel for both unified_exec sessions
|
||
//! and elevated capture. The legacy restricted‑token path spawns the child directly
|
||
//! and does not use these helpers.
|
||
|
||
use crate::helper_materialization::HelperExecutable;
|
||
use crate::helper_materialization::resolve_helper_for_launch;
|
||
use crate::winutil::resolve_sid;
|
||
use crate::winutil::string_from_sid_bytes;
|
||
use crate::winutil::to_wide;
|
||
use rand::Rng;
|
||
use rand::SeedableRng;
|
||
use rand::rngs::SmallRng;
|
||
use std::io;
|
||
use std::path::Path;
|
||
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;
|
||
use windows_sys::Win32::System::Pipes::ConnectNamedPipe;
|
||
use windows_sys::Win32::System::Pipes::CreateNamedPipeW;
|
||
use windows_sys::Win32::System::Pipes::GetNamedPipeClientProcessId;
|
||
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;
|
||
|
||
/// PIPE_ACCESS_INBOUND (win32 constant), not exposed in windows-sys 0.52.
|
||
pub const PIPE_ACCESS_INBOUND: u32 = 0x0000_0001;
|
||
/// PIPE_ACCESS_OUTBOUND (win32 constant), not exposed in windows-sys 0.52.
|
||
pub const PIPE_ACCESS_OUTBOUND: u32 = 0x0000_0002;
|
||
|
||
/// Resolves the elevated command runner path, preferring the copied helper under
|
||
/// `.sandbox-bin` and falling back to the legacy sibling lookup when needed.
|
||
pub fn find_runner_exe(codex_home: &Path, log_dir: Option<&Path>) -> PathBuf {
|
||
resolve_helper_for_launch(HelperExecutable::CommandRunner, codex_home, log_dir)
|
||
}
|
||
|
||
/// 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 nonce: u128 = rng.r#gen();
|
||
let base = format!(r"\\.\pipe\codex-runner-{nonce:x}");
|
||
(format!("{base}-in"), format!("{base}-out"))
|
||
}
|
||
|
||
/// Creates a named pipe whose DACL only allows the sandbox user to connect.
|
||
pub fn create_named_pipe(name: &str, access: u32, sandbox_username: &str) -> io::Result<HANDLE> {
|
||
let sandbox_sid = resolve_sid(sandbox_username)
|
||
.map_err(|err| io::Error::new(io::ErrorKind::PermissionDenied, err.to_string()))?;
|
||
let sandbox_sid = string_from_sid_bytes(&sandbox_sid)
|
||
.map_err(|err| io::Error::new(io::ErrorKind::PermissionDenied, err))?;
|
||
let sddl = to_wide(format!("D:(A;;GA;;;{sandbox_sid})"));
|
||
let mut sd: PSECURITY_DESCRIPTOR = ptr::null_mut();
|
||
let ok = unsafe {
|
||
ConvertStringSecurityDescriptorToSecurityDescriptorW(
|
||
sddl.as_ptr(),
|
||
1, // SDDL_REVISION_1
|
||
&mut sd,
|
||
ptr::null_mut(),
|
||
)
|
||
};
|
||
if ok == 0 {
|
||
return Err(io::Error::from_raw_os_error(unsafe {
|
||
GetLastError() as i32
|
||
}));
|
||
}
|
||
let mut sa = SECURITY_ATTRIBUTES {
|
||
nLength: std::mem::size_of::<SECURITY_ATTRIBUTES>() as u32,
|
||
lpSecurityDescriptor: sd,
|
||
bInheritHandle: 0,
|
||
};
|
||
let wide = to_wide(name);
|
||
let h = unsafe {
|
||
CreateNamedPipeW(
|
||
wide.as_ptr(),
|
||
access,
|
||
PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT,
|
||
1,
|
||
65536,
|
||
65536,
|
||
0,
|
||
&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
|
||
}));
|
||
}
|
||
Ok(h)
|
||
}
|
||
|
||
/// Waits for the runner to connect to a parent-created server pipe.
|
||
///
|
||
/// This is parent-side only: the runner opens the pipe with `CreateFileW`, while the
|
||
/// parent calls `ConnectNamedPipe`, tolerates the already-connected case, and
|
||
/// verifies that the connected client is the runner process we just spawned.
|
||
pub fn connect_pipe(h: HANDLE, expected_runner_pid: u32) -> io::Result<()> {
|
||
let ok = unsafe { ConnectNamedPipe(h, ptr::null_mut()) };
|
||
if ok == 0 {
|
||
let err = unsafe { GetLastError() };
|
||
const ERROR_PIPE_CONNECTED: u32 = 535;
|
||
if err != ERROR_PIPE_CONNECTED {
|
||
return Err(io::Error::from_raw_os_error(err as i32));
|
||
}
|
||
}
|
||
let mut client_pid = 0;
|
||
let ok = unsafe { GetNamedPipeClientProcessId(h, &mut client_pid) };
|
||
if ok == 0 {
|
||
return Err(io::Error::from_raw_os_error(unsafe {
|
||
GetLastError() as i32
|
||
}));
|
||
}
|
||
if client_pid != expected_runner_pid {
|
||
return Err(io::Error::new(
|
||
io::ErrorKind::PermissionDenied,
|
||
format!(
|
||
"named pipe client pid {client_pid} did not match runner pid {expected_runner_pid}"
|
||
),
|
||
));
|
||
}
|
||
Ok(())
|
||
}
|