Tighten Windows launch resolution boundary

This commit is contained in:
jif-oai
2026-06-26 15:44:22 +01:00
parent 7424930928
commit 4ea13a7c81
18 changed files with 111 additions and 214 deletions
+1 -5
View File
@@ -473,7 +473,6 @@ async fn run_command_under_windows_session(
) -> ! {
use codex_core::windows_sandbox::WindowsSandboxLevelExt;
use codex_protocol::config_types::WindowsSandboxLevel;
use codex_windows_sandbox::WindowsProcessLaunch;
use codex_windows_sandbox::WindowsSandboxProxySettingsMode;
use codex_windows_sandbox::WindowsSandboxSessionRequest;
use codex_windows_sandbox::spawn_windows_sandbox_session_for_level;
@@ -483,10 +482,7 @@ async fn run_command_under_windows_session(
permission_profile,
workspace_roots: workspace_roots.as_slice(),
codex_home: config.codex_home.as_path(),
launch: WindowsProcessLaunch {
application_path: None,
command,
},
command,
cwd: cwd.as_path(),
env_map: env,
windows_sandbox_level: WindowsSandboxLevel::from_config(config),
@@ -995,10 +995,7 @@ impl UnifiedExecProcessManager {
permission_profile: &request.permission_profile,
workspace_roots: request.windows_sandbox_workspace_roots.as_slice(),
codex_home: codex_home.as_ref(),
launch: codex_windows_sandbox::WindowsProcessLaunch {
application_path: None,
command: request.command.clone(),
},
command: request.command.clone(),
cwd: native_cwd.as_path(),
env_map: request.env.clone(),
windows_sandbox_level: request.windows_sandbox_level,
@@ -1,4 +1,5 @@
use crate::process::WindowsProcessLaunch;
use crate::env::windows_env_value;
use crate::process::ResolvedWindowsProcessLaunch;
use anyhow::Context;
use anyhow::Result;
use anyhow::anyhow;
@@ -88,14 +89,15 @@ pub fn resolve_windows_executable(
}
pub(crate) fn resolve_windows_launch(
mut launch: WindowsProcessLaunch,
command: Vec<String>,
cwd: &Path,
env_map: &HashMap<String, String>,
) -> Result<WindowsProcessLaunch> {
if launch.application_path.is_none() {
launch.application_path = Some(resolve_windows_executable(&launch.command, cwd, env_map)?);
}
Ok(launch)
) -> Result<ResolvedWindowsProcessLaunch> {
let application_path = resolve_windows_executable(&command, cwd, env_map)?;
Ok(ResolvedWindowsProcessLaunch {
application_path,
command,
})
}
fn windows_search_dirs(cwd: &Path, env_map: &HashMap<String, String>) -> Vec<PathBuf> {
@@ -161,18 +163,6 @@ fn is_drive_relative(path: &Path) -> bool {
)
}
fn windows_env_value<'a>(env_map: &'a HashMap<String, String>, key: &str) -> Option<&'a str> {
env_map
.get(key)
.or_else(|| {
env_map
.iter()
.find(|(existing, _)| existing.eq_ignore_ascii_case(key))
.map(|(_, value)| value)
})
.map(String::as_str)
}
#[cfg(test)]
#[path = "command_resolution_tests.rs"]
mod tests;
@@ -1,19 +1,12 @@
use super::resolve_windows_executable;
use super::resolve_windows_launch;
use crate::process::WindowsProcessLaunch;
use crate::process::ResolvedWindowsProcessLaunch;
use pretty_assertions::assert_eq;
use std::collections::HashMap;
use std::fs;
use std::path::PathBuf;
use tempfile::TempDir;
fn unresolved(program: &str) -> WindowsProcessLaunch {
WindowsProcessLaunch {
application_path: None,
command: vec![program.to_string(), "argument".to_string()],
}
}
#[test]
fn resolves_with_case_insensitive_child_path_and_pathext() {
let temp = TempDir::new().expect("tempdir");
@@ -28,13 +21,17 @@ fn resolves_with_case_insensitive_child_path_and_pathext() {
("PathExt".to_string(), ".BIN".to_string()),
]);
let launch = resolve_windows_launch(unresolved("child-only"), &cwd, &env)
.expect("resolve from child environment");
let launch = resolve_windows_launch(
vec!["child-only".to_string(), "argument".to_string()],
&cwd,
&env,
)
.expect("resolve from child environment");
assert_eq!(
launch,
WindowsProcessLaunch {
application_path: Some(executable),
ResolvedWindowsProcessLaunch {
application_path: executable,
command: vec!["child-only".to_string(), "argument".to_string()],
}
);
@@ -8,7 +8,7 @@
use crate::desktop::LaunchDesktop;
use crate::proc_thread_attr::ProcThreadAttributeList;
use crate::process::WindowsProcessLaunch;
use crate::process::ResolvedWindowsProcessLaunch;
use crate::winutil::format_last_error;
use crate::winutil::quote_windows_arg;
use crate::winutil::to_wide;
@@ -92,7 +92,7 @@ pub fn create_conpty(cols: i16, rows: i16) -> Result<ConptyInstance> {
#[doc(hidden)]
pub fn spawn_conpty_process_as_user(
h_token: HANDLE,
launch: &WindowsProcessLaunch,
launch: &ResolvedWindowsProcessLaunch,
cwd: &Path,
env_map: &HashMap<String, String>,
use_private_desktop: bool,
@@ -105,10 +105,7 @@ pub fn spawn_conpty_process_as_user(
.collect::<Vec<_>>()
.join(" ");
let mut cmdline: Vec<u16> = to_wide(&cmdline_str);
let application_name = launch.application_path.as_deref().map(to_wide);
let application_name_ptr = application_name
.as_ref()
.map_or(std::ptr::null(), Vec::as_ptr);
let application_name = to_wide(&launch.application_path);
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;
@@ -136,7 +133,7 @@ pub fn spawn_conpty_process_as_user(
let ok = unsafe {
CreateProcessAsUserW(
h_token,
application_name_ptr,
application_name.as_ptr(),
cmdline.as_mut_ptr(),
std::ptr::null_mut(),
std::ptr::null_mut(),
@@ -7,7 +7,7 @@
//! not use this protocol, and non‑unified exec capture uses it only when running
//! through the elevated runner.
use crate::process::WindowsProcessLaunch;
use crate::process::ResolvedWindowsProcessLaunch;
use anyhow::Result;
use base64::Engine as _;
use base64::engine::general_purpose::STANDARD;
@@ -58,7 +58,7 @@ pub enum Message {
/// Spawn parameters sent from parent to runner.
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct SpawnRequest {
pub launch: WindowsProcessLaunch,
pub launch: ResolvedWindowsProcessLaunch,
pub cwd: PathBuf,
pub env: HashMap<String, String>,
pub permission_profile: PermissionProfile,
@@ -213,8 +213,8 @@ mod tests {
AbsolutePathBuf::from_absolute_path(PathBuf::from(r"C:\workspace"))
.expect("absolute workspace root"),
];
let launch = WindowsProcessLaunch {
application_path: Some(PathBuf::from(r"C:\Windows\System32\cmd.exe")),
let launch = ResolvedWindowsProcessLaunch {
application_path: PathBuf::from(r"C:\Windows\System32\cmd.exe"),
command: vec!["cmd.exe".to_string(), "/c".to_string(), "ver".to_string()],
};
let msg = FramedMessage {
@@ -7,7 +7,7 @@ use crate::ipc_framed::Message;
use crate::ipc_framed::SpawnRequest;
use crate::ipc_framed::read_frame;
use crate::ipc_framed::write_frame;
use crate::process::WindowsProcessLaunch;
use crate::process::ResolvedWindowsProcessLaunch;
use crate::runner_pipe::PIPE_ACCESS_INBOUND;
use crate::runner_pipe::PIPE_ACCESS_OUTBOUND;
use crate::runner_pipe::connect_pipe;
@@ -103,24 +103,18 @@ fn is_refreshable_windows_error(code: u32) -> bool {
matches!(code, ERROR_LOGON_FAILURE | ERROR_NO_SUCH_LOGON_SESSION)
}
fn command_targets_windows_apps(launch: &WindowsProcessLaunch) -> bool {
launch
.application_path
.as_deref()
.or_else(|| launch.command.first().map(Path::new))
.is_some_and(|program| {
program.components().any(|component| {
component
.as_os_str()
.to_string_lossy()
.eq_ignore_ascii_case("WindowsApps")
})
})
fn command_targets_windows_apps(launch: &ResolvedWindowsProcessLaunch) -> bool {
launch.application_path.components().any(|component| {
component
.as_os_str()
.to_string_lossy()
.eq_ignore_ascii_case("WindowsApps")
})
}
pub(crate) fn is_refreshable_sandbox_creds_error(
err: &anyhow::Error,
launch: &WindowsProcessLaunch,
launch: &ResolvedWindowsProcessLaunch,
) -> bool {
if err
.downcast_ref::<RunnerLogonError>()
@@ -143,7 +137,7 @@ pub(crate) fn is_refreshable_sandbox_creds_error(
pub(crate) fn retry_runner_spawn_once<T>(
sandbox_creds: SandboxCreds,
launch: &WindowsProcessLaunch,
launch: &ResolvedWindowsProcessLaunch,
mut spawn: impl FnMut(SandboxCreds) -> Result<T>,
refresh: impl FnOnce() -> Result<SandboxCreds>,
) -> Result<T> {
@@ -501,7 +495,7 @@ mod tests {
use super::is_refreshable_sandbox_creds_error;
use crate::ipc_framed::ErrorPayload;
use crate::ipc_framed::ErrorStage;
use crate::process::WindowsProcessLaunch;
use crate::process::ResolvedWindowsProcessLaunch;
use pretty_assertions::assert_eq;
use std::path::PathBuf;
use windows_sys::Win32::Foundation::ERROR_LOGON_FAILURE;
@@ -510,8 +504,8 @@ mod tests {
#[test]
fn refreshable_sandbox_creds_error_recognizes_credential_and_child_start_failures() {
let launch = WindowsProcessLaunch {
application_path: None,
let launch = ResolvedWindowsProcessLaunch {
application_path: PathBuf::from(r"C:\Windows\System32\cmd.exe"),
command: vec!["cmd.exe".to_string()],
};
assert_eq!(
@@ -546,16 +540,16 @@ mod tests {
);
let windows_apps_launches = [
WindowsProcessLaunch {
application_path: Some(PathBuf::from(
ResolvedWindowsProcessLaunch {
application_path: PathBuf::from(
r"C:\Users\user\AppData\Local\Microsoft\WindowsApps\pwsh.exe",
)),
),
command: vec!["pwsh.exe".to_string()],
},
WindowsProcessLaunch {
application_path: Some(PathBuf::from(
ResolvedWindowsProcessLaunch {
application_path: PathBuf::from(
r"C:\Program Files\WindowsApps\Microsoft.PowerShell_7.6.3.0_x64__8wekyb3d8bbwe\pwsh.exe",
)),
),
command: vec!["pwsh.exe".to_string()],
},
];
@@ -24,6 +24,7 @@ pub struct ElevatedSandboxProfileCaptureRequest<'a> {
mod windows_impl {
use super::ElevatedSandboxProfileCaptureRequest;
use crate::env::inherit_path_env;
use crate::identity::refresh_logon_sandbox_creds;
use crate::ipc_framed::EmptyPayload;
use crate::ipc_framed::FramedMessage;
@@ -35,7 +36,6 @@ mod windows_impl {
use crate::ipc_framed::write_frame;
use crate::logging::log_failure;
use crate::logging::log_success;
use crate::process::WindowsProcessLaunch;
use crate::resolved_permissions::ResolvedWindowsSandboxPermissions;
use crate::runner_client::retry_runner_spawn_once;
use crate::runner_client::spawn_runner_transport;
@@ -118,15 +118,13 @@ mod windows_impl {
.iter()
.map(AbsolutePathBuf::to_path_buf)
.collect::<Vec<_>>();
inherit_path_env(&mut env_map);
let elevated = prepare_elevated_spawn_context_for_permissions(
permissions.clone(),
codex_home,
cwd,
&mut env_map,
WindowsProcessLaunch {
application_path: None,
command: command.clone(),
},
command.clone(),
read_roots_override,
read_roots_include_platform_defaults,
write_roots_override,
+16 -10
View File
@@ -31,7 +31,7 @@ pub fn ensure_non_interactive_pager(env_map: &mut HashMap<String, String>) {
env_map.entry("LESS".into()).or_insert_with(|| "".into());
}
// Keep PATH and PATHEXT stable for callers that rely on inheriting the parent process env.
// Capture APIs accept partial environments, so keep PATH and PATHEXT stable for those callers.
pub fn inherit_path_env(env_map: &mut HashMap<String, String>) {
if windows_env_value(env_map, "PATH").is_none()
&& let Ok(path) = env::var("PATH")
@@ -47,7 +47,7 @@ pub fn inherit_path_env(env_map: &mut HashMap<String, String>) {
fn prepend_path(env_map: &mut HashMap<String, String>, prefix: &str) {
let existing = windows_env_value(env_map, "PATH")
.cloned()
.map(ToOwned::to_owned)
.unwrap_or_default();
let parts: Vec<String> = existing.split(';').map(ToString::to_string).collect();
if parts
@@ -68,7 +68,7 @@ fn prepend_path(env_map: &mut HashMap<String, String>, prefix: &str) {
fn reorder_pathext_for_stubs(env_map: &mut HashMap<String, String>) {
let default = windows_env_value(env_map, "PATHEXT")
.cloned()
.map(ToOwned::to_owned)
.unwrap_or(".COM;.EXE;.BAT;.CMD".to_string());
let exts: Vec<String> = default
.split(';')
@@ -98,13 +98,19 @@ fn reorder_pathext_for_stubs(env_map: &mut HashMap<String, String>) {
set_windows_env_value(env_map, "PATHEXT", combined.join(";"));
}
fn windows_env_value<'a>(env_map: &'a HashMap<String, String>, key: &str) -> Option<&'a String> {
env_map.get(key).or_else(|| {
env_map
.iter()
.find(|(existing, _)| existing.eq_ignore_ascii_case(key))
.map(|(_, value)| value)
})
pub(crate) fn windows_env_value<'a>(
env_map: &'a HashMap<String, String>,
key: &str,
) -> Option<&'a str> {
env_map
.get(key)
.or_else(|| {
env_map
.iter()
.find(|(existing, _)| existing.eq_ignore_ascii_case(key))
.map(|(_, value)| value)
})
.map(String::as_str)
}
fn set_windows_env_value(env_map: &mut HashMap<String, String>, key: &str, value: String) {
+5 -8
View File
@@ -241,13 +241,13 @@ pub use path_normalization::canonicalize_path;
#[cfg(target_os = "windows")]
pub use process::PipeSpawnHandles;
#[cfg(target_os = "windows")]
#[doc(hidden)]
pub use process::ResolvedWindowsProcessLaunch;
#[cfg(target_os = "windows")]
pub use process::StderrMode;
#[cfg(target_os = "windows")]
pub use process::StdinMode;
#[cfg(target_os = "windows")]
#[doc(hidden)]
pub use process::WindowsProcessLaunch;
#[cfg(target_os = "windows")]
pub use process::read_handle_loop;
#[cfg(target_os = "windows")]
pub use process::spawn_process_with_pipes;
@@ -351,10 +351,10 @@ pub use stub::run_windows_sandbox_legacy_preflight;
#[cfg(target_os = "windows")]
mod windows_impl {
use super::WindowsSandboxCancellationToken;
use super::command_resolution::resolve_windows_launch;
use super::logging::log_failure;
use super::logging::log_success;
use super::process::CreateProcessAsUserRequest;
use super::process::WindowsProcessLaunch;
use super::process::create_process_as_user;
use super::sandbox_utils::ensure_codex_home_exists;
use super::spawn_prep::LegacyAclSids;
@@ -564,10 +564,7 @@ mod windows_impl {
)?;
let (stdin_pair, stdout_pair, stderr_pair) = unsafe { setup_stdio_pipes()? };
let ((in_r, in_w), (out_r, out_w), (err_r, err_w)) = (stdin_pair, stdout_pair, stderr_pair);
let launch = WindowsProcessLaunch {
application_path: None,
command: command.clone(),
};
let launch = resolve_windows_launch(command.clone(), cwd, &env_map)?;
let spawn_res = unsafe {
create_process_as_user(
security.h_token,
+8 -12
View File
@@ -39,14 +39,11 @@ pub struct CreatedProcess {
_desktop: LaunchDesktop,
}
/// The command line and optional executable path supplied to a Windows process launch.
///
/// Callers that have already resolved an executable can set `application_path` so it is passed as
/// `lpApplicationName`. A value of `None` preserves Windows' command-line lookup behavior.
/// A Windows process launch whose executable was resolved from the final child environment.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[doc(hidden)]
pub struct WindowsProcessLaunch {
pub application_path: Option<PathBuf>,
pub struct ResolvedWindowsProcessLaunch {
pub application_path: PathBuf,
pub command: Vec<String>,
}
@@ -87,7 +84,7 @@ unsafe fn ensure_inheritable_stdio(si: &mut STARTUPINFOW) -> Result<()> {
}
pub(crate) struct CreateProcessAsUserRequest<'a> {
pub(crate) launch: &'a WindowsProcessLaunch,
pub(crate) launch: &'a ResolvedWindowsProcessLaunch,
pub(crate) cwd: &'a Path,
pub(crate) env_map: &'a HashMap<String, String>,
pub(crate) logs_base_dir: Option<&'a Path>,
@@ -112,8 +109,7 @@ pub(crate) unsafe fn create_process_as_user(
} = request;
let cmdline_str = argv_to_command_line(&launch.command);
let mut cmdline: Vec<u16> = to_wide(&cmdline_str);
let application_name = launch.application_path.as_deref().map(to_wide);
let application_name_ptr = application_name.as_ref().map_or(ptr::null(), Vec::as_ptr);
let application_name = to_wide(&launch.application_path);
let env_block = make_env_block(env_map);
let desktop = LaunchDesktop::prepare(use_private_desktop, logs_base_dir)?;
let mut pi: PROCESS_INFORMATION = std::mem::zeroed();
@@ -150,7 +146,7 @@ pub(crate) unsafe fn create_process_as_user(
let creation_flags = CREATE_UNICODE_ENVIRONMENT | EXTENDED_STARTUPINFO_PRESENT;
let ok = CreateProcessAsUserW(
h_token,
application_name_ptr,
application_name.as_ptr(),
cmdline.as_mut_ptr(),
std::ptr::null_mut(),
std::ptr::null_mut(),
@@ -190,7 +186,7 @@ pub(crate) unsafe fn create_process_as_user(
let creation_flags = CREATE_UNICODE_ENVIRONMENT;
let ok = CreateProcessAsUserW(
h_token,
application_name_ptr,
application_name.as_ptr(),
cmdline.as_mut_ptr(),
std::ptr::null_mut(),
std::ptr::null_mut(),
@@ -253,7 +249,7 @@ pub struct PipeSpawnHandles {
#[allow(clippy::too_many_arguments)]
pub fn spawn_process_with_pipes(
h_token: HANDLE,
launch: &WindowsProcessLaunch,
launch: &ResolvedWindowsProcessLaunch,
cwd: &Path,
env_map: &HashMap<String, String>,
stdin_mode: StdinMode,
+5 -10
View File
@@ -12,13 +12,12 @@ use crate::command_resolution::resolve_windows_launch;
use crate::deny_read_state::sync_persistent_deny_read_acls;
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::SandboxCreds;
use crate::identity::require_logon_sandbox_creds;
use crate::logging::log_start;
use crate::path_normalization::canonicalize_path;
use crate::process::WindowsProcessLaunch;
use crate::process::ResolvedWindowsProcessLaunch;
use crate::resolved_permissions::ResolvedWindowsSandboxPermissions;
use crate::sandbox_utils::ensure_codex_home_exists;
use crate::sandbox_utils::inject_git_safe_directory;
@@ -55,7 +54,7 @@ pub(crate) struct ElevatedSpawnContext {
pub(crate) logs_base_dir: Option<PathBuf>,
pub(crate) sandbox_creds: SandboxCreds,
pub(crate) cap_sids: Vec<String>,
pub(crate) launch: WindowsProcessLaunch,
pub(crate) launch: ResolvedWindowsProcessLaunch,
pub(crate) read_roots_override: Vec<PathBuf>,
}
@@ -355,7 +354,7 @@ pub(crate) fn prepare_elevated_spawn_context_for_permissions(
codex_home: &Path,
cwd: &Path,
env_map: &mut HashMap<String, String>,
launch: WindowsProcessLaunch,
command: Vec<String>,
read_roots_override: Option<&[PathBuf]>,
read_roots_include_platform_defaults: bool,
write_roots_override: Option<&[PathBuf]>,
@@ -366,9 +365,8 @@ pub(crate) fn prepare_elevated_spawn_context_for_permissions(
) -> Result<ElevatedSpawnContext> {
normalize_null_device_env(env_map);
ensure_non_interactive_pager(env_map);
inherit_path_env(env_map);
inject_git_safe_directory(env_map, cwd);
let launch = resolve_windows_launch(launch, cwd, env_map)?;
let launch = resolve_windows_launch(command, cwd, env_map)?;
// Use a temp-based log dir that the sandbox user can write.
let sandbox_base = codex_home.join(".sandbox");
@@ -407,10 +405,7 @@ pub(crate) fn prepare_elevated_spawn_context_for_permissions(
let mut resolved_read_roots = read_roots_override
.map(<[PathBuf]>::to_vec)
.unwrap_or_else(|| gather_read_roots(cwd, &permissions, env_map, codex_home));
let application_path = launch
.application_path
.as_ref()
.ok_or_else(|| anyhow::anyhow!("resolved Windows launch is missing an application path"))?;
let application_path = &launch.application_path;
if !resolved_read_roots.contains(application_path) {
resolved_read_roots.push(application_path.clone());
}
@@ -10,7 +10,6 @@ use crate::ipc_framed::FramedMessage;
use crate::ipc_framed::IPC_PROTOCOL_VERSION;
use crate::ipc_framed::Message;
use crate::ipc_framed::SpawnRequest;
use crate::process::WindowsProcessLaunch;
use crate::resolved_permissions::ResolvedWindowsSandboxPermissions;
use crate::runner_client::RunnerTransport;
use crate::runner_client::retry_runner_spawn_once;
@@ -113,7 +112,7 @@ pub(crate) async fn spawn_windows_sandbox_session_elevated_for_permission_profil
permission_profile: &PermissionProfile,
workspace_roots: &[AbsolutePathBuf],
codex_home: &Path,
launch: WindowsProcessLaunch,
command: Vec<String>,
cwd: &Path,
mut env_map: HashMap<String, String>,
proxy_enforced: bool,
@@ -146,7 +145,7 @@ pub(crate) async fn spawn_windows_sandbox_session_elevated_for_permission_profil
codex_home,
cwd,
&mut env_map,
launch,
command,
read_roots_override,
read_roots_include_platform_defaults,
write_roots_override,
@@ -5,7 +5,7 @@ use crate::identity::SandboxCreds;
use crate::ipc_framed::ErrorPayload;
use crate::ipc_framed::ErrorStage;
use crate::ipc_framed::SpawnRequest;
use crate::process::WindowsProcessLaunch;
use crate::process::ResolvedWindowsProcessLaunch;
use crate::resolved_permissions::ResolvedWindowsSandboxPermissions;
use crate::runner_client::RunnerStartupError;
use codex_protocol::models::PermissionProfile;
@@ -69,8 +69,8 @@ fn retry_uses_original_unified_exec_request_and_stops_after_second_failure() {
env_map: env_map.clone(),
logs_base_dir: Some(PathBuf::from(r"C:\Users\codex\.sandbox")),
spawn_request: SpawnRequest {
launch: WindowsProcessLaunch {
application_path: Some(PathBuf::from(r"C:\Program Files\PowerShell\7\pwsh.exe")),
launch: ResolvedWindowsProcessLaunch {
application_path: PathBuf::from(r"C:\Program Files\PowerShell\7\pwsh.exe"),
command: vec!["pwsh.exe".to_string(), "-NoProfile".to_string()],
},
cwd: PathBuf::from(r"C:\workspace"),
@@ -5,9 +5,9 @@ use crate::conpty::spawn_conpty_process_as_user;
use crate::desktop::LaunchDesktop;
use crate::logging::log_failure;
use crate::logging::log_success;
use crate::process::ResolvedWindowsProcessLaunch;
use crate::process::StderrMode;
use crate::process::StdinMode;
use crate::process::WindowsProcessLaunch;
use crate::process::read_handle_loop;
use crate::process::spawn_process_with_pipes;
use crate::spawn_prep::LegacyAclSids;
@@ -60,7 +60,7 @@ struct LegacyProcessHandles {
#[allow(clippy::too_many_arguments)]
fn spawn_legacy_process(
h_token: HANDLE,
launch: &WindowsProcessLaunch,
launch: &ResolvedWindowsProcessLaunch,
cwd: &Path,
env_map: &HashMap<String, String>,
use_private_desktop: bool,
@@ -275,7 +275,7 @@ pub(crate) async fn spawn_windows_sandbox_session_legacy(
permission_profile: &PermissionProfile,
workspace_roots: &[AbsolutePathBuf],
codex_home: &Path,
launch: WindowsProcessLaunch,
command: Vec<String>,
cwd: &Path,
mut env_map: HashMap<String, String>,
timeout_ms: Option<u64>,
@@ -291,13 +291,13 @@ pub(crate) async fn spawn_windows_sandbox_session_legacy(
codex_home,
cwd,
&mut env_map,
&launch.command,
&command,
SpawnPrepOptions {
inherit_path: false,
add_git_safe_directory: false,
},
)?;
let launch = resolve_windows_launch(launch, cwd, &env_map)?;
let launch = resolve_windows_launch(command, cwd, &env_map)?;
if !common.permissions.has_full_disk_read_access() {
anyhow::bail!("Restricted read-only access requires the elevated Windows sandbox backend");
}
@@ -9,7 +9,6 @@
mod backends;
use crate::process::WindowsProcessLaunch;
use anyhow::Result;
use codex_protocol::config_types::WindowsSandboxLevel;
use codex_protocol::models::PermissionProfile;
@@ -19,7 +18,7 @@ use std::collections::HashMap;
use std::path::Path;
use std::path::PathBuf;
/// Fully resolved Windows sandbox session launch request.
/// Windows sandbox session launch request.
///
/// Callers should parse their own input shape first, then use this request to
/// share the elevated-vs-legacy backend selection and session launch path.
@@ -27,7 +26,7 @@ pub struct WindowsSandboxSessionRequest<'a> {
pub permission_profile: &'a PermissionProfile,
pub workspace_roots: &'a [AbsolutePathBuf],
pub codex_home: &'a Path,
pub launch: WindowsProcessLaunch,
pub command: Vec<String>,
pub cwd: &'a Path,
pub env_map: HashMap<String, String>,
pub windows_sandbox_level: WindowsSandboxLevel,
@@ -54,7 +53,7 @@ pub async fn spawn_windows_sandbox_session_for_level(
request.permission_profile,
request.workspace_roots,
request.codex_home,
request.launch,
request.command,
request.cwd,
request.env_map,
request.proxy_enforced,
@@ -75,7 +74,7 @@ pub async fn spawn_windows_sandbox_session_for_level(
request.permission_profile,
request.workspace_roots,
request.codex_home,
request.launch,
request.command,
request.cwd,
request.env_map,
request.timeout_ms,
@@ -1,15 +1,11 @@
#![cfg(target_os = "windows")]
use super::WindowsSandboxSessionRequest;
use super::backends::legacy::spawn_windows_sandbox_session_legacy;
use super::spawn_windows_sandbox_session_for_level;
use crate::WindowsProcessLaunch;
use crate::WindowsSandboxCancellationToken;
use crate::ipc_framed::Message;
use crate::ipc_framed::decode_bytes;
use crate::ipc_framed::read_frame;
use crate::run_windows_sandbox_capture;
use codex_protocol::config_types::WindowsSandboxLevel;
use codex_protocol::models::PermissionProfile;
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_pty::ProcessDriver;
@@ -87,13 +83,6 @@ fn workspace_roots_for(root: &Path) -> Vec<AbsolutePathBuf> {
vec![AbsolutePathBuf::from_absolute_path(root).expect("absolute workspace root")]
}
fn unresolved_launch(command: Vec<String>) -> WindowsProcessLaunch {
WindowsProcessLaunch {
application_path: None,
command,
}
}
fn wait_for_frame_count(frames_path: &Path, expected_frames: usize) -> Vec<Message> {
let deadline = Instant::now() + Duration::from_secs(2);
loop {
@@ -173,11 +162,11 @@ fn legacy_non_tty_cmd_emits_output() {
&permission_profile,
workspace_roots_for(cwd.as_path()).as_slice(),
codex_home.path(),
unresolved_launch(vec![
vec![
"C:\\Windows\\System32\\cmd.exe".to_string(),
"/c".to_string(),
"echo LEGACY-NONTTY-CMD".to_string(),
]),
],
cwd.as_path(),
HashMap::new(),
Some(5_000),
@@ -199,56 +188,6 @@ fn legacy_non_tty_cmd_emits_output() {
});
}
#[test]
fn legacy_non_tty_uses_explicit_application_path() {
let _guard = legacy_process_test_guard();
let runtime = current_thread_runtime();
runtime.block_on(async move {
let cwd = sandbox_cwd();
let codex_home = sandbox_home("legacy-explicit-application-path");
let permission_profile = PermissionProfile::workspace_write();
let workspace_roots = workspace_roots_for(cwd.as_path());
let spawned = spawn_windows_sandbox_session_for_level(WindowsSandboxSessionRequest {
permission_profile: &permission_profile,
workspace_roots: workspace_roots.as_slice(),
codex_home: codex_home.path(),
launch: WindowsProcessLaunch {
application_path: Some(PathBuf::from(r"C:\Windows\System32\cmd.exe")),
command: vec![
"not-the-real-executable.exe".to_string(),
"/c".to_string(),
"echo EXPLICIT-APPLICATION-PATH".to_string(),
],
},
cwd: cwd.as_path(),
env_map: HashMap::new(),
windows_sandbox_level: WindowsSandboxLevel::RestrictedToken,
proxy_enforced: false,
proxy_settings_mode: crate::WindowsSandboxProxySettingsMode::Reconcile,
timeout_ms: Some(5_000),
read_roots_override: None,
read_roots_include_platform_defaults: false,
write_roots_override: None,
deny_read_paths_override: &[],
deny_write_paths_override: &[],
tty: false,
stdin_open: false,
use_private_desktop: true,
})
.await
.expect("spawn with explicit application path");
let (stdout, exit_code) =
collect_stdout_and_exit(spawned, codex_home.path(), Duration::from_secs(10)).await;
let stdout = String::from_utf8_lossy(&stdout);
assert_eq!(exit_code, 0, "stdout={stdout:?}");
assert!(
stdout.contains("EXPLICIT-APPLICATION-PATH"),
"stdout={stdout:?}"
);
});
}
#[test]
fn legacy_resolves_request_pathext() {
let _guard = legacy_process_test_guard();
@@ -312,11 +251,11 @@ fn legacy_non_tty_cmd_rejects_deny_read_overrides() {
&permission_profile,
workspace_roots_for(cwd.as_path()).as_slice(),
codex_home.path(),
unresolved_launch(vec![
vec![
"C:\\Windows\\System32\\cmd.exe".to_string(),
"/c".to_string(),
"echo deny-read".to_string(),
]),
],
cwd.as_path(),
HashMap::new(),
Some(5_000),
@@ -352,12 +291,12 @@ fn legacy_non_tty_powershell_emits_output() {
&permission_profile,
workspace_roots_for(cwd.as_path()).as_slice(),
codex_home.path(),
unresolved_launch(vec![
vec![
pwsh.display().to_string(),
"-NoProfile".to_string(),
"-Command".to_string(),
"Write-Output LEGACY-NONTTY-DIRECT".to_string(),
]),
],
cwd.as_path(),
HashMap::new(),
Some(5_000),
@@ -630,14 +569,14 @@ fn legacy_tty_powershell_emits_output_and_accepts_input() {
&permission_profile,
workspace_roots_for(cwd.as_path()).as_slice(),
codex_home.path(),
unresolved_launch(vec![
vec![
pwsh.display().to_string(),
"-NoLogo".to_string(),
"-NoProfile".to_string(),
"-NoExit".to_string(),
"-Command".to_string(),
"$PID; Write-Output ready".to_string(),
]),
],
cwd.as_path(),
HashMap::new(),
Some(10_000),
@@ -684,11 +623,11 @@ fn legacy_tty_cmd_emits_output_and_accepts_input() {
&permission_profile,
workspace_roots_for(cwd.as_path()).as_slice(),
codex_home.path(),
unresolved_launch(vec![
vec![
"C:\\Windows\\System32\\cmd.exe".to_string(),
"/K".to_string(),
"echo ready".to_string(),
]),
],
cwd.as_path(),
HashMap::new(),
Some(10_000),
@@ -738,11 +677,11 @@ fn legacy_tty_cmd_default_desktop_emits_output_and_accepts_input() {
&permission_profile,
workspace_roots_for(cwd.as_path()).as_slice(),
codex_home.path(),
unresolved_launch(vec![
vec![
"C:\\Windows\\System32\\cmd.exe".to_string(),
"/K".to_string(),
"echo ready".to_string(),
]),
],
cwd.as_path(),
HashMap::new(),
Some(10_000),
+1 -4
View File
@@ -177,10 +177,7 @@ async fn run_windows_sandbox_wrapper_request(request: WindowsSandboxWrapperReque
permission_profile: &request.permission_profile,
workspace_roots: request.workspace_roots.as_slice(),
codex_home: request.codex_home.as_path(),
launch: crate::WindowsProcessLaunch {
application_path: None,
command: request.command,
},
command: request.command,
cwd: request.command_cwd.as_path(),
env_map: request.env_map,
windows_sandbox_level: request.windows_sandbox_level,