mirror of
https://github.com/openai/codex.git
synced 2026-04-19 14:01:44 +03:00
Compare commits
1 Commits
dev/flaky-
...
dev/icewea
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8874c898c9 |
2
codex-rs/Cargo.lock
generated
2
codex-rs/Cargo.lock
generated
@@ -2633,6 +2633,7 @@ dependencies = [
|
||||
"chrono",
|
||||
"codex-protocol",
|
||||
"codex-utils-absolute-path",
|
||||
"codex-utils-pty",
|
||||
"codex-utils-string",
|
||||
"dirs-next",
|
||||
"dunce",
|
||||
@@ -2641,6 +2642,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tempfile",
|
||||
"tokio",
|
||||
"windows 0.58.0",
|
||||
"windows-sys 0.52.0",
|
||||
"winres",
|
||||
|
||||
@@ -198,7 +198,7 @@ impl<'a> ToolRuntime<UnifiedExecRequest, UnifiedExecProcess> for UnifiedExecRunt
|
||||
.env_for(spec, req.network.as_ref())
|
||||
.map_err(|err| ToolError::Codex(err.into()))?;
|
||||
self.manager
|
||||
.open_session_with_exec_env(&exec_env, req.tty)
|
||||
.open_session_with_exec_env(&exec_env, req.tty, attempt.policy, attempt.sandbox_cwd)
|
||||
.await
|
||||
.map_err(|err| match err {
|
||||
UnifiedExecError::SandboxDenied { output, .. } => {
|
||||
|
||||
@@ -524,34 +524,88 @@ impl UnifiedExecProcessManager {
|
||||
);
|
||||
}
|
||||
|
||||
/// Spawn a unified_exec session from a fully prepared exec environment.
|
||||
pub(crate) async fn open_session_with_exec_env(
|
||||
&self,
|
||||
env: &ExecRequest,
|
||||
tty: bool,
|
||||
policy: &SandboxPolicy,
|
||||
sandbox_policy_cwd: &std::path::Path,
|
||||
) -> Result<UnifiedExecProcess, UnifiedExecError> {
|
||||
let (program, args) = env
|
||||
.command
|
||||
.split_first()
|
||||
.ok_or(UnifiedExecError::MissingCommandLine)?;
|
||||
|
||||
let spawn_result = if tty {
|
||||
codex_utils_pty::pty::spawn_process(
|
||||
program,
|
||||
args,
|
||||
env.cwd.as_path(),
|
||||
&env.env,
|
||||
&env.arg0,
|
||||
)
|
||||
.await
|
||||
} else {
|
||||
codex_utils_pty::pipe::spawn_process_no_stdin(
|
||||
program,
|
||||
args,
|
||||
env.cwd.as_path(),
|
||||
&env.env,
|
||||
&env.arg0,
|
||||
)
|
||||
.await
|
||||
let spawn_result = {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
if env.sandbox == crate::exec::SandboxType::WindowsRestrictedToken {
|
||||
let policy_json = serde_json::to_string(policy).map_err(|err| {
|
||||
UnifiedExecError::create_process(format!(
|
||||
"failed to serialize Windows sandbox policy: {err}"
|
||||
))
|
||||
})?;
|
||||
let codex_home = crate::config::find_codex_home().map_err(|err| {
|
||||
UnifiedExecError::create_process(format!(
|
||||
"windows sandbox: failed to resolve codex_home: {err}"
|
||||
))
|
||||
})?;
|
||||
let spawned = match env.windows_sandbox_level {
|
||||
codex_protocol::config_types::WindowsSandboxLevel::Elevated => {
|
||||
codex_windows_sandbox::spawn_windows_sandbox_session_elevated(
|
||||
policy_json.as_str(),
|
||||
sandbox_policy_cwd,
|
||||
codex_home.as_ref(),
|
||||
env.command.clone(),
|
||||
env.cwd.as_path(),
|
||||
env.env.clone(),
|
||||
None,
|
||||
tty,
|
||||
)
|
||||
.await
|
||||
}
|
||||
_ => {
|
||||
codex_windows_sandbox::spawn_windows_sandbox_session_legacy(
|
||||
policy_json.as_str(),
|
||||
sandbox_policy_cwd,
|
||||
codex_home.as_ref(),
|
||||
env.command.clone(),
|
||||
env.cwd.as_path(),
|
||||
env.env.clone(),
|
||||
None,
|
||||
tty,
|
||||
)
|
||||
.await
|
||||
}
|
||||
};
|
||||
return UnifiedExecProcess::from_spawned(
|
||||
spawned.map_err(|err| UnifiedExecError::create_process(err.to_string()))?,
|
||||
env.sandbox,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
if tty {
|
||||
codex_utils_pty::pty::spawn_process(
|
||||
program,
|
||||
args,
|
||||
env.cwd.as_path(),
|
||||
&env.env,
|
||||
&env.arg0,
|
||||
)
|
||||
.await
|
||||
} else {
|
||||
codex_utils_pty::pipe::spawn_process_no_stdin(
|
||||
program,
|
||||
args,
|
||||
env.cwd.as_path(),
|
||||
&env.env,
|
||||
&env.arg0,
|
||||
)
|
||||
.await
|
||||
}
|
||||
};
|
||||
let spawned =
|
||||
spawn_result.map_err(|err| UnifiedExecError::create_process(err.to_string()))?;
|
||||
|
||||
@@ -13,8 +13,10 @@ pub use pipe::spawn_process as spawn_pipe_process;
|
||||
pub use pipe::spawn_process_no_stdin as spawn_pipe_process_no_stdin;
|
||||
/// Handle for interacting with a spawned process (PTY or pipe).
|
||||
pub use process::ProcessHandle;
|
||||
pub use process::ProcessDriver;
|
||||
/// Bundle of process handles plus output and exit receivers returned by spawn helpers.
|
||||
pub use process::SpawnedProcess;
|
||||
pub use process::spawn_from_driver;
|
||||
/// Backwards-compatible alias for ProcessHandle.
|
||||
pub type ExecCommandSession = ProcessHandle;
|
||||
/// Backwards-compatible alias for SpawnedProcess.
|
||||
|
||||
@@ -138,6 +138,29 @@ impl Drop for ProcessHandle {
|
||||
}
|
||||
}
|
||||
|
||||
/// Adapts a closure into a `ChildTerminator` implementation.
|
||||
struct ClosureTerminator {
|
||||
inner: Option<Box<dyn FnMut() + Send + Sync>>,
|
||||
}
|
||||
|
||||
impl ChildTerminator for ClosureTerminator {
|
||||
fn kill(&mut self) -> io::Result<()> {
|
||||
if let Some(inner) = self.inner.as_mut() {
|
||||
(inner)();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Driver-backed process handles for non-standard spawn backends.
|
||||
pub struct ProcessDriver {
|
||||
pub writer_tx: mpsc::Sender<Vec<u8>>,
|
||||
pub output_rx: broadcast::Receiver<Vec<u8>>,
|
||||
pub exit_rx: oneshot::Receiver<i32>,
|
||||
pub terminator: Option<Box<dyn FnMut() + Send + Sync>>,
|
||||
pub writer_handle: Option<JoinHandle<()>>,
|
||||
}
|
||||
|
||||
/// Return value from spawn helpers (PTY or pipe).
|
||||
#[derive(Debug)]
|
||||
pub struct SpawnedProcess {
|
||||
@@ -145,3 +168,66 @@ pub struct SpawnedProcess {
|
||||
pub output_rx: broadcast::Receiver<Vec<u8>>,
|
||||
pub exit_rx: oneshot::Receiver<i32>,
|
||||
}
|
||||
|
||||
/// Build a `SpawnedProcess` from a driver that supplies stdin/output/exit channels.
|
||||
pub fn spawn_from_driver(driver: ProcessDriver) -> SpawnedProcess {
|
||||
let ProcessDriver {
|
||||
writer_tx,
|
||||
mut output_rx,
|
||||
exit_rx,
|
||||
terminator,
|
||||
writer_handle,
|
||||
} = driver;
|
||||
|
||||
let (output_tx, _) = broadcast::channel::<Vec<u8>>(256);
|
||||
let initial_output_rx = output_tx.subscribe();
|
||||
let output_tx_clone = output_tx.clone();
|
||||
let reader_handle = tokio::spawn(async move {
|
||||
loop {
|
||||
match output_rx.recv().await {
|
||||
Ok(chunk) => {
|
||||
let _ = output_tx_clone.send(chunk);
|
||||
}
|
||||
Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => continue,
|
||||
Err(tokio::sync::broadcast::error::RecvError::Closed) => break,
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let writer_handle = writer_handle.unwrap_or_else(|| tokio::spawn(async {}));
|
||||
|
||||
let (exit_tx, exit_rx_out) = oneshot::channel::<i32>();
|
||||
let exit_status = Arc::new(AtomicBool::new(false));
|
||||
let wait_exit_status = Arc::clone(&exit_status);
|
||||
let exit_code = Arc::new(StdMutex::new(None));
|
||||
let wait_exit_code = Arc::clone(&exit_code);
|
||||
let wait_handle = tokio::spawn(async move {
|
||||
let code = exit_rx.await.unwrap_or(-1);
|
||||
wait_exit_status.store(true, std::sync::atomic::Ordering::SeqCst);
|
||||
if let Ok(mut guard) = wait_exit_code.lock() {
|
||||
*guard = Some(code);
|
||||
}
|
||||
let _ = exit_tx.send(code);
|
||||
});
|
||||
|
||||
let killer = Box::new(ClosureTerminator { inner: terminator });
|
||||
let (handle, output_rx) = ProcessHandle::new(
|
||||
writer_tx,
|
||||
output_tx,
|
||||
initial_output_rx,
|
||||
killer,
|
||||
reader_handle,
|
||||
Vec::new(),
|
||||
writer_handle,
|
||||
wait_handle,
|
||||
exit_status,
|
||||
exit_code,
|
||||
None,
|
||||
);
|
||||
|
||||
SpawnedProcess {
|
||||
session: handle,
|
||||
output_rx,
|
||||
exit_rx: exit_rx_out,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,11 +24,13 @@ chrono = { version = "0.4.42", default-features = false, features = [
|
||||
"clock",
|
||||
"std",
|
||||
] }
|
||||
codex-utils-pty = { workspace = true }
|
||||
codex-utils-absolute-path = { workspace = true }
|
||||
codex-utils-string = { workspace = true }
|
||||
dunce = "1.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
tokio = { workspace = true, features = ["sync", "rt"] }
|
||||
windows = { version = "0.58", features = [
|
||||
"Win32_Foundation",
|
||||
"Win32_NetworkManagement_WindowsFirewall",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#[path = "../command_runner_win.rs"]
|
||||
#[path = "../elevated/command_runner_win.rs"]
|
||||
mod win;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
|
||||
@@ -1,318 +0,0 @@
|
||||
#![cfg(target_os = "windows")]
|
||||
|
||||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
use codex_windows_sandbox::allow_null_device;
|
||||
use codex_windows_sandbox::convert_string_sid_to_sid;
|
||||
use codex_windows_sandbox::create_process_as_user;
|
||||
use codex_windows_sandbox::create_readonly_token_with_caps_from;
|
||||
use codex_windows_sandbox::create_workspace_write_token_with_caps_from;
|
||||
use codex_windows_sandbox::get_current_token_for_restriction;
|
||||
use codex_windows_sandbox::hide_current_user_profile_dir;
|
||||
use codex_windows_sandbox::log_note;
|
||||
use codex_windows_sandbox::parse_policy;
|
||||
use codex_windows_sandbox::to_wide;
|
||||
use codex_windows_sandbox::SandboxPolicy;
|
||||
use serde::Deserialize;
|
||||
use std::collections::HashMap;
|
||||
use std::ffi::c_void;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use windows_sys::Win32::Foundation::CloseHandle;
|
||||
use windows_sys::Win32::Foundation::GetLastError;
|
||||
use windows_sys::Win32::Foundation::LocalFree;
|
||||
use windows_sys::Win32::Foundation::HANDLE;
|
||||
use windows_sys::Win32::Foundation::HLOCAL;
|
||||
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::JobObjects::AssignProcessToJobObject;
|
||||
use windows_sys::Win32::System::JobObjects::CreateJobObjectW;
|
||||
use windows_sys::Win32::System::JobObjects::JobObjectExtendedLimitInformation;
|
||||
use windows_sys::Win32::System::JobObjects::SetInformationJobObject;
|
||||
use windows_sys::Win32::System::JobObjects::JOBOBJECT_EXTENDED_LIMIT_INFORMATION;
|
||||
use windows_sys::Win32::System::JobObjects::JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
|
||||
use windows_sys::Win32::System::Threading::TerminateProcess;
|
||||
use windows_sys::Win32::System::Threading::WaitForSingleObject;
|
||||
use windows_sys::Win32::System::Threading::INFINITE;
|
||||
|
||||
#[path = "cwd_junction.rs"]
|
||||
mod cwd_junction;
|
||||
|
||||
#[allow(dead_code)]
|
||||
mod read_acl_mutex;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct RunnerRequest {
|
||||
policy_json_or_preset: String,
|
||||
// Writable location for logs (sandbox user's .codex).
|
||||
codex_home: PathBuf,
|
||||
// Real user's CODEX_HOME for shared data (caps, config).
|
||||
real_codex_home: PathBuf,
|
||||
cap_sids: Vec<String>,
|
||||
command: Vec<String>,
|
||||
cwd: PathBuf,
|
||||
env_map: HashMap<String, String>,
|
||||
timeout_ms: Option<u64>,
|
||||
stdin_pipe: String,
|
||||
stdout_pipe: String,
|
||||
stderr_pipe: String,
|
||||
}
|
||||
|
||||
const WAIT_TIMEOUT: u32 = 0x0000_0102;
|
||||
|
||||
unsafe fn create_job_kill_on_close() -> Result<HANDLE> {
|
||||
let h = CreateJobObjectW(std::ptr::null_mut(), std::ptr::null());
|
||||
if h == 0 {
|
||||
return Err(anyhow::anyhow!("CreateJobObjectW failed"));
|
||||
}
|
||||
let mut limits: JOBOBJECT_EXTENDED_LIMIT_INFORMATION = std::mem::zeroed();
|
||||
limits.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
|
||||
let ok = SetInformationJobObject(
|
||||
h,
|
||||
JobObjectExtendedLimitInformation,
|
||||
&mut limits as *mut _ as *mut _,
|
||||
std::mem::size_of::<JOBOBJECT_EXTENDED_LIMIT_INFORMATION>() as u32,
|
||||
);
|
||||
if ok == 0 {
|
||||
return Err(anyhow::anyhow!("SetInformationJobObject failed"));
|
||||
}
|
||||
Ok(h)
|
||||
}
|
||||
|
||||
fn read_request_file(req_path: &Path) -> Result<String> {
|
||||
let content = std::fs::read_to_string(req_path)
|
||||
.with_context(|| format!("read request file {}", req_path.display()));
|
||||
let _ = std::fs::remove_file(req_path);
|
||||
content
|
||||
}
|
||||
|
||||
pub fn main() -> Result<()> {
|
||||
let mut input = String::new();
|
||||
let mut args = std::env::args().skip(1);
|
||||
if let Some(first) = args.next() {
|
||||
if let Some(rest) = first.strip_prefix("--request-file=") {
|
||||
let req_path = PathBuf::from(rest);
|
||||
input = read_request_file(&req_path)?;
|
||||
}
|
||||
}
|
||||
if input.is_empty() {
|
||||
anyhow::bail!("runner: no request-file provided");
|
||||
}
|
||||
let req: RunnerRequest = serde_json::from_str(&input).context("parse runner request json")?;
|
||||
let log_dir = Some(req.codex_home.as_path());
|
||||
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")?;
|
||||
if !policy.has_full_disk_read_access() {
|
||||
anyhow::bail!(
|
||||
"Restricted read-only access is not yet supported by the Windows sandbox backend"
|
||||
);
|
||||
}
|
||||
let mut cap_psids: Vec<*mut c_void> = Vec::new();
|
||||
for sid in &req.cap_sids {
|
||||
let Some(psid) = (unsafe { convert_string_sid_to_sid(sid) }) else {
|
||||
anyhow::bail!("ConvertStringSidToSidW failed for capability SID");
|
||||
};
|
||||
cap_psids.push(psid);
|
||||
}
|
||||
if cap_psids.is_empty() {
|
||||
anyhow::bail!("runner: empty capability SID list");
|
||||
}
|
||||
|
||||
// Create restricted token from current process token.
|
||||
let base = unsafe { get_current_token_for_restriction()? };
|
||||
let token_res: Result<HANDLE> = unsafe {
|
||||
match &policy {
|
||||
SandboxPolicy::ReadOnly { .. } => {
|
||||
create_readonly_token_with_caps_from(base, &cap_psids)
|
||||
}
|
||||
SandboxPolicy::WorkspaceWrite { .. } => {
|
||||
create_workspace_write_token_with_caps_from(base, &cap_psids)
|
||||
}
|
||||
SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. } => {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
};
|
||||
let h_token = token_res?;
|
||||
unsafe {
|
||||
CloseHandle(base);
|
||||
}
|
||||
unsafe {
|
||||
for psid in &cap_psids {
|
||||
allow_null_device(*psid);
|
||||
}
|
||||
for psid in cap_psids {
|
||||
if !psid.is_null() {
|
||||
LocalFree(psid as HLOCAL);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Open named pipes for stdio.
|
||||
let open_pipe = |name: &str, access: u32| -> Result<HANDLE> {
|
||||
let path = to_wide(name);
|
||||
let handle = unsafe {
|
||||
CreateFileW(
|
||||
path.as_ptr(),
|
||||
access,
|
||||
0,
|
||||
std::ptr::null_mut(),
|
||||
OPEN_EXISTING,
|
||||
0,
|
||||
0,
|
||||
)
|
||||
};
|
||||
if handle == windows_sys::Win32::Foundation::INVALID_HANDLE_VALUE {
|
||||
let err = unsafe { GetLastError() };
|
||||
log_note(
|
||||
&format!("CreateFileW failed for pipe {name}: {err}"),
|
||||
Some(&req.codex_home),
|
||||
);
|
||||
return Err(anyhow::anyhow!("CreateFileW failed for pipe {name}: {err}"));
|
||||
}
|
||||
Ok(handle)
|
||||
};
|
||||
let h_stdin = open_pipe(&req.stdin_pipe, FILE_GENERIC_READ)?;
|
||||
let h_stdout = open_pipe(&req.stdout_pipe, FILE_GENERIC_WRITE)?;
|
||||
let h_stderr = open_pipe(&req.stderr_pipe, FILE_GENERIC_WRITE)?;
|
||||
let stdio = Some((h_stdin, h_stdout, h_stderr));
|
||||
|
||||
// While the read-ACL helper is running, PowerShell can fail to start in the requested CWD due
|
||||
// to unreadable ancestors. Use a junction CWD for that window; once the helper finishes, go
|
||||
// back to using the real requested CWD (no probing, no extra state).
|
||||
let use_junction = match read_acl_mutex::read_acl_mutex_exists() {
|
||||
Ok(exists) => exists,
|
||||
Err(err) => {
|
||||
// Fail-safe: if we can't determine the state, assume the helper might be running and
|
||||
// use the junction path to avoid CWD failures on unreadable ancestors.
|
||||
log_note(
|
||||
&format!("junction: read_acl_mutex_exists failed: {err}; assuming read ACL helper is running"),
|
||||
log_dir,
|
||||
);
|
||||
true
|
||||
}
|
||||
};
|
||||
if use_junction {
|
||||
log_note(
|
||||
"junction: read ACL helper running; using junction CWD",
|
||||
log_dir,
|
||||
);
|
||||
}
|
||||
let effective_cwd = if use_junction {
|
||||
cwd_junction::create_cwd_junction(&req.cwd, log_dir).unwrap_or_else(|| req.cwd.clone())
|
||||
} else {
|
||||
req.cwd.clone()
|
||||
};
|
||||
log_note(
|
||||
&format!(
|
||||
"runner: effective cwd={} (requested {})",
|
||||
effective_cwd.display(),
|
||||
req.cwd.display()
|
||||
),
|
||||
log_dir,
|
||||
);
|
||||
|
||||
// Build command and env, spawn with CreateProcessAsUserW.
|
||||
let spawn_result = unsafe {
|
||||
create_process_as_user(
|
||||
h_token,
|
||||
&req.command,
|
||||
&effective_cwd,
|
||||
&req.env_map,
|
||||
Some(&req.codex_home),
|
||||
stdio,
|
||||
)
|
||||
};
|
||||
let (proc_info, _si) = match spawn_result {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
log_note(&format!("runner: spawn failed: {e:?}"), log_dir);
|
||||
unsafe {
|
||||
CloseHandle(h_stdin);
|
||||
CloseHandle(h_stdout);
|
||||
CloseHandle(h_stderr);
|
||||
CloseHandle(h_token);
|
||||
}
|
||||
return Err(e);
|
||||
}
|
||||
};
|
||||
|
||||
// Optional job kill on close.
|
||||
let h_job = unsafe { create_job_kill_on_close().ok() };
|
||||
if let Some(job) = h_job {
|
||||
unsafe {
|
||||
let _ = AssignProcessToJobObject(job, proc_info.hProcess);
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for process.
|
||||
let wait_res = unsafe {
|
||||
WaitForSingleObject(
|
||||
proc_info.hProcess,
|
||||
req.timeout_ms.map(|ms| ms as u32).unwrap_or(INFINITE),
|
||||
)
|
||||
};
|
||||
let timed_out = wait_res == WAIT_TIMEOUT;
|
||||
|
||||
let exit_code: i32;
|
||||
unsafe {
|
||||
if timed_out {
|
||||
let _ = TerminateProcess(proc_info.hProcess, 1);
|
||||
exit_code = 128 + 64;
|
||||
} else {
|
||||
let mut raw_exit: u32 = 1;
|
||||
windows_sys::Win32::System::Threading::GetExitCodeProcess(
|
||||
proc_info.hProcess,
|
||||
&mut raw_exit,
|
||||
);
|
||||
exit_code = raw_exit as i32;
|
||||
}
|
||||
if proc_info.hThread != 0 {
|
||||
CloseHandle(proc_info.hThread);
|
||||
}
|
||||
if proc_info.hProcess != 0 {
|
||||
CloseHandle(proc_info.hProcess);
|
||||
}
|
||||
CloseHandle(h_stdin);
|
||||
CloseHandle(h_stdout);
|
||||
CloseHandle(h_stderr);
|
||||
CloseHandle(h_token);
|
||||
if let Some(job) = h_job {
|
||||
CloseHandle(job);
|
||||
}
|
||||
}
|
||||
if exit_code != 0 {
|
||||
eprintln!("runner child exited with code {}", exit_code);
|
||||
}
|
||||
std::process::exit(exit_code);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::read_request_file;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::fs;
|
||||
|
||||
#[test]
|
||||
fn removes_request_file_after_read() {
|
||||
let dir = tempfile::tempdir().expect("tempdir");
|
||||
let req_path = dir.path().join("request.json");
|
||||
fs::write(&req_path, "{\"ok\":true}").expect("write request");
|
||||
|
||||
let content = read_request_file(&req_path).expect("read request");
|
||||
assert_eq!(content, "{\"ok\":true}");
|
||||
assert!(!req_path.exists(), "request file should be removed");
|
||||
}
|
||||
}
|
||||
171
codex-rs/windows-sandbox-rs/src/conpty/mod.rs
Normal file
171
codex-rs/windows-sandbox-rs/src/conpty/mod.rs
Normal file
@@ -0,0 +1,171 @@
|
||||
//! ConPTY helpers for spawning sandboxed processes with a PTY on Windows.
|
||||
//!
|
||||
//! This module encapsulates ConPTY creation and process spawn with the required
|
||||
//! `PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE` plumbing. It is shared by both the legacy
|
||||
//! restricted‑token path and the elevated runner path when unified_exec runs with
|
||||
//! `tty=true`. The helpers are not tied to the IPC layer and can be reused by other
|
||||
//! Windows sandbox flows that need a PTY.
|
||||
|
||||
mod proc_thread_attr;
|
||||
|
||||
use self::proc_thread_attr::ProcThreadAttributeList;
|
||||
use crate::winutil::format_last_error;
|
||||
use crate::winutil::quote_windows_arg;
|
||||
use crate::winutil::to_wide;
|
||||
use anyhow::Result;
|
||||
use std::collections::HashMap;
|
||||
use std::ffi::c_void;
|
||||
use std::path::Path;
|
||||
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::System::Console::ClosePseudoConsole;
|
||||
use windows_sys::Win32::System::Console::CreatePseudoConsole;
|
||||
use windows_sys::Win32::System::Console::COORD;
|
||||
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::EXTENDED_STARTUPINFO_PRESENT;
|
||||
use windows_sys::Win32::System::Threading::PROCESS_INFORMATION;
|
||||
use windows_sys::Win32::System::Threading::STARTF_USESTDHANDLES;
|
||||
use windows_sys::Win32::System::Threading::STARTUPINFOEXW;
|
||||
|
||||
use crate::process::make_env_block;
|
||||
|
||||
/// Owns a ConPTY handle and its backing pipe handles.
|
||||
pub struct ConptyInstance {
|
||||
pub hpc: HANDLE,
|
||||
pub input_write: HANDLE,
|
||||
pub output_read: HANDLE,
|
||||
}
|
||||
|
||||
impl Drop for ConptyInstance {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
if self.input_write != 0 && self.input_write != INVALID_HANDLE_VALUE {
|
||||
CloseHandle(self.input_write);
|
||||
}
|
||||
if self.output_read != 0 && self.output_read != INVALID_HANDLE_VALUE {
|
||||
CloseHandle(self.output_read);
|
||||
}
|
||||
if self.hpc != 0 && self.hpc != INVALID_HANDLE_VALUE {
|
||||
ClosePseudoConsole(self.hpc);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ConptyInstance {
|
||||
/// Consume the instance and return raw handles without closing them.
|
||||
pub fn into_raw(self) -> (HANDLE, HANDLE, HANDLE) {
|
||||
let me = std::mem::ManuallyDrop::new(self);
|
||||
(me.hpc, me.input_write, me.output_read)
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a ConPTY with backing pipes.
|
||||
/// weasel: does this have to be public?
|
||||
pub fn create_conpty(cols: i16, rows: i16) -> Result<ConptyInstance> {
|
||||
let mut in_read: HANDLE = 0;
|
||||
let mut in_write: HANDLE = 0;
|
||||
let mut out_read: HANDLE = 0;
|
||||
let mut out_write: HANDLE = 0;
|
||||
unsafe {
|
||||
if CreatePipe(&mut in_read, &mut in_write, std::ptr::null_mut(), 0) == 0 {
|
||||
return Err(anyhow::anyhow!(
|
||||
"CreatePipe stdin failed: {}",
|
||||
GetLastError()
|
||||
));
|
||||
}
|
||||
if CreatePipe(&mut out_read, &mut out_write, std::ptr::null_mut(), 0) == 0 {
|
||||
CloseHandle(in_read);
|
||||
CloseHandle(in_write);
|
||||
return Err(anyhow::anyhow!(
|
||||
"CreatePipe stdout failed: {}",
|
||||
GetLastError()
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
let mut hpc: HANDLE = 0;
|
||||
let size = COORD { X: cols, Y: rows };
|
||||
let hr = unsafe { CreatePseudoConsole(size, in_read, out_write, 0, &mut hpc) };
|
||||
unsafe {
|
||||
CloseHandle(in_read);
|
||||
CloseHandle(out_write);
|
||||
}
|
||||
if hr != 0 {
|
||||
unsafe {
|
||||
CloseHandle(in_write);
|
||||
CloseHandle(out_read);
|
||||
}
|
||||
return Err(anyhow::anyhow!("CreatePseudoConsole failed: {}", hr));
|
||||
}
|
||||
|
||||
Ok(ConptyInstance {
|
||||
hpc,
|
||||
input_write: in_write,
|
||||
output_read: out_read,
|
||||
})
|
||||
}
|
||||
|
||||
/// Spawn a process under `h_token` with ConPTY attached.
|
||||
/// weasel: this is the main entry point for conpty? I assume this is called for legacy and elevated paths
|
||||
pub fn spawn_conpty_process_as_user(
|
||||
h_token: HANDLE,
|
||||
argv: &[String],
|
||||
cwd: &Path,
|
||||
env_map: &HashMap<String, String>,
|
||||
) -> Result<(PROCESS_INFORMATION, ConptyInstance)> {
|
||||
let cmdline_str = argv
|
||||
.iter()
|
||||
.map(|arg| quote_windows_arg(arg))
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ");
|
||||
let mut cmdline: Vec<u16> = to_wide(&cmdline_str);
|
||||
let env_block = make_env_block(env_map);
|
||||
let mut si: STARTUPINFOEXW = unsafe { std::mem::zeroed() };
|
||||
si.StartupInfo.cb = std::mem::size_of::<STARTUPINFOEXW>() as u32;
|
||||
si.StartupInfo.dwFlags = STARTF_USESTDHANDLES;
|
||||
si.StartupInfo.hStdInput = INVALID_HANDLE_VALUE;
|
||||
si.StartupInfo.hStdOutput = INVALID_HANDLE_VALUE;
|
||||
si.StartupInfo.hStdError = INVALID_HANDLE_VALUE;
|
||||
let desktop = to_wide("Winsta0\\Default");
|
||||
si.StartupInfo.lpDesktop = desktop.as_ptr() as *mut u16;
|
||||
|
||||
let conpty = create_conpty(80, 24)?;
|
||||
let mut attrs = ProcThreadAttributeList::new(1)?;
|
||||
attrs.set_pseudoconsole(conpty.hpc)?;
|
||||
si.lpAttributeList = attrs.as_mut_ptr();
|
||||
|
||||
let mut pi: PROCESS_INFORMATION = unsafe { std::mem::zeroed() };
|
||||
let ok = unsafe {
|
||||
// weasel: does this mean CreatepProcessAsUserW moved from old locations to here? This is called from the CLI (legacy) and also the runner (elevated) right?
|
||||
CreateProcessAsUserW(
|
||||
h_token,
|
||||
std::ptr::null(),
|
||||
cmdline.as_mut_ptr(),
|
||||
std::ptr::null_mut(),
|
||||
std::ptr::null_mut(),
|
||||
0,
|
||||
EXTENDED_STARTUPINFO_PRESENT | CREATE_UNICODE_ENVIRONMENT,
|
||||
env_block.as_ptr() as *mut c_void,
|
||||
to_wide(cwd).as_ptr(),
|
||||
&si.StartupInfo,
|
||||
&mut pi,
|
||||
)
|
||||
};
|
||||
if ok == 0 {
|
||||
let err = unsafe { GetLastError() } as i32;
|
||||
return Err(anyhow::anyhow!(
|
||||
"CreateProcessAsUserW failed: {} ({}) | cwd={} | cmd={} | env_u16_len={}",
|
||||
err,
|
||||
format_last_error(err),
|
||||
cwd.display(),
|
||||
cmdline_str,
|
||||
env_block.len()
|
||||
));
|
||||
}
|
||||
Ok((pi, conpty))
|
||||
}
|
||||
79
codex-rs/windows-sandbox-rs/src/conpty/proc_thread_attr.rs
Normal file
79
codex-rs/windows-sandbox-rs/src/conpty/proc_thread_attr.rs
Normal file
@@ -0,0 +1,79 @@
|
||||
//weasel: what is this module for? Why is it separate from conpty.rs ?
|
||||
//! Low-level Windows thread attribute helpers used by ConPTY spawn.
|
||||
//!
|
||||
//! This module wraps the Win32 `PROC_THREAD_ATTRIBUTE_LIST` APIs so ConPTY handles can
|
||||
//! be attached to a child process. It is ConPTY‑specific and used in both legacy and
|
||||
//! elevated unified_exec paths when spawning a PTY‑backed process.
|
||||
|
||||
use std::io;
|
||||
use windows_sys::Win32::Foundation::GetLastError;
|
||||
use windows_sys::Win32::System::Threading::DeleteProcThreadAttributeList;
|
||||
use windows_sys::Win32::System::Threading::InitializeProcThreadAttributeList;
|
||||
use windows_sys::Win32::System::Threading::UpdateProcThreadAttribute;
|
||||
use windows_sys::Win32::System::Threading::LPPROC_THREAD_ATTRIBUTE_LIST;
|
||||
|
||||
const PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE: usize = 0x00020016;
|
||||
|
||||
/// RAII wrapper for Windows PROC_THREAD_ATTRIBUTE_LIST.
|
||||
pub struct ProcThreadAttributeList {
|
||||
buffer: Vec<u8>,
|
||||
}
|
||||
|
||||
impl ProcThreadAttributeList {
|
||||
/// Allocate and initialize a thread attribute list.
|
||||
pub fn new(attr_count: u32) -> io::Result<Self> {
|
||||
let mut size: usize = 0;
|
||||
unsafe {
|
||||
InitializeProcThreadAttributeList(std::ptr::null_mut(), attr_count, 0, &mut size);
|
||||
}
|
||||
if size == 0 {
|
||||
return Err(io::Error::from_raw_os_error(unsafe {
|
||||
GetLastError() as i32
|
||||
}));
|
||||
}
|
||||
let mut buffer = vec![0u8; size];
|
||||
let list = buffer.as_mut_ptr() as LPPROC_THREAD_ATTRIBUTE_LIST;
|
||||
let ok = unsafe { InitializeProcThreadAttributeList(list, attr_count, 0, &mut size) };
|
||||
if ok == 0 {
|
||||
return Err(io::Error::from_raw_os_error(unsafe {
|
||||
GetLastError() as i32
|
||||
}));
|
||||
}
|
||||
Ok(Self { buffer })
|
||||
}
|
||||
|
||||
/// Return a mutable pointer to the attribute list for Win32 APIs.
|
||||
pub fn as_mut_ptr(&mut self) -> LPPROC_THREAD_ATTRIBUTE_LIST {
|
||||
self.buffer.as_mut_ptr() as LPPROC_THREAD_ATTRIBUTE_LIST
|
||||
}
|
||||
|
||||
/// Attach a ConPTY handle to the attribute list.
|
||||
pub fn set_pseudoconsole(&mut self, hpc: isize) -> io::Result<()> {
|
||||
let list = self.as_mut_ptr();
|
||||
let ok = unsafe {
|
||||
UpdateProcThreadAttribute(
|
||||
list,
|
||||
0,
|
||||
PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE,
|
||||
hpc as *mut _,
|
||||
std::mem::size_of::<isize>(),
|
||||
std::ptr::null_mut(),
|
||||
std::ptr::null_mut(),
|
||||
)
|
||||
};
|
||||
if ok == 0 {
|
||||
return Err(io::Error::from_raw_os_error(unsafe {
|
||||
GetLastError() as i32
|
||||
}));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for ProcThreadAttributeList {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
DeleteProcThreadAttributeList(self.as_mut_ptr());
|
||||
}
|
||||
}
|
||||
}
|
||||
482
codex-rs/windows-sandbox-rs/src/elevated/command_runner_win.rs
Normal file
482
codex-rs/windows-sandbox-rs/src/elevated/command_runner_win.rs
Normal file
@@ -0,0 +1,482 @@
|
||||
//! Windows command runner used by the **elevated** sandbox path.
|
||||
//!
|
||||
//! The CLI launches this binary under the sandbox user when Windows sandbox level is
|
||||
//! Elevated. It connects to the IPC pipes, reads the framed `SpawnRequest`, derives a
|
||||
//! restricted token from the sandbox user, and spawns the child process via ConPTY
|
||||
//! (`tty=true`) or pipes (`tty=false`). It then streams output frames back to the parent,
|
||||
//! accepts stdin/terminate frames, and emits a final exit frame. The legacy restricted‑token
|
||||
//! path spawns the child directly and does not use this runner.
|
||||
|
||||
#![cfg(target_os = "windows")]
|
||||
|
||||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
use codex_windows_sandbox::allow_null_device;
|
||||
use codex_windows_sandbox::convert_string_sid_to_sid;
|
||||
use codex_windows_sandbox::create_readonly_token_with_cap_from;
|
||||
use codex_windows_sandbox::create_workspace_write_token_with_cap_from;
|
||||
use codex_windows_sandbox::get_current_token_for_restriction;
|
||||
use codex_windows_sandbox::hide_current_user_profile_dir;
|
||||
use codex_windows_sandbox::ipc_framed::decode_bytes;
|
||||
use codex_windows_sandbox::ipc_framed::encode_bytes;
|
||||
use codex_windows_sandbox::ipc_framed::read_frame;
|
||||
use codex_windows_sandbox::ipc_framed::write_frame;
|
||||
use codex_windows_sandbox::ipc_framed::ErrorPayload;
|
||||
use codex_windows_sandbox::ipc_framed::ExitPayload;
|
||||
use codex_windows_sandbox::ipc_framed::FramedMessage;
|
||||
use codex_windows_sandbox::ipc_framed::Message;
|
||||
use codex_windows_sandbox::ipc_framed::OutputPayload;
|
||||
use codex_windows_sandbox::ipc_framed::OutputStream;
|
||||
use codex_windows_sandbox::log_note;
|
||||
use codex_windows_sandbox::parse_policy;
|
||||
use codex_windows_sandbox::read_handle_loop;
|
||||
use codex_windows_sandbox::spawn_process_with_pipes;
|
||||
use codex_windows_sandbox::to_wide;
|
||||
use codex_windows_sandbox::PipeSpawnHandles;
|
||||
use codex_windows_sandbox::SandboxPolicy;
|
||||
use codex_windows_sandbox::StderrMode;
|
||||
use codex_windows_sandbox::StdinMode;
|
||||
use std::ffi::c_void;
|
||||
use std::fs::File;
|
||||
use std::os::windows::io::FromRawHandle;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::ptr;
|
||||
use std::sync::Arc;
|
||||
use std::sync::Mutex as StdMutex;
|
||||
use windows_sys::Win32::Foundation::CloseHandle;
|
||||
use windows_sys::Win32::Foundation::GetLastError;
|
||||
use windows_sys::Win32::Foundation::HANDLE;
|
||||
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::ClosePseudoConsole;
|
||||
use windows_sys::Win32::System::JobObjects::AssignProcessToJobObject;
|
||||
use windows_sys::Win32::System::JobObjects::CreateJobObjectW;
|
||||
use windows_sys::Win32::System::JobObjects::JobObjectExtendedLimitInformation;
|
||||
use windows_sys::Win32::System::JobObjects::SetInformationJobObject;
|
||||
use windows_sys::Win32::System::JobObjects::JOBOBJECT_EXTENDED_LIMIT_INFORMATION;
|
||||
use windows_sys::Win32::System::JobObjects::JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
|
||||
use windows_sys::Win32::System::Threading::GetExitCodeProcess;
|
||||
use windows_sys::Win32::System::Threading::GetProcessId;
|
||||
use windows_sys::Win32::System::Threading::TerminateProcess;
|
||||
use windows_sys::Win32::System::Threading::WaitForSingleObject;
|
||||
use windows_sys::Win32::System::Threading::INFINITE;
|
||||
|
||||
#[path = "cwd_junction.rs"]
|
||||
mod cwd_junction;
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[path = "../read_acl_mutex.rs"]
|
||||
mod read_acl_mutex;
|
||||
|
||||
const WAIT_TIMEOUT: u32 = 0x0000_0102;
|
||||
|
||||
unsafe fn create_job_kill_on_close() -> Result<HANDLE> {
|
||||
let h = CreateJobObjectW(std::ptr::null_mut(), std::ptr::null());
|
||||
if h == 0 {
|
||||
return Err(anyhow::anyhow!("CreateJobObjectW failed"));
|
||||
}
|
||||
let mut limits: JOBOBJECT_EXTENDED_LIMIT_INFORMATION = std::mem::zeroed();
|
||||
limits.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
|
||||
let ok = SetInformationJobObject(
|
||||
h,
|
||||
JobObjectExtendedLimitInformation,
|
||||
&mut limits as *mut _ as *mut _,
|
||||
std::mem::size_of::<JOBOBJECT_EXTENDED_LIMIT_INFORMATION>() as u32,
|
||||
);
|
||||
if ok == 0 {
|
||||
return Err(anyhow::anyhow!("SetInformationJobObject failed"));
|
||||
}
|
||||
Ok(h)
|
||||
}
|
||||
|
||||
/// Open a named pipe created by the parent process.
|
||||
/// weasel: seems like most of the changes in this file are for unified_exec, is that right? The new methods seem to be around supporting the "interactive" aspect of reading input and writing output to the pipe that the parent (CLI) can read
|
||||
fn open_pipe(name: &str, access: u32) -> Result<HANDLE> {
|
||||
let path = to_wide(name);
|
||||
let handle = unsafe {
|
||||
CreateFileW(
|
||||
path.as_ptr(),
|
||||
access,
|
||||
0,
|
||||
std::ptr::null_mut(),
|
||||
OPEN_EXISTING,
|
||||
0,
|
||||
0,
|
||||
)
|
||||
};
|
||||
if handle == windows_sys::Win32::Foundation::INVALID_HANDLE_VALUE {
|
||||
let err = unsafe { GetLastError() };
|
||||
return Err(anyhow::anyhow!("CreateFileW failed for pipe {name}: {err}"));
|
||||
}
|
||||
Ok(handle)
|
||||
}
|
||||
|
||||
/// Send an error frame back to the parent process.
|
||||
fn send_error(writer: &Arc<StdMutex<File>>, code: &str, message: String) -> Result<()> {
|
||||
let msg = FramedMessage {
|
||||
version: 1,
|
||||
message: Message::Error {
|
||||
payload: ErrorPayload {
|
||||
message,
|
||||
code: code.to_string(),
|
||||
},
|
||||
},
|
||||
};
|
||||
if let Ok(mut guard) = writer.lock() {
|
||||
write_frame(&mut *guard, &msg)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Read and validate the initial spawn request frame.
|
||||
fn read_spawn_request(
|
||||
reader: &mut File,
|
||||
) -> Result<codex_windows_sandbox::ipc_framed::SpawnRequest> {
|
||||
let Some(msg) = read_frame(reader)? else {
|
||||
anyhow::bail!("runner: pipe closed before spawn_request");
|
||||
};
|
||||
if msg.version != 1 {
|
||||
anyhow::bail!("runner: unsupported protocol version {}", msg.version);
|
||||
}
|
||||
match msg.message {
|
||||
Message::SpawnRequest { payload } => Ok(*payload),
|
||||
other => anyhow::bail!("runner: expected spawn_request, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Pick an effective CWD, using a junction if the ACL helper is active.
|
||||
fn effective_cwd(req_cwd: &Path, log_dir: Option<&Path>) -> PathBuf {
|
||||
let use_junction = match read_acl_mutex::read_acl_mutex_exists() {
|
||||
Ok(exists) => exists,
|
||||
Err(err) => {
|
||||
log_note(
|
||||
&format!(
|
||||
"junction: read_acl_mutex_exists failed: {err}; assuming read ACL helper is running"
|
||||
),
|
||||
log_dir,
|
||||
);
|
||||
true
|
||||
}
|
||||
};
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
/// Stream stdout/stderr from the child into Output frames.
|
||||
fn spawn_output_reader(
|
||||
writer: Arc<StdMutex<File>>,
|
||||
handle: HANDLE,
|
||||
stream: OutputStream,
|
||||
log_dir: Option<PathBuf>,
|
||||
) -> std::thread::JoinHandle<()> {
|
||||
read_handle_loop(handle, move |chunk| {
|
||||
let msg = FramedMessage {
|
||||
version: 1,
|
||||
message: Message::Output {
|
||||
payload: OutputPayload {
|
||||
data_b64: encode_bytes(chunk),
|
||||
stream,
|
||||
},
|
||||
},
|
||||
};
|
||||
if let Ok(mut guard) = writer.lock() {
|
||||
if let Err(err) = write_frame(&mut *guard, &msg) {
|
||||
log_note(
|
||||
&format!("runner output write failed: {err}"),
|
||||
log_dir.as_deref(),
|
||||
);
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Read stdin/terminate frames and forward to the child process.
|
||||
fn spawn_input_loop(
|
||||
mut reader: File,
|
||||
stdin_handle: Option<HANDLE>,
|
||||
process_handle: Arc<StdMutex<Option<HANDLE>>>,
|
||||
log_dir: Option<PathBuf>,
|
||||
) -> std::thread::JoinHandle<()> {
|
||||
std::thread::spawn(move || {
|
||||
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;
|
||||
}
|
||||
};
|
||||
match msg.message {
|
||||
Message::Stdin { payload } => {
|
||||
let Ok(bytes) = decode_bytes(&payload.data_b64) else {
|
||||
continue;
|
||||
};
|
||||
if let Some(handle) = stdin_handle {
|
||||
let mut written: u32 = 0;
|
||||
unsafe {
|
||||
let _ = windows_sys::Win32::Storage::FileSystem::WriteFile(
|
||||
handle,
|
||||
bytes.as_ptr(),
|
||||
bytes.len() as u32,
|
||||
&mut written,
|
||||
ptr::null_mut(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Message::Terminate { .. } => {
|
||||
if let Ok(guard) = process_handle.lock() {
|
||||
if let Some(handle) = guard.as_ref() {
|
||||
unsafe {
|
||||
let _ = TerminateProcess(*handle, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Message::SpawnRequest { .. } => {}
|
||||
Message::SpawnReady { .. } => {}
|
||||
Message::Output { .. } => {}
|
||||
Message::Exit { .. } => {}
|
||||
Message::Error { .. } => {}
|
||||
}
|
||||
}
|
||||
if let Some(handle) = stdin_handle {
|
||||
unsafe {
|
||||
CloseHandle(handle);
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Entry point for the Windows command runner process.
|
||||
pub fn main() -> Result<()> {
|
||||
let mut pipe_in = None;
|
||||
let mut pipe_out = None;
|
||||
let mut pipe_single = None;
|
||||
for arg in std::env::args().skip(1) {
|
||||
if let Some(rest) = arg.strip_prefix("--pipe-in=") {
|
||||
pipe_in = Some(rest.to_string());
|
||||
} else if let Some(rest) = arg.strip_prefix("--pipe-out=") {
|
||||
pipe_out = Some(rest.to_string());
|
||||
} else if let Some(rest) = arg.strip_prefix("--pipe=") {
|
||||
pipe_single = Some(rest.to_string());
|
||||
}
|
||||
}
|
||||
if pipe_in.is_none() && pipe_out.is_none() {
|
||||
if let Some(single) = pipe_single {
|
||||
pipe_in = Some(single.clone());
|
||||
pipe_out = Some(single);
|
||||
}
|
||||
}
|
||||
let Some(pipe_in) = pipe_in else {
|
||||
anyhow::bail!("runner: no pipe-in provided");
|
||||
};
|
||||
let Some(pipe_out) = pipe_out else {
|
||||
anyhow::bail!("runner: no pipe-out provided");
|
||||
};
|
||||
|
||||
let h_pipe_in = open_pipe(&pipe_in, FILE_GENERIC_READ)?;
|
||||
let h_pipe_out = open_pipe(&pipe_out, FILE_GENERIC_WRITE)?;
|
||||
let mut pipe_read = unsafe { File::from_raw_handle(h_pipe_in as _) };
|
||||
let pipe_write = Arc::new(StdMutex::new(unsafe {
|
||||
File::from_raw_handle(h_pipe_out as _)
|
||||
}));
|
||||
|
||||
let req = match read_spawn_request(&mut pipe_read) {
|
||||
Ok(v) => v,
|
||||
Err(err) => {
|
||||
let _ = send_error(&pipe_write, "spawn_failed", err.to_string());
|
||||
return Err(err);
|
||||
}
|
||||
};
|
||||
|
||||
let log_dir = Some(req.codex_home.as_path());
|
||||
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 psid_cap: *mut c_void = unsafe { convert_string_sid_to_sid(&req.cap_sid).unwrap() };
|
||||
|
||||
// Create restricted token from current process token.
|
||||
let base = unsafe { get_current_token_for_restriction()? };
|
||||
let token_res: Result<(HANDLE, *mut c_void)> = unsafe {
|
||||
match &policy {
|
||||
SandboxPolicy::ReadOnly => create_readonly_token_with_cap_from(base, psid_cap),
|
||||
SandboxPolicy::WorkspaceWrite { .. } => {
|
||||
create_workspace_write_token_with_cap_from(base, psid_cap)
|
||||
}
|
||||
SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. } => {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
};
|
||||
let (h_token, psid_to_use) = token_res?;
|
||||
unsafe {
|
||||
CloseHandle(base);
|
||||
allow_null_device(psid_to_use);
|
||||
}
|
||||
|
||||
let effective_cwd = effective_cwd(&req.cwd, log_dir);
|
||||
log_note(
|
||||
&format!(
|
||||
"runner: effective cwd={} (requested {})",
|
||||
effective_cwd.display(),
|
||||
req.cwd.display()
|
||||
),
|
||||
log_dir,
|
||||
);
|
||||
|
||||
let mut hpc_handle: Option<HANDLE> = 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,
|
||||
&req.command,
|
||||
&effective_cwd,
|
||||
&req.env,
|
||||
)?;
|
||||
let (hpc, input_write, output_read) = conpty.into_raw();
|
||||
hpc_handle = Some(hpc);
|
||||
(
|
||||
pi,
|
||||
output_read,
|
||||
windows_sys::Win32::Foundation::INVALID_HANDLE_VALUE,
|
||||
Some(input_write),
|
||||
)
|
||||
} else {
|
||||
let pipe_handles: PipeSpawnHandles = spawn_process_with_pipes(
|
||||
h_token,
|
||||
&req.command,
|
||||
&effective_cwd,
|
||||
&req.env,
|
||||
StdinMode::Closed,
|
||||
StderrMode::Separate,
|
||||
)?;
|
||||
(
|
||||
pipe_handles.process,
|
||||
pipe_handles.stdout_read,
|
||||
pipe_handles
|
||||
.stderr_read
|
||||
.unwrap_or(windows_sys::Win32::Foundation::INVALID_HANDLE_VALUE),
|
||||
pipe_handles.stdin_write,
|
||||
)
|
||||
};
|
||||
|
||||
unsafe {
|
||||
CloseHandle(h_token);
|
||||
}
|
||||
|
||||
let h_job = unsafe { create_job_kill_on_close().ok() };
|
||||
if let Some(job) = h_job {
|
||||
unsafe {
|
||||
let _ = AssignProcessToJobObject(job, pi.hProcess);
|
||||
}
|
||||
}
|
||||
|
||||
let process_handle = Arc::new(StdMutex::new(Some(pi.hProcess)));
|
||||
|
||||
let msg = FramedMessage {
|
||||
version: 1,
|
||||
message: Message::SpawnReady {
|
||||
payload: codex_windows_sandbox::ipc_framed::SpawnReady {
|
||||
process_id: unsafe { GetProcessId(pi.hProcess) },
|
||||
},
|
||||
},
|
||||
};
|
||||
if let Ok(mut guard) = pipe_write.lock() {
|
||||
if let Err(err) = write_frame(&mut *guard, &msg) {
|
||||
log_note(&format!("runner spawn_ready write failed: {err}"), log_dir);
|
||||
let _ = send_error(&pipe_write, "spawn_failed", err.to_string());
|
||||
return Err(err);
|
||||
}
|
||||
}
|
||||
let log_dir_owned = log_dir.map(|p| p.to_path_buf());
|
||||
let _out_thread = spawn_output_reader(
|
||||
Arc::clone(&pipe_write),
|
||||
stdout_handle,
|
||||
OutputStream::Stdout,
|
||||
log_dir_owned.clone(),
|
||||
);
|
||||
let _err_thread = if stderr_handle != windows_sys::Win32::Foundation::INVALID_HANDLE_VALUE {
|
||||
Some(spawn_output_reader(
|
||||
Arc::clone(&pipe_write),
|
||||
stderr_handle,
|
||||
OutputStream::Stderr,
|
||||
log_dir_owned.clone(),
|
||||
))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let _input_thread = spawn_input_loop(
|
||||
pipe_read,
|
||||
stdin_handle,
|
||||
Arc::clone(&process_handle),
|
||||
log_dir_owned,
|
||||
);
|
||||
|
||||
let timeout = req.timeout_ms.map(|ms| ms as u32).unwrap_or(INFINITE);
|
||||
let wait_res = unsafe { WaitForSingleObject(pi.hProcess, timeout) };
|
||||
let timed_out = wait_res == WAIT_TIMEOUT;
|
||||
|
||||
let exit_code: i32;
|
||||
unsafe {
|
||||
if timed_out {
|
||||
let _ = TerminateProcess(pi.hProcess, 1);
|
||||
exit_code = 128 + 64;
|
||||
} else {
|
||||
let mut raw_exit: u32 = 1;
|
||||
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);
|
||||
}
|
||||
if pi.hProcess != 0 {
|
||||
CloseHandle(pi.hProcess);
|
||||
}
|
||||
if let Some(job) = h_job {
|
||||
CloseHandle(job);
|
||||
}
|
||||
}
|
||||
let exit_msg = FramedMessage {
|
||||
version: 1,
|
||||
message: Message::Exit {
|
||||
payload: ExitPayload {
|
||||
exit_code,
|
||||
timed_out,
|
||||
},
|
||||
},
|
||||
};
|
||||
if let Ok(mut guard) = pipe_write.lock() {
|
||||
if let Err(err) = write_frame(&mut *guard, &exit_msg) {
|
||||
log_note(&format!("runner exit write failed: {err}"), log_dir);
|
||||
}
|
||||
}
|
||||
|
||||
std::process::exit(exit_code);
|
||||
}
|
||||
313
codex-rs/windows-sandbox-rs/src/elevated/elevated_impl.rs
Normal file
313
codex-rs/windows-sandbox-rs/src/elevated/elevated_impl.rs
Normal file
@@ -0,0 +1,313 @@
|
||||
//weasel: seems like all the changes to this file are around using framed IPC instead of a request file. Is there anything else?
|
||||
mod windows_impl {
|
||||
use crate::acl::allow_null_device;
|
||||
use crate::allow::compute_allow_paths;
|
||||
use crate::allow::AllowDenyPaths;
|
||||
use crate::cap::load_or_create_cap_sids;
|
||||
use crate::env::ensure_non_interactive_pager;
|
||||
use crate::env::inherit_path_env;
|
||||
use crate::env::normalize_null_device_env;
|
||||
use crate::identity::require_logon_sandbox_creds;
|
||||
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::SpawnRequest;
|
||||
use crate::logging::log_failure;
|
||||
use crate::logging::log_start;
|
||||
use crate::logging::log_success;
|
||||
use crate::policy::parse_policy;
|
||||
use crate::policy::SandboxPolicy;
|
||||
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::runner_pipe::PIPE_ACCESS_INBOUND;
|
||||
use crate::runner_pipe::PIPE_ACCESS_OUTBOUND;
|
||||
use crate::sandbox_utils::ensure_codex_home_exists;
|
||||
use crate::sandbox_utils::inject_git_safe_directory;
|
||||
use crate::token::convert_string_sid_to_sid;
|
||||
use crate::winutil::quote_windows_arg;
|
||||
use crate::winutil::to_wide;
|
||||
use anyhow::Result;
|
||||
use std::collections::HashMap;
|
||||
use std::ffi::c_void;
|
||||
use std::fs::File;
|
||||
use std::os::windows::io::FromRawHandle;
|
||||
use std::path::Path;
|
||||
use std::ptr;
|
||||
use windows_sys::Win32::Foundation::CloseHandle;
|
||||
use windows_sys::Win32::Foundation::GetLastError;
|
||||
use windows_sys::Win32::System::Diagnostics::Debug::SetErrorMode;
|
||||
use windows_sys::Win32::System::Threading::CreateProcessWithLogonW;
|
||||
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;
|
||||
|
||||
pub use crate::windows_impl::CaptureResult;
|
||||
|
||||
/// Launches the command runner under the sandbox user and captures its output via IPC.
|
||||
pub fn run_windows_sandbox_capture(
|
||||
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>,
|
||||
) -> Result<CaptureResult> {
|
||||
let policy = parse_policy(policy_json_or_preset)?;
|
||||
normalize_null_device_env(&mut env_map);
|
||||
ensure_non_interactive_pager(&mut env_map);
|
||||
inherit_path_env(&mut env_map);
|
||||
inject_git_safe_directory(&mut env_map, cwd);
|
||||
let current_dir = cwd.to_path_buf();
|
||||
// Use a temp-based log dir that the sandbox user can write.
|
||||
let sandbox_base = codex_home.join(".sandbox");
|
||||
ensure_codex_home_exists(&sandbox_base)?;
|
||||
|
||||
let logs_base_dir: Option<&Path> = Some(sandbox_base.as_path());
|
||||
log_start(&command, logs_base_dir);
|
||||
let sandbox_creds =
|
||||
require_logon_sandbox_creds(&policy, sandbox_policy_cwd, cwd, &env_map, codex_home)?;
|
||||
// Build capability SID for ACL grants.
|
||||
if matches!(
|
||||
&policy,
|
||||
SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. }
|
||||
) {
|
||||
anyhow::bail!("DangerFullAccess and ExternalSandbox are not supported for sandboxing")
|
||||
}
|
||||
let caps = load_or_create_cap_sids(codex_home)?;
|
||||
let (psid_to_use, cap_sid_str) = match &policy {
|
||||
SandboxPolicy::ReadOnly => (
|
||||
unsafe { convert_string_sid_to_sid(&caps.readonly).unwrap() },
|
||||
caps.readonly.clone(),
|
||||
),
|
||||
SandboxPolicy::WorkspaceWrite { .. } => (
|
||||
unsafe { convert_string_sid_to_sid(&caps.workspace).unwrap() },
|
||||
caps.workspace.clone(),
|
||||
),
|
||||
SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. } => {
|
||||
unreachable!("DangerFullAccess handled above")
|
||||
}
|
||||
};
|
||||
|
||||
let AllowDenyPaths { allow: _, deny: _ } =
|
||||
compute_allow_paths(&policy, sandbox_policy_cwd, ¤t_dir, &env_map);
|
||||
// Deny/allow ACEs are now applied during setup; avoid per-command churn.
|
||||
unsafe {
|
||||
allow_null_device(psid_to_use);
|
||||
}
|
||||
|
||||
// Prepare named pipe for runner.
|
||||
let (pipe_in, pipe_out) = pipe_pair();
|
||||
let h_pipe_in = create_named_pipe(&pipe_in, PIPE_ACCESS_OUTBOUND)?;
|
||||
let h_pipe_out = create_named_pipe(&pipe_out, PIPE_ACCESS_INBOUND)?;
|
||||
|
||||
// Launch runner as sandbox user via CreateProcessWithLogonW.
|
||||
let runner_exe = find_runner_exe();
|
||||
let runner_cmdline = runner_exe
|
||||
.to_str()
|
||||
.map(|s| s.to_string())
|
||||
.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}")),
|
||||
quote_windows_arg(&format!("--pipe-out={pipe_out}"))
|
||||
);
|
||||
let mut cmdline_vec: Vec<u16> = to_wide(&runner_full_cmd);
|
||||
let exe_w: Vec<u16> = to_wide(&runner_cmdline);
|
||||
let cwd_w: Vec<u16> = to_wide(cwd);
|
||||
|
||||
// Minimal CPWL launch: inherit env, no desktop override, no handle inheritance.
|
||||
let env_block: Option<Vec<u16>> = None;
|
||||
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 user_w = to_wide(&sandbox_creds.username);
|
||||
let domain_w = to_wide(".");
|
||||
let password_w = to_wide(&sandbox_creds.password);
|
||||
// Suppress WER/UI popups from the runner process so we can collect exit codes.
|
||||
let _ = unsafe { SetErrorMode(0x0001 | 0x0002) }; // SEM_FAILCRITICALERRORS | SEM_NOGPFAULTERRORBOX
|
||||
|
||||
// Ensure command line buffer is mutable and includes the exe as argv[0].
|
||||
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(|b| b.as_ptr() as *const c_void)
|
||||
.unwrap_or(ptr::null()),
|
||||
cwd_w.as_ptr(),
|
||||
&si,
|
||||
&mut pi,
|
||||
)
|
||||
};
|
||||
if spawn_res == 0 {
|
||||
let err = unsafe { GetLastError() } as i32;
|
||||
return Err(anyhow::anyhow!("CreateProcessWithLogonW failed: {}", err));
|
||||
}
|
||||
|
||||
connect_pipe(h_pipe_in)?;
|
||||
connect_pipe(h_pipe_out)?;
|
||||
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 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_sid: cap_sid_str.clone(),
|
||||
timeout_ms,
|
||||
//weasel: does tty=true not go through this file at all?
|
||||
tty: false,
|
||||
}),
|
||||
},
|
||||
};
|
||||
write_frame(&mut pipe_write, &spawn_request)?;
|
||||
|
||||
let mut stdout = Vec::new();
|
||||
let mut stderr = Vec::new();
|
||||
let mut exit_code = 1;
|
||||
let mut timed_out = false;
|
||||
loop {
|
||||
let msg = match read_frame(&mut pipe_read)? {
|
||||
Some(v) => v,
|
||||
None => break,
|
||||
};
|
||||
match msg.message {
|
||||
Message::SpawnReady { .. } => {}
|
||||
Message::Output { payload } => {
|
||||
if let Ok(data) = crate::ipc_framed::decode_bytes(&payload.data_b64) {
|
||||
match payload.stream {
|
||||
crate::ipc_framed::OutputStream::Stdout => {
|
||||
stdout.extend_from_slice(&data);
|
||||
}
|
||||
crate::ipc_framed::OutputStream::Stderr => {
|
||||
stderr.extend_from_slice(&data);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Message::Exit { payload } => {
|
||||
exit_code = payload.exit_code;
|
||||
timed_out = payload.timed_out;
|
||||
break;
|
||||
}
|
||||
Message::Error { payload } => {
|
||||
return Err(anyhow::anyhow!("runner error: {}", payload.message));
|
||||
}
|
||||
Message::Stdin { .. } => {}
|
||||
Message::SpawnRequest { .. } => {}
|
||||
Message::Terminate { .. } => {}
|
||||
}
|
||||
}
|
||||
|
||||
let timeout = timeout_ms.map(|ms| ms as u32).unwrap_or(INFINITE);
|
||||
let _ = unsafe { WaitForSingleObject(pi.hProcess, timeout) };
|
||||
|
||||
unsafe {
|
||||
if pi.hThread != 0 {
|
||||
CloseHandle(pi.hThread);
|
||||
}
|
||||
if pi.hProcess != 0 {
|
||||
CloseHandle(pi.hProcess);
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::policy::SandboxPolicy;
|
||||
|
||||
fn workspace_policy(network_access: bool) -> SandboxPolicy {
|
||||
SandboxPolicy::WorkspaceWrite {
|
||||
writable_roots: Vec::new(),
|
||||
network_access,
|
||||
exclude_tmpdir_env_var: false,
|
||||
exclude_slash_tmp: false,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn applies_network_block_when_access_is_disabled() {
|
||||
assert!(!workspace_policy(false).has_full_network_access());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skips_network_block_when_access_is_allowed() {
|
||||
assert!(workspace_policy(true).has_full_network_access());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn applies_network_block_for_read_only() {
|
||||
assert!(!SandboxPolicy::ReadOnly.has_full_network_access());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use windows_impl::run_windows_sandbox_capture;
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
mod stub {
|
||||
use anyhow::bail;
|
||||
use anyhow::Result;
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct CaptureResult {
|
||||
pub exit_code: i32,
|
||||
pub stdout: Vec<u8>,
|
||||
pub stderr: Vec<u8>,
|
||||
pub timed_out: bool,
|
||||
}
|
||||
|
||||
/// Stub implementation for non-Windows targets; sandboxing only works on Windows.
|
||||
pub fn run_windows_sandbox_capture(
|
||||
_policy_json_or_preset: &str,
|
||||
_sandbox_policy_cwd: &Path,
|
||||
_codex_home: &Path,
|
||||
_command: Vec<String>,
|
||||
_cwd: &Path,
|
||||
_env_map: HashMap<String, String>,
|
||||
_timeout_ms: Option<u64>,
|
||||
) -> Result<CaptureResult> {
|
||||
bail!("Windows sandbox is only available on Windows")
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
pub use stub::run_windows_sandbox_capture;
|
||||
176
codex-rs/windows-sandbox-rs/src/elevated/ipc_framed.rs
Normal file
176
codex-rs/windows-sandbox-rs/src/elevated/ipc_framed.rs
Normal file
@@ -0,0 +1,176 @@
|
||||
//weasel: this file *only* applies to the elevated sandbox right? This is the interface between CLI and runner?
|
||||
//! Framed IPC protocol used between the parent (CLI) and the elevated command runner.
|
||||
//!
|
||||
//! This module defines the JSON message schema (spawn request/ready, output, stdin,
|
||||
//! exit, error, terminate) plus length‑prefixed framing helpers for a byte stream.
|
||||
//! It is **elevated-path only**: the parent uses it to bootstrap the runner and
|
||||
//! stream unified_exec I/O over named pipes. The legacy restricted‑token path does
|
||||
//! not use this protocol, and non‑unified exec capture uses it only when running
|
||||
//! through the elevated runner.
|
||||
|
||||
use anyhow::Result;
|
||||
use base64::engine::general_purpose::STANDARD;
|
||||
use base64::Engine as _;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::collections::HashMap;
|
||||
use std::io::Read;
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
|
||||
//weasel: is this arbitrary?
|
||||
const MAX_FRAME_LEN: usize = 8 * 1024 * 1024;
|
||||
|
||||
/// Length-prefixed, JSON-encoded frame.
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct FramedMessage {
|
||||
// weasel: is v version? let's make this a better variable name if so
|
||||
pub version: u8,
|
||||
#[serde(flatten)]
|
||||
pub message: Message,
|
||||
}
|
||||
|
||||
/// IPC message variants exchanged between parent and runner.
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
pub enum Message {
|
||||
//weasel: don't totally undersand these. SpawnRequest and SpawnReady seem like "actions" but Output/Stdin/etc do not. explain.
|
||||
SpawnRequest { payload: Box<SpawnRequest> },
|
||||
SpawnReady { payload: SpawnReady },
|
||||
Output { payload: OutputPayload },
|
||||
Stdin { payload: StdinPayload },
|
||||
Exit { payload: ExitPayload },
|
||||
Error { payload: ErrorPayload },
|
||||
Terminate { payload: EmptyPayload },
|
||||
}
|
||||
|
||||
/// Spawn parameters sent from parent to runner.
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct SpawnRequest {
|
||||
pub command: Vec<String>,
|
||||
pub cwd: PathBuf,
|
||||
pub env: HashMap<String, String>,
|
||||
pub policy_json_or_preset: String,
|
||||
pub sandbox_policy_cwd: PathBuf,
|
||||
pub codex_home: PathBuf,
|
||||
pub real_codex_home: PathBuf,
|
||||
pub cap_sid: String,
|
||||
pub timeout_ms: Option<u64>,
|
||||
pub tty: bool,
|
||||
}
|
||||
|
||||
/// Ack from runner after it spawns the child process.
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct SpawnReady {
|
||||
pub process_id: u32,
|
||||
}
|
||||
|
||||
/// Output data sent from runner to parent.
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct OutputPayload {
|
||||
pub data_b64: String,
|
||||
pub stream: OutputStream,
|
||||
}
|
||||
|
||||
/// Output stream identifier for `OutputPayload`.
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum OutputStream {
|
||||
Stdout,
|
||||
Stderr,
|
||||
}
|
||||
|
||||
/// Stdin bytes sent from parent to runner.
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct StdinPayload {
|
||||
pub data_b64: String,
|
||||
}
|
||||
|
||||
/// Exit status sent from runner to parent.
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct ExitPayload {
|
||||
pub exit_code: i32,
|
||||
pub timed_out: bool,
|
||||
}
|
||||
|
||||
/// Error payload sent when the runner fails to spawn or stream.
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct ErrorPayload {
|
||||
pub message: String,
|
||||
pub code: String,
|
||||
}
|
||||
|
||||
/// Empty payload for control messages.
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
|
||||
pub struct EmptyPayload {}
|
||||
|
||||
/// Base64-encode raw bytes for IPC payloads.
|
||||
pub fn encode_bytes(data: &[u8]) -> String {
|
||||
STANDARD.encode(data)
|
||||
}
|
||||
|
||||
/// Decode base64 payload data into raw bytes.
|
||||
pub fn decode_bytes(data: &str) -> Result<Vec<u8>> {
|
||||
Ok(STANDARD.decode(data.as_bytes())?)
|
||||
}
|
||||
|
||||
/// Write a length-prefixed JSON frame.
|
||||
pub fn write_frame<W: Write>(mut writer: W, msg: &FramedMessage) -> Result<()> {
|
||||
let payload = serde_json::to_vec(msg)?;
|
||||
if payload.len() > MAX_FRAME_LEN {
|
||||
anyhow::bail!("frame too large: {}", payload.len());
|
||||
}
|
||||
let len = payload.len() as u32;
|
||||
writer.write_all(&len.to_le_bytes())?;
|
||||
writer.write_all(&payload)?;
|
||||
writer.flush()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Read a length-prefixed JSON frame; returns `Ok(None)` on EOF.
|
||||
pub fn read_frame<R: Read>(mut reader: R) -> Result<Option<FramedMessage>> {
|
||||
let mut len_buf = [0u8; 4];
|
||||
match reader.read_exact(&mut len_buf) {
|
||||
Ok(()) => {}
|
||||
Err(err) if err.kind() == std::io::ErrorKind::UnexpectedEof => return Ok(None),
|
||||
Err(err) => return Err(err.into()),
|
||||
}
|
||||
let len = u32::from_le_bytes(len_buf) as usize;
|
||||
if len > MAX_FRAME_LEN {
|
||||
anyhow::bail!("frame too large: {}", len);
|
||||
}
|
||||
let mut payload = vec![0u8; len];
|
||||
reader.read_exact(&mut payload)?;
|
||||
let msg: FramedMessage = serde_json::from_slice(&payload)?;
|
||||
Ok(Some(msg))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn framed_round_trip() {
|
||||
let msg = FramedMessage {
|
||||
version: 1,
|
||||
message: Message::Output {
|
||||
payload: OutputPayload {
|
||||
data_b64: encode_bytes(b"hello"),
|
||||
stream: OutputStream::Stdout,
|
||||
},
|
||||
},
|
||||
};
|
||||
let mut buf = Vec::new();
|
||||
write_frame(&mut buf, &msg).expect("write");
|
||||
let decoded = read_frame(buf.as_slice()).expect("read").expect("some");
|
||||
assert_eq!(decoded.version, 1);
|
||||
match decoded.message {
|
||||
Message::Output { payload } => {
|
||||
assert_eq!(payload.stream, OutputStream::Stdout);
|
||||
let data = decode_bytes(&payload.data_b64).expect("decode");
|
||||
assert_eq!(data, b"hello");
|
||||
}
|
||||
other => panic!("unexpected message: {other:?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
109
codex-rs/windows-sandbox-rs/src/elevated/runner_pipe.rs
Normal file
109
codex-rs/windows-sandbox-rs/src/elevated/runner_pipe.rs
Normal file
@@ -0,0 +1,109 @@
|
||||
//! Named pipe helpers for the elevated Windows sandbox runner.
|
||||
//!
|
||||
//! This module generates paired pipe names, creates server‑side pipes with permissive
|
||||
//! 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::winutil::to_wide;
|
||||
use rand::rngs::SmallRng;
|
||||
use rand::Rng;
|
||||
use rand::SeedableRng;
|
||||
use std::io;
|
||||
use std::path::PathBuf;
|
||||
use std::ptr;
|
||||
use windows_sys::Win32::Foundation::GetLastError;
|
||||
use windows_sys::Win32::Foundation::HANDLE;
|
||||
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::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;
|
||||
|
||||
/// Locates `codex-command-runner.exe` next to the current binary.
|
||||
/// weasel: was this moved from somewhere else? This must be pre-existing. make sure it isn't duplicated
|
||||
pub fn find_runner_exe() -> PathBuf {
|
||||
if let Ok(exe) = std::env::current_exe() {
|
||||
if let Some(dir) = exe.parent() {
|
||||
let candidate = dir.join("codex-command-runner.exe");
|
||||
if candidate.exists() {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
}
|
||||
PathBuf::from("codex-command-runner.exe")
|
||||
}
|
||||
|
||||
/// 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>());
|
||||
(format!("{base}-in"), format!("{base}-out"))
|
||||
}
|
||||
|
||||
/// Creates a named pipe with permissive ACLs so the sandbox user can connect.
|
||||
pub fn create_named_pipe(name: &str, access: u32) -> io::Result<HANDLE> {
|
||||
// Allow sandbox users to connect by granting Everyone full access on the pipe.
|
||||
let sddl = to_wide("D:(A;;GA;;;WD)");
|
||||
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,
|
||||
)
|
||||
};
|
||||
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 a client connection on the named pipe, tolerating an existing connection.
|
||||
/// weasel: is this called from the cli or the runner (or both?)
|
||||
pub fn connect_pipe(h: HANDLE) -> 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));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,535 +0,0 @@
|
||||
mod windows_impl {
|
||||
use crate::acl::allow_null_device;
|
||||
use crate::allow::compute_allow_paths;
|
||||
use crate::allow::AllowDenyPaths;
|
||||
use crate::cap::load_or_create_cap_sids;
|
||||
use crate::env::ensure_non_interactive_pager;
|
||||
use crate::env::inherit_path_env;
|
||||
use crate::env::normalize_null_device_env;
|
||||
use crate::identity::require_logon_sandbox_creds;
|
||||
use crate::logging::log_failure;
|
||||
use crate::logging::log_note;
|
||||
use crate::logging::log_start;
|
||||
use crate::logging::log_success;
|
||||
use crate::policy::parse_policy;
|
||||
use crate::policy::SandboxPolicy;
|
||||
use crate::token::convert_string_sid_to_sid;
|
||||
use crate::winutil::quote_windows_arg;
|
||||
use crate::winutil::to_wide;
|
||||
use anyhow::Result;
|
||||
use rand::rngs::SmallRng;
|
||||
use rand::Rng;
|
||||
use rand::SeedableRng;
|
||||
use std::collections::HashMap;
|
||||
use std::ffi::c_void;
|
||||
use std::fs;
|
||||
use std::io;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::ptr;
|
||||
use windows_sys::Win32::Foundation::CloseHandle;
|
||||
use windows_sys::Win32::Foundation::GetLastError;
|
||||
use windows_sys::Win32::Foundation::HANDLE;
|
||||
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::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;
|
||||
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;
|
||||
|
||||
/// Ensures the parent directory of a path exists before writing to it.
|
||||
/// Walks upward from `start` to locate the git worktree root, following gitfile redirects.
|
||||
fn find_git_root(start: &Path) -> Option<PathBuf> {
|
||||
let mut cur = dunce::canonicalize(start).ok()?;
|
||||
loop {
|
||||
let marker = cur.join(".git");
|
||||
if marker.is_dir() {
|
||||
return Some(cur);
|
||||
}
|
||||
if marker.is_file() {
|
||||
if let Ok(txt) = std::fs::read_to_string(&marker) {
|
||||
if let Some(rest) = txt.trim().strip_prefix("gitdir:") {
|
||||
let gitdir = rest.trim();
|
||||
let resolved = if Path::new(gitdir).is_absolute() {
|
||||
PathBuf::from(gitdir)
|
||||
} else {
|
||||
cur.join(gitdir)
|
||||
};
|
||||
return resolved.parent().map(|p| p.to_path_buf()).or(Some(cur));
|
||||
}
|
||||
}
|
||||
return Some(cur);
|
||||
}
|
||||
let parent = cur.parent()?;
|
||||
if parent == cur {
|
||||
return None;
|
||||
}
|
||||
cur = parent.to_path_buf();
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates the sandbox user's Codex home directory if it does not already exist.
|
||||
fn ensure_codex_home_exists(p: &Path) -> Result<()> {
|
||||
std::fs::create_dir_all(p)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Adds a git safe.directory entry to the environment when running inside a repository.
|
||||
/// git will not otherwise allow the Sandbox user to run git commands on the repo directory
|
||||
/// which is owned by the primary user.
|
||||
fn inject_git_safe_directory(
|
||||
env_map: &mut HashMap<String, String>,
|
||||
cwd: &Path,
|
||||
_logs_base_dir: Option<&Path>,
|
||||
) {
|
||||
if let Some(git_root) = find_git_root(cwd) {
|
||||
let mut cfg_count: usize = env_map
|
||||
.get("GIT_CONFIG_COUNT")
|
||||
.and_then(|v| v.parse::<usize>().ok())
|
||||
.unwrap_or(0);
|
||||
let git_path = git_root.to_string_lossy().replace("\\\\", "/");
|
||||
env_map.insert(
|
||||
format!("GIT_CONFIG_KEY_{cfg_count}"),
|
||||
"safe.directory".to_string(),
|
||||
);
|
||||
env_map.insert(format!("GIT_CONFIG_VALUE_{cfg_count}"), git_path);
|
||||
cfg_count += 1;
|
||||
env_map.insert("GIT_CONFIG_COUNT".to_string(), cfg_count.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
/// Locates `codex-command-runner.exe` next to the current binary.
|
||||
fn find_runner_exe() -> PathBuf {
|
||||
if let Ok(exe) = std::env::current_exe() {
|
||||
if let Some(dir) = exe.parent() {
|
||||
let candidate = dir.join("codex-command-runner.exe");
|
||||
if candidate.exists() {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
}
|
||||
PathBuf::from("codex-command-runner.exe")
|
||||
}
|
||||
|
||||
/// Generates a unique named-pipe path used to communicate with the runner process.
|
||||
fn pipe_name(suffix: &str) -> String {
|
||||
let mut rng = SmallRng::from_entropy();
|
||||
format!(r"\\.\pipe\codex-runner-{:x}-{}", rng.gen::<u128>(), suffix)
|
||||
}
|
||||
|
||||
/// Creates a named pipe with permissive ACLs so the sandbox user can connect.
|
||||
fn create_named_pipe(name: &str, access: u32) -> io::Result<HANDLE> {
|
||||
// Allow sandbox users to connect by granting Everyone full access on the pipe.
|
||||
let sddl = to_wide("D:(A;;GA;;;WD)");
|
||||
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,
|
||||
)
|
||||
};
|
||||
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 a client connection on the named pipe, tolerating an existing connection.
|
||||
fn connect_pipe(h: HANDLE) -> 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));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
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>,
|
||||
stdin_pipe: String,
|
||||
stdout_pipe: String,
|
||||
stderr_pipe: String,
|
||||
}
|
||||
|
||||
/// Launches the command runner under the sandbox user and captures its output.
|
||||
pub fn run_windows_sandbox_capture(
|
||||
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>,
|
||||
) -> Result<CaptureResult> {
|
||||
let policy = parse_policy(policy_json_or_preset)?;
|
||||
normalize_null_device_env(&mut env_map);
|
||||
ensure_non_interactive_pager(&mut env_map);
|
||||
inherit_path_env(&mut env_map);
|
||||
inject_git_safe_directory(&mut env_map, cwd, None);
|
||||
let current_dir = cwd.to_path_buf();
|
||||
// Use a temp-based log dir that the sandbox user can write.
|
||||
let sandbox_base = codex_home.join(".sandbox");
|
||||
ensure_codex_home_exists(&sandbox_base)?;
|
||||
|
||||
let logs_base_dir: Option<&Path> = Some(sandbox_base.as_path());
|
||||
log_start(&command, logs_base_dir);
|
||||
let sandbox_creds =
|
||||
require_logon_sandbox_creds(&policy, sandbox_policy_cwd, cwd, &env_map, codex_home)?;
|
||||
// Build capability SID for ACL grants.
|
||||
if matches!(
|
||||
&policy,
|
||||
SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. }
|
||||
) {
|
||||
anyhow::bail!("DangerFullAccess and ExternalSandbox are not supported for sandboxing")
|
||||
}
|
||||
if !policy.has_full_disk_read_access() {
|
||||
anyhow::bail!(
|
||||
"Restricted read-only access is not yet supported by the Windows sandbox backend"
|
||||
);
|
||||
}
|
||||
let caps = load_or_create_cap_sids(codex_home)?;
|
||||
let (psid_to_use, cap_sids) = match &policy {
|
||||
SandboxPolicy::ReadOnly { .. } => (
|
||||
unsafe { convert_string_sid_to_sid(&caps.readonly).unwrap() },
|
||||
vec![caps.readonly.clone()],
|
||||
),
|
||||
SandboxPolicy::WorkspaceWrite { .. } => (
|
||||
unsafe { convert_string_sid_to_sid(&caps.workspace).unwrap() },
|
||||
vec![
|
||||
caps.workspace.clone(),
|
||||
crate::cap::workspace_cap_sid_for_cwd(codex_home, cwd)?,
|
||||
],
|
||||
),
|
||||
SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. } => {
|
||||
unreachable!("DangerFullAccess handled above")
|
||||
}
|
||||
};
|
||||
|
||||
let AllowDenyPaths { allow: _, deny: _ } =
|
||||
compute_allow_paths(&policy, sandbox_policy_cwd, ¤t_dir, &env_map);
|
||||
// Deny/allow ACEs are now applied during setup; avoid per-command churn.
|
||||
unsafe {
|
||||
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,
|
||||
)?;
|
||||
let h_stdout_pipe = create_named_pipe(
|
||||
&stdout_name,
|
||||
PIPE_ACCESS_DUPLEX | PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT,
|
||||
)?;
|
||||
let h_stderr_pipe = create_named_pipe(
|
||||
&stderr_name,
|
||||
PIPE_ACCESS_DUPLEX | PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT,
|
||||
)?;
|
||||
|
||||
// Launch runner as sandbox user via CreateProcessWithLogonW.
|
||||
let runner_exe = find_runner_exe();
|
||||
let runner_cmdline = runner_exe
|
||||
.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,
|
||||
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()))
|
||||
);
|
||||
let mut cmdline_vec: Vec<u16> = to_wide(&runner_full_cmd);
|
||||
let exe_w: Vec<u16> = to_wide(&runner_cmdline);
|
||||
let cwd_w: Vec<u16> = to_wide(cwd);
|
||||
|
||||
// Minimal CPWL launch: inherit env, no desktop override, no handle inheritance.
|
||||
let env_block: Option<Vec<u16>> = None;
|
||||
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 user_w = to_wide(&sandbox_creds.username);
|
||||
let domain_w = to_wide(".");
|
||||
let password_w = to_wide(&sandbox_creds.password);
|
||||
// Suppress WER/UI popups from the runner process so we can collect exit codes.
|
||||
let _ = unsafe { SetErrorMode(0x0001 | 0x0002) }; // SEM_FAILCRITICALERRORS | SEM_NOGPFAULTERRORBOX
|
||||
|
||||
// Ensure command line buffer is mutable and includes the exe as argv[0].
|
||||
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(|b| b.as_ptr() as *const c_void)
|
||||
.unwrap_or(ptr::null()),
|
||||
cwd_w.as_ptr(),
|
||||
&si,
|
||||
&mut pi,
|
||||
)
|
||||
};
|
||||
if spawn_res == 0 {
|
||||
let err = unsafe { GetLastError() } as i32;
|
||||
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);
|
||||
}
|
||||
|
||||
// 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 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);
|
||||
}
|
||||
} else {
|
||||
unsafe {
|
||||
windows_sys::Win32::System::Threading::TerminateProcess(pi.hProcess, 1);
|
||||
}
|
||||
}
|
||||
|
||||
unsafe {
|
||||
if pi.hThread != 0 {
|
||||
CloseHandle(pi.hThread);
|
||||
}
|
||||
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,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::policy::SandboxPolicy;
|
||||
|
||||
fn workspace_policy(network_access: bool) -> SandboxPolicy {
|
||||
SandboxPolicy::WorkspaceWrite {
|
||||
writable_roots: Vec::new(),
|
||||
read_only_access: Default::default(),
|
||||
network_access,
|
||||
exclude_tmpdir_env_var: false,
|
||||
exclude_slash_tmp: false,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn applies_network_block_when_access_is_disabled() {
|
||||
assert!(!workspace_policy(false).has_full_network_access());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skips_network_block_when_access_is_allowed() {
|
||||
assert!(workspace_policy(true).has_full_network_access());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn applies_network_block_for_read_only() {
|
||||
assert!(!SandboxPolicy::new_read_only_policy().has_full_network_access());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use windows_impl::run_windows_sandbox_capture;
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
mod stub {
|
||||
use anyhow::bail;
|
||||
use anyhow::Result;
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct CaptureResult {
|
||||
pub exit_code: i32,
|
||||
pub stdout: Vec<u8>,
|
||||
pub stderr: Vec<u8>,
|
||||
pub timed_out: bool,
|
||||
}
|
||||
|
||||
/// Stub implementation for non-Windows targets; sandboxing only works on Windows.
|
||||
pub fn run_windows_sandbox_capture(
|
||||
_policy_json_or_preset: &str,
|
||||
_sandbox_policy_cwd: &Path,
|
||||
_codex_home: &Path,
|
||||
_command: Vec<String>,
|
||||
_cwd: &Path,
|
||||
_env_map: HashMap<String, String>,
|
||||
_timeout_ms: Option<u64>,
|
||||
) -> Result<CaptureResult> {
|
||||
bail!("Windows sandbox is only available on Windows")
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
pub use stub::run_windows_sandbox_capture;
|
||||
@@ -22,11 +22,20 @@ windows_modules!(
|
||||
workspace_acl
|
||||
);
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
#[path = "conpty/mod.rs"]
|
||||
mod conpty;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
#[path = "elevated/ipc_framed.rs"]
|
||||
pub mod ipc_framed;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
#[path = "setup_orchestrator.rs"]
|
||||
mod setup;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
#[path = "elevated/elevated_impl.rs"]
|
||||
mod elevated_impl;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
@@ -34,6 +43,18 @@ mod setup_error;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use acl::add_deny_write_ace;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
#[path = "elevated/runner_pipe.rs"]
|
||||
mod runner_pipe;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
#[path = "unified_exec/session.rs"]
|
||||
mod session;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
#[path = "sandbox_utils.rs"]
|
||||
mod sandbox_utils;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use acl::allow_null_device;
|
||||
#[cfg(target_os = "windows")]
|
||||
@@ -77,8 +98,20 @@ pub use policy::parse_policy;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use policy::SandboxPolicy;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use conpty::spawn_conpty_process_as_user;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use process::create_process_as_user;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use process::read_handle_loop;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use process::spawn_process_with_pipes;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use process::PipeSpawnHandles;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use process::StderrMode;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use process::StdinMode;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use setup::run_elevated_setup;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use setup::run_setup_refresh;
|
||||
@@ -121,6 +154,10 @@ pub use windows_impl::run_windows_sandbox_legacy_preflight;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use windows_impl::CaptureResult;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use session::spawn_windows_sandbox_session_legacy;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use session::spawn_windows_sandbox_session_elevated;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use winutil::string_from_sid_bytes;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use winutil::to_wide;
|
||||
|
||||
@@ -7,15 +7,19 @@ 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;
|
||||
@@ -136,3 +140,139 @@ pub unsafe fn create_process_as_user(
|
||||
}
|
||||
Ok((pi, si))
|
||||
}
|
||||
|
||||
/// 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,
|
||||
) -> 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) };
|
||||
let (pi, _si) = 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);
|
||||
}
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
67
codex-rs/windows-sandbox-rs/src/sandbox_utils.rs
Normal file
67
codex-rs/windows-sandbox-rs/src/sandbox_utils.rs
Normal file
@@ -0,0 +1,67 @@
|
||||
//! Shared helper utilities for Windows sandbox setup.
|
||||
//!
|
||||
//! These helpers centralize small pieces of setup logic used across both legacy and
|
||||
//! elevated paths, including unified_exec sessions and capture flows. They cover
|
||||
//! codex home directory creation and git safe.directory injection so sandboxed
|
||||
//! users can run git inside a repo owned by the primary user.
|
||||
|
||||
use anyhow::Result;
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// Walk upward from `start` to locate the git worktree root (supports gitfile redirects).
|
||||
fn find_git_root(start: &Path) -> Option<PathBuf> {
|
||||
let mut cur = dunce::canonicalize(start).ok()?;
|
||||
loop {
|
||||
let marker = cur.join(".git");
|
||||
if marker.is_dir() {
|
||||
return Some(cur);
|
||||
}
|
||||
if marker.is_file() {
|
||||
if let Ok(txt) = std::fs::read_to_string(&marker) {
|
||||
if let Some(rest) = txt.trim().strip_prefix("gitdir:") {
|
||||
let gitdir = rest.trim();
|
||||
let resolved = if Path::new(gitdir).is_absolute() {
|
||||
PathBuf::from(gitdir)
|
||||
} else {
|
||||
cur.join(gitdir)
|
||||
};
|
||||
return resolved.parent().map(|p| p.to_path_buf()).or(Some(cur));
|
||||
}
|
||||
}
|
||||
return Some(cur);
|
||||
}
|
||||
let parent = cur.parent()?;
|
||||
if parent == cur {
|
||||
return None;
|
||||
}
|
||||
cur = parent.to_path_buf();
|
||||
}
|
||||
}
|
||||
|
||||
/// Ensure the sandbox codex home directory exists.
|
||||
pub fn ensure_codex_home_exists(p: &Path) -> Result<()> {
|
||||
std::fs::create_dir_all(p)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Adds a git safe.directory entry to the environment when running inside a repository.
|
||||
/// git will not otherwise allow the Sandbox user to run git commands on the repo directory
|
||||
/// which is owned by the primary user.
|
||||
pub fn inject_git_safe_directory(env_map: &mut HashMap<String, String>, cwd: &Path) {
|
||||
if let Some(git_root) = find_git_root(cwd) {
|
||||
let mut cfg_count: usize = env_map
|
||||
.get("GIT_CONFIG_COUNT")
|
||||
.and_then(|v| v.parse::<usize>().ok())
|
||||
.unwrap_or(0);
|
||||
let git_path = git_root.to_string_lossy().replace("\\\\", "/");
|
||||
env_map.insert(
|
||||
format!("GIT_CONFIG_KEY_{cfg_count}"),
|
||||
"safe.directory".to_string(),
|
||||
);
|
||||
env_map.insert(format!("GIT_CONFIG_VALUE_{cfg_count}"), git_path);
|
||||
cfg_count += 1;
|
||||
env_map.insert("GIT_CONFIG_COUNT".to_string(), cfg_count.to_string());
|
||||
}
|
||||
}
|
||||
710
codex-rs/windows-sandbox-rs/src/unified_exec/session.rs
Normal file
710
codex-rs/windows-sandbox-rs/src/unified_exec/session.rs
Normal file
@@ -0,0 +1,710 @@
|
||||
//weasel: help me understand the scope of this file. It looks like a high level session/spawn manager? Is it for elevated/legacy/both? Is it specific to unified-exec? Is most of the stuff here new, or moved around?
|
||||
//! Unified exec session spawner for Windows sandboxing.
|
||||
//!
|
||||
//! This module implements the **unified_exec session** paths for Windows by returning a
|
||||
//! long‑lived `SpawnedProcess` wired for stdin/out/exit. It covers both the legacy
|
||||
//! restricted‑token path (direct spawn under a restricted token) and the elevated path
|
||||
//! (spawn via the command runner IPC). It is not used for non‑unified exec capture flows,
|
||||
//! which continue to use the one‑shot capture APIs.
|
||||
|
||||
use crate::acl::add_allow_ace;
|
||||
use crate::acl::add_deny_write_ace;
|
||||
use crate::acl::allow_null_device;
|
||||
use crate::acl::revoke_ace;
|
||||
use crate::allow::compute_allow_paths;
|
||||
use crate::allow::AllowDenyPaths;
|
||||
use crate::cap::load_or_create_cap_sids;
|
||||
use crate::conpty::spawn_conpty_process_as_user;
|
||||
use crate::env::apply_no_network_to_env;
|
||||
use crate::env::ensure_non_interactive_pager;
|
||||
use crate::env::inherit_path_env;
|
||||
use crate::env::normalize_null_device_env;
|
||||
use crate::identity::require_logon_sandbox_creds;
|
||||
use crate::identity::SandboxCreds;
|
||||
use crate::ipc_framed::decode_bytes;
|
||||
use crate::ipc_framed::encode_bytes;
|
||||
use crate::ipc_framed::read_frame;
|
||||
use crate::ipc_framed::write_frame;
|
||||
use crate::ipc_framed::EmptyPayload;
|
||||
use crate::ipc_framed::FramedMessage;
|
||||
use crate::ipc_framed::Message;
|
||||
use crate::ipc_framed::SpawnRequest;
|
||||
use crate::ipc_framed::StdinPayload;
|
||||
use crate::logging::log_failure;
|
||||
use crate::logging::log_start;
|
||||
use crate::logging::log_success;
|
||||
use crate::policy::parse_policy;
|
||||
use crate::policy::SandboxPolicy;
|
||||
use crate::process::read_handle_loop;
|
||||
use crate::process::spawn_process_with_pipes;
|
||||
use crate::process::StderrMode;
|
||||
use crate::process::StdinMode;
|
||||
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::runner_pipe::PIPE_ACCESS_INBOUND;
|
||||
use crate::runner_pipe::PIPE_ACCESS_OUTBOUND;
|
||||
use crate::sandbox_utils::ensure_codex_home_exists;
|
||||
use crate::token::convert_string_sid_to_sid;
|
||||
use crate::token::create_readonly_token_with_cap;
|
||||
use crate::token::create_workspace_write_token_with_cap;
|
||||
use crate::token::get_current_token_for_restriction;
|
||||
use crate::token::get_logon_sid_bytes;
|
||||
use crate::winutil::quote_windows_arg;
|
||||
use crate::winutil::to_wide;
|
||||
use anyhow::Result;
|
||||
use codex_utils_pty::spawn_from_driver;
|
||||
use codex_utils_pty::ProcessDriver;
|
||||
use codex_utils_pty::SpawnedProcess;
|
||||
use std::collections::HashMap;
|
||||
use std::ffi::c_void;
|
||||
use std::fs::File;
|
||||
use std::os::windows::io::FromRawHandle;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
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::ClosePseudoConsole;
|
||||
use windows_sys::Win32::System::Diagnostics::Debug::SetErrorMode;
|
||||
use windows_sys::Win32::System::Threading::CreateProcessWithLogonW;
|
||||
use windows_sys::Win32::System::Threading::GetExitCodeProcess;
|
||||
use windows_sys::Win32::System::Threading::PROCESS_INFORMATION;
|
||||
use windows_sys::Win32::System::Threading::TerminateProcess;
|
||||
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::STARTUPINFOW;
|
||||
|
||||
const WAIT_TIMEOUT: u32 = 0x0000_0102;
|
||||
|
||||
/// Returns true if the sandbox policy requires network blocking.
|
||||
fn should_apply_network_block(policy: &SandboxPolicy) -> bool {
|
||||
!policy.has_full_network_access()
|
||||
}
|
||||
|
||||
struct LegacyContext {
|
||||
policy: SandboxPolicy,
|
||||
logs_base_dir: Option<PathBuf>,
|
||||
is_workspace_write: bool,
|
||||
}
|
||||
|
||||
struct LegacyProcessHandles {
|
||||
process: PROCESS_INFORMATION,
|
||||
output_join: std::thread::JoinHandle<()>,
|
||||
writer_handle: tokio::task::JoinHandle<()>,
|
||||
hpc: Option<HANDLE>,
|
||||
}
|
||||
|
||||
fn prepare_legacy_context(
|
||||
policy_json_or_preset: &str,
|
||||
codex_home: &Path,
|
||||
env_map: &mut HashMap<String, String>,
|
||||
command: &[String],
|
||||
) -> Result<LegacyContext> {
|
||||
let policy = parse_policy(policy_json_or_preset)?;
|
||||
let apply_network_block = should_apply_network_block(&policy);
|
||||
normalize_null_device_env(env_map);
|
||||
ensure_non_interactive_pager(env_map);
|
||||
if apply_network_block {
|
||||
apply_no_network_to_env(env_map)?;
|
||||
}
|
||||
ensure_codex_home_exists(codex_home)?;
|
||||
let sandbox_base = codex_home.join(".sandbox");
|
||||
std::fs::create_dir_all(&sandbox_base)?;
|
||||
let logs_base_dir = Some(sandbox_base.as_path());
|
||||
log_start(command, logs_base_dir);
|
||||
let is_workspace_write = matches!(&policy, SandboxPolicy::WorkspaceWrite { .. });
|
||||
Ok(LegacyContext {
|
||||
policy,
|
||||
logs_base_dir: logs_base_dir.map(|p| p.to_path_buf()),
|
||||
is_workspace_write,
|
||||
})
|
||||
}
|
||||
|
||||
fn create_restricted_token_for_policy(
|
||||
policy: &SandboxPolicy,
|
||||
codex_home: &Path,
|
||||
) -> Result<(HANDLE, *mut c_void, String)> {
|
||||
let caps = load_or_create_cap_sids(codex_home)?;
|
||||
let (h_token, psid_to_use, cap_sid_str): (HANDLE, *mut c_void, String) = unsafe {
|
||||
match policy {
|
||||
SandboxPolicy::ReadOnly => {
|
||||
let psid = convert_string_sid_to_sid(&caps.readonly).unwrap();
|
||||
let (h_token, psid) = create_readonly_token_with_cap(psid)?;
|
||||
(h_token, psid, caps.readonly.clone())
|
||||
}
|
||||
SandboxPolicy::WorkspaceWrite { .. } => {
|
||||
let psid = convert_string_sid_to_sid(&caps.workspace).unwrap();
|
||||
let (h_token, psid) = create_workspace_write_token_with_cap(psid)?;
|
||||
(h_token, psid, caps.workspace.clone())
|
||||
}
|
||||
SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. } => {
|
||||
unreachable!("DangerFullAccess handled above")
|
||||
}
|
||||
}
|
||||
};
|
||||
Ok((h_token, psid_to_use, cap_sid_str))
|
||||
}
|
||||
|
||||
fn allow_null_device_for_workspace_write(is_workspace_write: bool) {
|
||||
if !is_workspace_write {
|
||||
return;
|
||||
}
|
||||
unsafe {
|
||||
if let Ok(base) = get_current_token_for_restriction() {
|
||||
if let Ok(bytes) = get_logon_sid_bytes(base) {
|
||||
let mut tmp = bytes.clone();
|
||||
let psid2 = tmp.as_mut_ptr() as *mut c_void;
|
||||
allow_null_device(psid2);
|
||||
}
|
||||
CloseHandle(base);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_legacy_acl_rules(
|
||||
policy: &SandboxPolicy,
|
||||
sandbox_policy_cwd: &Path,
|
||||
current_dir: &Path,
|
||||
env_map: &HashMap<String, String>,
|
||||
psid_to_use: *mut c_void,
|
||||
persist_aces: bool,
|
||||
) -> Vec<PathBuf> {
|
||||
let AllowDenyPaths { allow, deny } =
|
||||
compute_allow_paths(policy, sandbox_policy_cwd, current_dir, env_map);
|
||||
let mut guards: Vec<PathBuf> = Vec::new();
|
||||
unsafe {
|
||||
for p in &allow {
|
||||
if matches!(add_allow_ace(p, psid_to_use), Ok(true)) && !persist_aces {
|
||||
guards.push(p.clone());
|
||||
}
|
||||
}
|
||||
for p in &deny {
|
||||
if let Ok(added) = add_deny_write_ace(p, psid_to_use) {
|
||||
if added && !persist_aces {
|
||||
guards.push(p.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
allow_null_device(psid_to_use);
|
||||
}
|
||||
guards
|
||||
}
|
||||
|
||||
fn spawn_legacy_process(
|
||||
h_token: HANDLE,
|
||||
command: &[String],
|
||||
cwd: &Path,
|
||||
env_map: &HashMap<String, String>,
|
||||
tty: bool,
|
||||
output_tx: broadcast::Sender<Vec<u8>>,
|
||||
writer_rx: mpsc::Receiver<Vec<u8>>,
|
||||
) -> Result<LegacyProcessHandles> {
|
||||
let (pi, output_join, writer_handle, hpc) = if tty {
|
||||
let (pi, conpty) = spawn_conpty_process_as_user(h_token, command, cwd, env_map)?;
|
||||
let (hpc, input_write, output_read) = conpty.into_raw();
|
||||
unsafe {
|
||||
CloseHandle(h_token);
|
||||
}
|
||||
let output_join = spawn_output_reader(output_read, output_tx);
|
||||
let writer_handle = spawn_input_writer(Some(input_write), writer_rx);
|
||||
(pi, output_join, writer_handle, Some(hpc))
|
||||
} else {
|
||||
let pipe_handles = spawn_process_with_pipes(
|
||||
h_token,
|
||||
command,
|
||||
cwd,
|
||||
env_map,
|
||||
StdinMode::Closed,
|
||||
StderrMode::MergeStdout,
|
||||
)?;
|
||||
unsafe {
|
||||
CloseHandle(h_token);
|
||||
}
|
||||
let output_join = spawn_output_reader(pipe_handles.stdout_read, output_tx);
|
||||
let writer_handle = spawn_input_writer(None, writer_rx);
|
||||
(pipe_handles.process, output_join, writer_handle, None)
|
||||
};
|
||||
Ok(LegacyProcessHandles {
|
||||
process: pi,
|
||||
output_join,
|
||||
writer_handle,
|
||||
hpc,
|
||||
})
|
||||
}
|
||||
|
||||
/// Read process output and forward chunks into a broadcast channel.
|
||||
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());
|
||||
})
|
||||
}
|
||||
|
||||
/// Write stdin chunks from a channel into the child process input handle.
|
||||
fn spawn_input_writer(
|
||||
input_write: Option<HANDLE>,
|
||||
mut writer_rx: mpsc::Receiver<Vec<u8>>,
|
||||
) -> tokio::task::JoinHandle<()> {
|
||||
tokio::task::spawn_blocking(move || {
|
||||
while let Some(bytes) = writer_rx.blocking_recv() {
|
||||
let Some(handle) = input_write else {
|
||||
continue;
|
||||
};
|
||||
let mut written: u32 = 0;
|
||||
unsafe {
|
||||
let _ = WriteFile(
|
||||
handle,
|
||||
bytes.as_ptr(),
|
||||
bytes.len() as u32,
|
||||
&mut written,
|
||||
ptr::null_mut(),
|
||||
);
|
||||
}
|
||||
}
|
||||
if let Some(handle) = input_write {
|
||||
unsafe {
|
||||
CloseHandle(handle);
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn launch_runner_pipes(
|
||||
cwd: &Path,
|
||||
sandbox_creds: &SandboxCreds,
|
||||
pipe_in: String,
|
||||
pipe_out: String,
|
||||
) -> Result<(File, File)> {
|
||||
let h_pipe_in = create_named_pipe(&pipe_in, PIPE_ACCESS_OUTBOUND)?;
|
||||
let h_pipe_out = create_named_pipe(&pipe_out, PIPE_ACCESS_INBOUND)?;
|
||||
let runner_exe = find_runner_exe();
|
||||
let runner_cmdline = runner_exe
|
||||
.to_str()
|
||||
.map(|s| s.to_string())
|
||||
.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}")),
|
||||
quote_windows_arg(&format!("--pipe-out={pipe_out}"))
|
||||
);
|
||||
let mut cmdline_vec: Vec<u16> = to_wide(&runner_full_cmd);
|
||||
let exe_w: Vec<u16> = to_wide(&runner_cmdline);
|
||||
let cwd_w: Vec<u16> = to_wide(cwd);
|
||||
let env_block: Option<Vec<u16>> = None;
|
||||
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 user_w = to_wide(&sandbox_creds.username);
|
||||
let domain_w = to_wide(".");
|
||||
let password_w = to_wide(&sandbox_creds.password);
|
||||
let _ = unsafe { SetErrorMode(0x0001 | 0x0002) };
|
||||
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(|b| b.as_ptr() as *const c_void)
|
||||
.unwrap_or(ptr::null()),
|
||||
cwd_w.as_ptr(),
|
||||
&si,
|
||||
&mut pi,
|
||||
)
|
||||
};
|
||||
if spawn_res == 0 {
|
||||
let err = unsafe { GetLastError() } as i32;
|
||||
return Err(anyhow::anyhow!("CreateProcessWithLogonW failed: {}", err));
|
||||
}
|
||||
|
||||
connect_pipe(h_pipe_in)?;
|
||||
connect_pipe(h_pipe_out)?;
|
||||
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((pipe_write, pipe_read))
|
||||
}
|
||||
|
||||
fn send_spawn_request(pipe_write: &mut File, request: SpawnRequest) -> Result<()> {
|
||||
let spawn_request = FramedMessage {
|
||||
version: 1,
|
||||
message: Message::SpawnRequest {
|
||||
payload: Box::new(request),
|
||||
},
|
||||
};
|
||||
write_frame(pipe_write, &spawn_request)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn read_spawn_ready(pipe_read: &mut File) -> Result<()> {
|
||||
let first = read_frame(pipe_read)?
|
||||
.ok_or_else(|| anyhow::anyhow!("runner pipe closed before spawn_ready"))?;
|
||||
match first.message {
|
||||
Message::SpawnReady { .. } => Ok(()),
|
||||
Message::Error { payload } => Err(anyhow::anyhow!("runner error: {}", payload.message)),
|
||||
other => Err(anyhow::anyhow!("unexpected runner message: {other:?}")),
|
||||
}
|
||||
}
|
||||
|
||||
fn start_runner_stdin_writer(
|
||||
mut writer_rx: mpsc::Receiver<Vec<u8>>,
|
||||
mut pipe_write: File,
|
||||
) -> tokio::task::JoinHandle<()> {
|
||||
tokio::task::spawn_blocking(move || {
|
||||
while let Some(bytes) = writer_rx.blocking_recv() {
|
||||
let msg = FramedMessage {
|
||||
version: 1,
|
||||
message: Message::Stdin {
|
||||
payload: StdinPayload {
|
||||
data_b64: encode_bytes(&bytes),
|
||||
},
|
||||
},
|
||||
};
|
||||
let _ = write_frame(&mut pipe_write, &msg);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn start_runner_stdout_reader(
|
||||
mut pipe_read: File,
|
||||
output_tx: broadcast::Sender<Vec<u8>>,
|
||||
exit_tx: oneshot::Sender<i32>,
|
||||
) {
|
||||
std::thread::spawn(move || loop {
|
||||
let msg = match read_frame(&mut pipe_read) {
|
||||
Ok(Some(v)) => v,
|
||||
Ok(None) => {
|
||||
let _ = exit_tx.send(-1);
|
||||
break;
|
||||
}
|
||||
Err(_err) => {
|
||||
let _ = exit_tx.send(-1);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
match msg.message {
|
||||
Message::Output { payload } => {
|
||||
if let Ok(data) = decode_bytes(&payload.data_b64) {
|
||||
let _ = output_tx.send(data);
|
||||
}
|
||||
}
|
||||
Message::Exit { payload } => {
|
||||
let _ = exit_tx.send(payload.exit_code);
|
||||
break;
|
||||
}
|
||||
Message::Error { payload: _ } => {
|
||||
let _ = exit_tx.send(-1);
|
||||
break;
|
||||
}
|
||||
Message::SpawnReady { .. } => {}
|
||||
Message::Stdin { .. } => {}
|
||||
Message::SpawnRequest { .. } => {}
|
||||
Message::Terminate { .. } => {}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Finalize process exit, emit exit code, and cleanup handles/ACLs.
|
||||
#[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<()>,
|
||||
guards: Vec<PathBuf>,
|
||||
cap_sid: Option<String>,
|
||||
logs_base_dir: Option<&Path>,
|
||||
command: Vec<String>,
|
||||
) {
|
||||
let exit_code = {
|
||||
let mut raw_exit: u32 = 1;
|
||||
if let Ok(guard) = process_handle.lock() {
|
||||
if let Some(handle) = guard.as_ref() {
|
||||
unsafe {
|
||||
GetExitCodeProcess(*handle, &mut raw_exit);
|
||||
}
|
||||
}
|
||||
}
|
||||
raw_exit as i32
|
||||
};
|
||||
|
||||
// Avoid blocking exit propagation if the output reader gets stuck.
|
||||
let _ = exit_tx.send(exit_code);
|
||||
|
||||
let _ = output_join.join();
|
||||
|
||||
unsafe {
|
||||
if thread_handle != 0 && thread_handle != INVALID_HANDLE_VALUE {
|
||||
CloseHandle(thread_handle);
|
||||
}
|
||||
if let Ok(mut guard) = process_handle.lock() {
|
||||
if 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);
|
||||
}
|
||||
|
||||
if !guards.is_empty() {
|
||||
if let Some(cap_sid) = cap_sid {
|
||||
if let Some(sid) = unsafe { convert_string_sid_to_sid(&cap_sid) } {
|
||||
unsafe {
|
||||
for p in guards {
|
||||
revoke_ace(&p, sid);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// exit_tx already sent above.
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
/// Spawn a sandboxed process under a restricted token and return a live session.
|
||||
/// weasel: this is the "non-elevated" path right? If so, this function should be named as such. Is there an opportunity to merge any part of these two spawn_windows_sandbox_... functions?
|
||||
pub 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>,
|
||||
tty: bool,
|
||||
) -> Result<SpawnedProcess> {
|
||||
let LegacyContext {
|
||||
policy,
|
||||
logs_base_dir,
|
||||
is_workspace_write,
|
||||
} = prepare_legacy_context(policy_json_or_preset, codex_home, &mut env_map, &command)?;
|
||||
let current_dir = cwd.to_path_buf();
|
||||
|
||||
if matches!(
|
||||
&policy,
|
||||
SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. }
|
||||
) {
|
||||
anyhow::bail!("DangerFullAccess and ExternalSandbox are not supported for sandboxing")
|
||||
}
|
||||
let (h_token, psid_to_use, cap_sid_str) =
|
||||
create_restricted_token_for_policy(&policy, codex_home)?;
|
||||
allow_null_device_for_workspace_write(is_workspace_write);
|
||||
|
||||
let persist_aces = is_workspace_write;
|
||||
let guards = apply_legacy_acl_rules(
|
||||
&policy,
|
||||
sandbox_policy_cwd,
|
||||
¤t_dir,
|
||||
&env_map,
|
||||
psid_to_use,
|
||||
persist_aces,
|
||||
);
|
||||
|
||||
let (writer_tx, writer_rx) = mpsc::channel::<Vec<u8>>(128);
|
||||
let (output_tx, output_rx) = broadcast::channel::<Vec<u8>>(256);
|
||||
let (exit_tx, exit_rx) = oneshot::channel::<i32>();
|
||||
|
||||
let LegacyProcessHandles {
|
||||
process: pi,
|
||||
output_join,
|
||||
writer_handle,
|
||||
hpc,
|
||||
} = spawn_legacy_process(
|
||||
h_token,
|
||||
&command,
|
||||
cwd,
|
||||
&env_map,
|
||||
tty,
|
||||
output_tx.clone(),
|
||||
writer_rx,
|
||||
)?;
|
||||
|
||||
let process_handle = Arc::new(StdMutex::new(Some(pi.hProcess)));
|
||||
let wait_handle = Arc::clone(&process_handle);
|
||||
let command_for_wait = command.clone();
|
||||
let cap_sid_for_wait = if persist_aces {
|
||||
None
|
||||
} else {
|
||||
Some(cap_sid_str.clone())
|
||||
};
|
||||
let guards_for_wait = if persist_aces { Vec::new() } else { guards };
|
||||
std::thread::spawn(move || {
|
||||
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() {
|
||||
if let Some(handle) = guard.as_ref() {
|
||||
let _ = TerminateProcess(*handle, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(hpc) = hpc {
|
||||
unsafe {
|
||||
ClosePseudoConsole(hpc);
|
||||
}
|
||||
}
|
||||
finalize_exit(
|
||||
exit_tx,
|
||||
wait_handle,
|
||||
pi.hThread,
|
||||
output_join,
|
||||
guards_for_wait,
|
||||
cap_sid_for_wait,
|
||||
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() {
|
||||
if let Some(handle) = guard.as_ref() {
|
||||
unsafe {
|
||||
let _ = TerminateProcess(*handle, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}) as Box<dyn FnMut() + Send + Sync>)
|
||||
};
|
||||
|
||||
let driver = ProcessDriver {
|
||||
writer_tx,
|
||||
output_rx,
|
||||
exit_rx,
|
||||
terminator,
|
||||
writer_handle: Some(writer_handle),
|
||||
};
|
||||
|
||||
Ok(spawn_from_driver(driver))
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
/// Spawn a sandboxed process via the elevated runner IPC path and return a live session.
|
||||
/// weasel: this method is *MASSIVE* - let's think about how to break some of it into helper functions or something
|
||||
pub async fn spawn_windows_sandbox_session_elevated(
|
||||
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>,
|
||||
tty: bool,
|
||||
) -> Result<SpawnedProcess> {
|
||||
let _ = timeout_ms;
|
||||
let policy = parse_policy(policy_json_or_preset)?;
|
||||
normalize_null_device_env(&mut env_map);
|
||||
ensure_non_interactive_pager(&mut env_map);
|
||||
inherit_path_env(&mut env_map);
|
||||
let current_dir = cwd.to_path_buf();
|
||||
let sandbox_base = codex_home.join(".sandbox");
|
||||
ensure_codex_home_exists(&sandbox_base)?;
|
||||
let logs_base_dir = Some(sandbox_base.clone());
|
||||
log_start(&command, logs_base_dir.as_deref());
|
||||
let sandbox_creds =
|
||||
require_logon_sandbox_creds(&policy, sandbox_policy_cwd, cwd, &env_map, codex_home)?;
|
||||
if matches!(
|
||||
&policy,
|
||||
SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. }
|
||||
) {
|
||||
anyhow::bail!("DangerFullAccess and ExternalSandbox are not supported for sandboxing")
|
||||
}
|
||||
let caps = load_or_create_cap_sids(codex_home)?;
|
||||
let (psid_to_use, cap_sid_str) = match &policy {
|
||||
SandboxPolicy::ReadOnly => (
|
||||
unsafe { convert_string_sid_to_sid(&caps.readonly).unwrap() },
|
||||
caps.readonly.clone(),
|
||||
),
|
||||
SandboxPolicy::WorkspaceWrite { .. } => (
|
||||
unsafe { convert_string_sid_to_sid(&caps.workspace).unwrap() },
|
||||
caps.workspace.clone(),
|
||||
),
|
||||
SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. } => {
|
||||
unreachable!("DangerFullAccess handled above")
|
||||
}
|
||||
};
|
||||
let AllowDenyPaths { allow: _, deny: _ } =
|
||||
compute_allow_paths(&policy, sandbox_policy_cwd, ¤t_dir, &env_map);
|
||||
unsafe {
|
||||
allow_null_device(psid_to_use);
|
||||
}
|
||||
|
||||
let (pipe_in, pipe_out) = pipe_pair();
|
||||
let (mut pipe_write, mut pipe_read) =
|
||||
launch_runner_pipes(cwd, &sandbox_creds, pipe_in, pipe_out)?;
|
||||
|
||||
let spawn_request = 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_sid: cap_sid_str.clone(),
|
||||
timeout_ms: None,
|
||||
tty,
|
||||
};
|
||||
send_spawn_request(&mut pipe_write, spawn_request)?;
|
||||
read_spawn_ready(&mut pipe_read)?;
|
||||
|
||||
let (writer_tx, writer_rx) = mpsc::channel::<Vec<u8>>(128);
|
||||
let (output_tx, output_rx) = broadcast::channel::<Vec<u8>>(256);
|
||||
let (exit_tx, exit_rx) = oneshot::channel::<i32>();
|
||||
|
||||
let writer_handle = start_runner_stdin_writer(writer_rx, pipe_write.try_clone()?);
|
||||
|
||||
let pipe_write = Arc::new(StdMutex::new(pipe_write));
|
||||
let terminator = {
|
||||
let pipe_write = Arc::clone(&pipe_write);
|
||||
Some(Box::new(move || {
|
||||
if let Ok(mut guard) = pipe_write.lock() {
|
||||
let msg = FramedMessage {
|
||||
version: 1,
|
||||
message: Message::Terminate {
|
||||
payload: EmptyPayload::default(),
|
||||
},
|
||||
};
|
||||
let _ = write_frame(&mut *guard, &msg);
|
||||
}
|
||||
}) as Box<dyn FnMut() + Send + Sync>)
|
||||
};
|
||||
|
||||
start_runner_stdout_reader(pipe_read, output_tx, exit_tx);
|
||||
|
||||
Ok(spawn_from_driver(ProcessDriver {
|
||||
writer_tx,
|
||||
output_rx,
|
||||
exit_rx,
|
||||
terminator,
|
||||
writer_handle: Some(writer_handle),
|
||||
}))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user