mirror of
https://github.com/openai/codex.git
synced 2026-11-14 06:48:26 +00:00
## Why This is the next step in the Windows sandbox migration away from the legacy `SandboxPolicy` abstraction. #22923 moved write-root and token decisions onto `ResolvedWindowsSandboxPermissions`, but setup and identity still accepted `SandboxPolicy` and converted internally. This PR pushes that conversion outward so the setup path consumes the resolved Windows permission view directly. ## What Changed - Changed `SandboxSetupRequest` to carry `ResolvedWindowsSandboxPermissions` instead of `SandboxPolicy` plus policy cwd. - Updated setup refresh/elevation and identity credential preparation to use resolved permissions for read roots, write roots, network identity, and deny-write payload planning. - Removed the production `allow.rs` legacy wrapper; allow-path computation now takes resolved permissions directly. - Added a permissions-based world-writable audit entry point while keeping the existing legacy wrapper for compatibility. - Updated legacy ACL setup and the core Windows setup bridge to construct resolved permissions at the boundary. - Hardened the Windows sandbox integration test helper staging so Bazel retries can reuse an already-staged helper if a prior sandbox helper process still has the executable open. ## Verification - `cargo test -p codex-windows-sandbox` - `cargo test -p codex-core --test all --no-run` - `just fix -p codex-windows-sandbox` - `just fix -p codex-core` - Attempted `cargo check -p codex-windows-sandbox --target x86_64-pc-windows-gnullvm`, but the local machine is missing `x86_64-w64-mingw32-clang`; Windows CI should cover that target. --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/openai/codex/pull/23167). * #23715 * #23714 * __->__ #23167
440 lines
13 KiB
Rust
440 lines
13 KiB
Rust
use super::windows_common::finish_driver_spawn;
|
|
use super::windows_common::normalize_windows_tty_input;
|
|
use crate::conpty::ConptyInstance;
|
|
use crate::conpty::spawn_conpty_process_as_user;
|
|
use crate::desktop::LaunchDesktop;
|
|
use crate::logging::log_failure;
|
|
use crate::logging::log_success;
|
|
use crate::process::StderrMode;
|
|
use crate::process::StdinMode;
|
|
use crate::process::read_handle_loop;
|
|
use crate::process::spawn_process_with_pipes;
|
|
use crate::spawn_prep::LegacyAclSids;
|
|
use crate::spawn_prep::SpawnPrepOptions;
|
|
use crate::spawn_prep::allow_null_device_for_workspace_write;
|
|
use crate::spawn_prep::apply_legacy_session_acl_rules;
|
|
use crate::spawn_prep::legacy_session_capability_roots;
|
|
use crate::spawn_prep::prepare_legacy_session_security;
|
|
use crate::spawn_prep::prepare_legacy_spawn_context;
|
|
use anyhow::Result;
|
|
use codex_utils_absolute_path::AbsolutePathBuf;
|
|
use codex_utils_pty::ProcessDriver;
|
|
use codex_utils_pty::SpawnedProcess;
|
|
use codex_utils_pty::TerminalSize;
|
|
use std::collections::HashMap;
|
|
use std::path::Path;
|
|
use std::ptr;
|
|
use std::sync::Arc;
|
|
use std::sync::Mutex as StdMutex;
|
|
use tokio::sync::broadcast;
|
|
use tokio::sync::mpsc;
|
|
use tokio::sync::oneshot;
|
|
use windows_sys::Win32::Foundation::CloseHandle;
|
|
use windows_sys::Win32::Foundation::GetLastError;
|
|
use windows_sys::Win32::Foundation::HANDLE;
|
|
use windows_sys::Win32::Foundation::INVALID_HANDLE_VALUE;
|
|
use windows_sys::Win32::Storage::FileSystem::WriteFile;
|
|
use windows_sys::Win32::System::Console::COORD;
|
|
use windows_sys::Win32::System::Console::ResizePseudoConsole;
|
|
use windows_sys::Win32::System::Threading::GetExitCodeProcess;
|
|
use windows_sys::Win32::System::Threading::INFINITE;
|
|
use windows_sys::Win32::System::Threading::PROCESS_INFORMATION;
|
|
use windows_sys::Win32::System::Threading::TerminateProcess;
|
|
use windows_sys::Win32::System::Threading::WaitForSingleObject;
|
|
|
|
const WAIT_TIMEOUT: u32 = 0x0000_0102;
|
|
|
|
struct LegacyProcessHandles {
|
|
process: PROCESS_INFORMATION,
|
|
output_join: std::thread::JoinHandle<()>,
|
|
writer_handle: tokio::task::JoinHandle<()>,
|
|
hpc: Option<HANDLE>,
|
|
conpty_owner: Option<ConptyInstance>,
|
|
token_handle: HANDLE,
|
|
desktop: Option<LaunchDesktop>,
|
|
}
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
fn spawn_legacy_process(
|
|
h_token: HANDLE,
|
|
command: &[String],
|
|
cwd: &Path,
|
|
env_map: &HashMap<String, String>,
|
|
use_private_desktop: bool,
|
|
tty: bool,
|
|
stdin_open: bool,
|
|
stdout_tx: broadcast::Sender<Vec<u8>>,
|
|
stderr_tx: Option<broadcast::Sender<Vec<u8>>>,
|
|
writer_rx: mpsc::Receiver<Vec<u8>>,
|
|
logs_base_dir: Option<&Path>,
|
|
) -> Result<LegacyProcessHandles> {
|
|
let (pi, output_join, writer_handle, hpc, conpty_owner, desktop) = if tty {
|
|
let (pi, mut conpty) = spawn_conpty_process_as_user(
|
|
h_token,
|
|
command,
|
|
cwd,
|
|
env_map,
|
|
use_private_desktop,
|
|
logs_base_dir,
|
|
)?;
|
|
let hpc = conpty.raw_handle();
|
|
let output_join = spawn_output_reader(conpty.take_output_read(), stdout_tx);
|
|
let writer_handle = spawn_input_writer(
|
|
Some(conpty.take_input_write()),
|
|
writer_rx,
|
|
/*normalize_newlines*/ true,
|
|
);
|
|
(pi, output_join, writer_handle, hpc, Some(conpty), None)
|
|
} else {
|
|
let pipe_handles = spawn_process_with_pipes(
|
|
h_token,
|
|
command,
|
|
cwd,
|
|
env_map,
|
|
if stdin_open {
|
|
StdinMode::Open
|
|
} else {
|
|
StdinMode::Closed
|
|
},
|
|
StderrMode::Separate,
|
|
use_private_desktop,
|
|
logs_base_dir,
|
|
)?;
|
|
let stdout_join = spawn_output_reader(pipe_handles.stdout_read, stdout_tx);
|
|
let Some(stderr_read) = pipe_handles.stderr_read else {
|
|
anyhow::bail!("separate stderr handle should be present");
|
|
};
|
|
let Some(stderr_tx) = stderr_tx else {
|
|
anyhow::bail!("separate stderr channel should be present");
|
|
};
|
|
let stderr_join = spawn_output_reader(stderr_read, stderr_tx);
|
|
let output_join = std::thread::spawn(move || {
|
|
let _ = stdout_join.join();
|
|
let _ = stderr_join.join();
|
|
});
|
|
let writer_handle = spawn_input_writer(
|
|
pipe_handles.stdin_write,
|
|
writer_rx,
|
|
/*normalize_newlines*/ false,
|
|
);
|
|
(
|
|
pipe_handles.process,
|
|
output_join,
|
|
writer_handle,
|
|
None,
|
|
None,
|
|
Some(pipe_handles.desktop),
|
|
)
|
|
};
|
|
Ok(LegacyProcessHandles {
|
|
process: pi,
|
|
output_join,
|
|
writer_handle,
|
|
hpc,
|
|
conpty_owner,
|
|
token_handle: h_token,
|
|
desktop,
|
|
})
|
|
}
|
|
|
|
fn spawn_output_reader(
|
|
output_read: HANDLE,
|
|
output_tx: broadcast::Sender<Vec<u8>>,
|
|
) -> std::thread::JoinHandle<()> {
|
|
read_handle_loop(output_read, move |chunk| {
|
|
let _ = output_tx.send(chunk.to_vec());
|
|
})
|
|
}
|
|
|
|
fn spawn_input_writer(
|
|
input_write: Option<HANDLE>,
|
|
mut writer_rx: mpsc::Receiver<Vec<u8>>,
|
|
normalize_newlines: bool,
|
|
) -> tokio::task::JoinHandle<()> {
|
|
tokio::task::spawn_blocking(move || {
|
|
let mut previous_was_cr = false;
|
|
while let Some(bytes) = writer_rx.blocking_recv() {
|
|
let Some(handle) = input_write else {
|
|
continue;
|
|
};
|
|
let bytes = if normalize_newlines {
|
|
normalize_windows_tty_input(&bytes, &mut previous_was_cr)
|
|
} else {
|
|
bytes
|
|
};
|
|
if write_all_handle(handle, &bytes).is_err() {
|
|
break;
|
|
}
|
|
}
|
|
if let Some(handle) = input_write {
|
|
unsafe {
|
|
CloseHandle(handle);
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
fn write_all_handle(handle: HANDLE, mut bytes: &[u8]) -> Result<()> {
|
|
while !bytes.is_empty() {
|
|
let mut written = 0u32;
|
|
let ok = unsafe {
|
|
WriteFile(
|
|
handle,
|
|
bytes.as_ptr() as *const _,
|
|
bytes.len() as u32,
|
|
&mut written,
|
|
ptr::null_mut(),
|
|
)
|
|
};
|
|
if ok == 0 {
|
|
let err = unsafe { GetLastError() } as i32;
|
|
return Err(anyhow::anyhow!("WriteFile failed: {err}"));
|
|
}
|
|
if written == 0 {
|
|
anyhow::bail!("WriteFile returned success but wrote 0 bytes");
|
|
}
|
|
bytes = &bytes[written as usize..];
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
fn finalize_exit(
|
|
exit_tx: oneshot::Sender<i32>,
|
|
process_handle: Arc<StdMutex<Option<HANDLE>>>,
|
|
thread_handle: HANDLE,
|
|
output_join: std::thread::JoinHandle<()>,
|
|
logs_base_dir: Option<&Path>,
|
|
command: Vec<String>,
|
|
) {
|
|
let exit_code = {
|
|
let mut raw_exit = 1u32;
|
|
if let Ok(guard) = process_handle.lock()
|
|
&& let Some(handle) = guard.as_ref()
|
|
{
|
|
unsafe {
|
|
WaitForSingleObject(*handle, INFINITE);
|
|
GetExitCodeProcess(*handle, &mut raw_exit);
|
|
}
|
|
}
|
|
raw_exit as i32
|
|
};
|
|
|
|
let _ = output_join.join();
|
|
let _ = exit_tx.send(exit_code);
|
|
|
|
unsafe {
|
|
if thread_handle != 0 && thread_handle != INVALID_HANDLE_VALUE {
|
|
CloseHandle(thread_handle);
|
|
}
|
|
if let Ok(mut guard) = process_handle.lock()
|
|
&& let Some(handle) = guard.take()
|
|
{
|
|
CloseHandle(handle);
|
|
}
|
|
}
|
|
|
|
if exit_code == 0 {
|
|
log_success(&command, logs_base_dir);
|
|
} else {
|
|
log_failure(&command, &format!("exit code {exit_code}"), logs_base_dir);
|
|
}
|
|
}
|
|
|
|
fn resize_conpty_handle(hpc: &Arc<StdMutex<Option<HANDLE>>>, size: TerminalSize) -> Result<()> {
|
|
let guard = hpc
|
|
.lock()
|
|
.map_err(|_| anyhow::anyhow!("failed to lock ConPTY handle"))?;
|
|
let hpc = guard
|
|
.as_ref()
|
|
.copied()
|
|
.ok_or_else(|| anyhow::anyhow!("process is not attached to a PTY"))?;
|
|
let result = unsafe {
|
|
ResizePseudoConsole(
|
|
hpc,
|
|
COORD {
|
|
X: size.cols as i16,
|
|
Y: size.rows as i16,
|
|
},
|
|
)
|
|
};
|
|
if result == 0 {
|
|
Ok(())
|
|
} else {
|
|
Err(anyhow::anyhow!(
|
|
"failed to resize console: HRESULT {result}"
|
|
))
|
|
}
|
|
}
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
pub(crate) async fn spawn_windows_sandbox_session_legacy(
|
|
policy_json_or_preset: &str,
|
|
sandbox_policy_cwd: &Path,
|
|
codex_home: &Path,
|
|
command: Vec<String>,
|
|
cwd: &Path,
|
|
mut env_map: HashMap<String, String>,
|
|
timeout_ms: Option<u64>,
|
|
additional_deny_read_paths: &[AbsolutePathBuf],
|
|
additional_deny_write_paths: &[AbsolutePathBuf],
|
|
tty: bool,
|
|
stdin_open: bool,
|
|
use_private_desktop: bool,
|
|
) -> Result<SpawnedProcess> {
|
|
let common = prepare_legacy_spawn_context(
|
|
policy_json_or_preset,
|
|
sandbox_policy_cwd,
|
|
codex_home,
|
|
cwd,
|
|
&mut env_map,
|
|
&command,
|
|
SpawnPrepOptions {
|
|
inherit_path: false,
|
|
add_git_safe_directory: false,
|
|
},
|
|
)?;
|
|
if !common.policy.has_full_disk_read_access() {
|
|
anyhow::bail!("Restricted read-only access requires the elevated Windows sandbox backend");
|
|
}
|
|
// WRITE_RESTRICTED tokens consult restricting SIDs only for writes, so this
|
|
// backend cannot make capability-SID deny-read ACLs authoritative.
|
|
if !additional_deny_read_paths.is_empty() {
|
|
anyhow::bail!("deny-read overrides require the elevated Windows sandbox backend");
|
|
}
|
|
let additional_deny_write_paths = additional_deny_write_paths
|
|
.iter()
|
|
.map(AbsolutePathBuf::to_path_buf)
|
|
.collect::<Vec<_>>();
|
|
let capability_roots = legacy_session_capability_roots(
|
|
&common.policy,
|
|
sandbox_policy_cwd,
|
|
&common.current_dir,
|
|
&env_map,
|
|
codex_home,
|
|
);
|
|
let security =
|
|
prepare_legacy_session_security(&common.policy, codex_home, cwd, capability_roots)?;
|
|
allow_null_device_for_workspace_write(common.uses_write_capabilities);
|
|
|
|
apply_legacy_session_acl_rules(
|
|
&common.permissions,
|
|
codex_home,
|
|
&common.current_dir,
|
|
&env_map,
|
|
&[],
|
|
&additional_deny_write_paths,
|
|
LegacyAclSids {
|
|
readonly_sid: security.readonly_sid.as_ref(),
|
|
readonly_sid_str: security.readonly_sid_str.as_deref(),
|
|
write_root_sids: &security.write_root_sids,
|
|
},
|
|
)?;
|
|
|
|
let (writer_tx, writer_rx) = mpsc::channel::<Vec<u8>>(128);
|
|
let (stdout_tx, stdout_rx) = broadcast::channel::<Vec<u8>>(256);
|
|
let stderr_rx = if tty {
|
|
None
|
|
} else {
|
|
Some(broadcast::channel::<Vec<u8>>(256))
|
|
};
|
|
let (exit_tx, exit_rx) = oneshot::channel::<i32>();
|
|
|
|
let LegacyProcessHandles {
|
|
process: pi,
|
|
output_join,
|
|
writer_handle,
|
|
hpc,
|
|
mut conpty_owner,
|
|
token_handle,
|
|
desktop,
|
|
} = match spawn_legacy_process(
|
|
security.h_token,
|
|
&command,
|
|
cwd,
|
|
&env_map,
|
|
use_private_desktop,
|
|
tty,
|
|
stdin_open,
|
|
stdout_tx,
|
|
stderr_rx.as_ref().map(|(tx, _rx)| tx.clone()),
|
|
writer_rx,
|
|
common.logs_base_dir.as_deref(),
|
|
) {
|
|
Ok(handles) => handles,
|
|
Err(err) => {
|
|
unsafe {
|
|
CloseHandle(security.h_token);
|
|
}
|
|
return Err(err);
|
|
}
|
|
};
|
|
let hpc_handle = hpc.map(|hpc| Arc::new(StdMutex::new(Some(hpc))));
|
|
|
|
let process_handle = Arc::new(StdMutex::new(Some(pi.hProcess)));
|
|
let wait_handle = Arc::clone(&process_handle);
|
|
let command_for_wait = command.clone();
|
|
let hpc_for_wait = hpc_handle.clone();
|
|
std::thread::spawn(move || {
|
|
let _desktop = desktop;
|
|
let timeout = timeout_ms.map(|ms| ms as u32).unwrap_or(INFINITE);
|
|
let wait_res = unsafe { WaitForSingleObject(pi.hProcess, timeout) };
|
|
if wait_res == WAIT_TIMEOUT {
|
|
unsafe {
|
|
if let Ok(guard) = wait_handle.lock()
|
|
&& let Some(handle) = guard.as_ref()
|
|
{
|
|
let _ = TerminateProcess(*handle, 1);
|
|
}
|
|
}
|
|
}
|
|
if let Some(hpc) = hpc_for_wait
|
|
&& let Ok(mut guard) = hpc.lock()
|
|
{
|
|
let _ = guard.take();
|
|
}
|
|
drop(conpty_owner.take());
|
|
unsafe {
|
|
if token_handle != 0 && token_handle != INVALID_HANDLE_VALUE {
|
|
CloseHandle(token_handle);
|
|
}
|
|
}
|
|
finalize_exit(
|
|
exit_tx,
|
|
wait_handle,
|
|
pi.hThread,
|
|
output_join,
|
|
common.logs_base_dir.as_deref(),
|
|
command_for_wait,
|
|
);
|
|
});
|
|
|
|
let terminator = {
|
|
let process_handle = Arc::clone(&process_handle);
|
|
Some(Box::new(move || {
|
|
if let Ok(guard) = process_handle.lock()
|
|
&& let Some(handle) = guard.as_ref()
|
|
{
|
|
unsafe {
|
|
let _ = TerminateProcess(*handle, 1);
|
|
}
|
|
}
|
|
}) as Box<dyn FnMut() + Send + Sync>)
|
|
};
|
|
|
|
let driver = ProcessDriver {
|
|
writer_tx,
|
|
stdout_rx,
|
|
stderr_rx: stderr_rx.map(|(_tx, rx)| rx),
|
|
exit_rx,
|
|
terminator,
|
|
writer_handle: Some(writer_handle),
|
|
resizer: hpc_handle.map(|hpc| {
|
|
Box::new(move |size| resize_conpty_handle(&hpc, size))
|
|
as Box<dyn FnMut(TerminalSize) -> Result<()> + Send>
|
|
}),
|
|
};
|
|
|
|
Ok(finish_driver_spawn(driver, stdin_open))
|
|
}
|