windows-sandbox: resolve PATH commands before spawn

This commit is contained in:
David Wiesen
2026-06-15 11:15:33 -07:00
parent dfd03ea01b
commit 25533ee9b2
6 changed files with 135 additions and 2 deletions
+1
View File
@@ -39,6 +39,7 @@ serde_json = "1.0"
tempfile = "3"
tokio = { workspace = true, features = ["sync", "rt"] }
tracing-appender = { workspace = true }
which = { workspace = true }
windows = { version = "0.58", features = [
"Win32_Foundation",
"Win32_NetworkManagement_WindowsFirewall",
@@ -7,6 +7,7 @@
//! Windows sandbox flows that need a PTY.
use crate::desktop::LaunchDesktop;
use crate::program_resolution::resolve_bare_program_on_path;
use crate::proc_thread_attr::ProcThreadAttributeList;
use crate::winutil::format_last_error;
use crate::winutil::quote_windows_arg;
@@ -98,7 +99,8 @@ pub fn spawn_conpty_process_as_user(
use_private_desktop: bool,
logs_base_dir: Option<&Path>,
) -> Result<(PROCESS_INFORMATION, ConptyInstance)> {
let cmdline_str = argv
let resolved_argv = resolve_bare_program_on_path(argv, cwd, env_map);
let cmdline_str = resolved_argv
.iter()
.map(|arg| quote_windows_arg(arg))
.collect::<Vec<_>>()
+2
View File
@@ -64,6 +64,8 @@ mod logging;
#[cfg(target_os = "windows")]
mod path_normalization;
#[cfg(target_os = "windows")]
mod program_resolution;
#[cfg(target_os = "windows")]
mod process;
#[cfg(target_os = "windows")]
mod resolved_permissions;
+3 -1
View File
@@ -1,5 +1,6 @@
use crate::desktop::LaunchDesktop;
use crate::logging;
use crate::program_resolution::resolve_bare_program_on_path;
use crate::proc_thread_attr::ProcThreadAttributeList;
use crate::winutil::argv_to_command_line;
use crate::winutil::format_last_error;
@@ -84,7 +85,8 @@ pub unsafe fn create_process_as_user(
stdio: Option<(HANDLE, HANDLE, HANDLE)>,
use_private_desktop: bool,
) -> Result<CreatedProcess> {
let cmdline_str = argv_to_command_line(argv);
let resolved_argv = resolve_bare_program_on_path(argv, cwd, env_map);
let cmdline_str = argv_to_command_line(&resolved_argv);
let mut cmdline: Vec<u16> = to_wide(&cmdline_str);
let env_block = make_env_block(env_map);
let desktop = LaunchDesktop::prepare(use_private_desktop, logs_base_dir)?;
@@ -0,0 +1,81 @@
use std::collections::HashMap;
use std::ffi::OsString;
use std::path::Path;
pub(crate) fn resolve_bare_program_on_path(
argv: &[String],
cwd: &Path,
env_map: &HashMap<String, String>,
) -> Vec<String> {
let Some(program) = argv.first() else {
return Vec::new();
};
if !is_bare_program_name(program) {
return argv.to_vec();
}
let search_path = env_map
.iter()
.find(|(key, _)| key.eq_ignore_ascii_case("PATH"))
.map(|(_, value)| OsString::from(value));
let Ok(resolved) = which::which_in(program, search_path.as_ref(), cwd) else {
return argv.to_vec();
};
let mut resolved_argv = argv.to_vec();
resolved_argv[0] = resolved.to_string_lossy().into_owned();
resolved_argv
}
fn is_bare_program_name(program: &str) -> bool {
!program.is_empty() && !program.contains(['\\', '/', ':'])
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
#[test]
fn resolves_bare_programs_from_path() {
let temp = TempDir::new().expect("tempdir");
let bin_dir = temp.path().join("bin");
fs::create_dir_all(&bin_dir).expect("create bin dir");
let exe_path = bin_dir.join("go.exe");
fs::write(&exe_path, b"").expect("write executable stub");
let env_map = HashMap::from([(
"PATH".to_string(),
bin_dir.to_string_lossy().into_owned(),
)]);
let argv = vec!["go".to_string(), "version".to_string()];
let resolved = resolve_bare_program_on_path(&argv, temp.path(), &env_map);
assert_eq!(resolved[0], exe_path.to_string_lossy());
assert_eq!(resolved[1], "version");
}
#[test]
fn leaves_explicit_paths_unchanged() {
let env_map = HashMap::from([("PATH".to_string(), "C:\\tools".to_string())]);
let argv = vec![r"C:\tools\go.exe".to_string(), "version".to_string()];
let resolved = resolve_bare_program_on_path(&argv, Path::new("."), &env_map);
assert_eq!(resolved, argv);
}
#[test]
fn leaves_missing_programs_unchanged() {
let env_map = HashMap::from([("PATH".to_string(), "C:\\tools".to_string())]);
let argv = vec!["missing-tool".to_string(), "--help".to_string()];
let resolved = resolve_bare_program_on_path(&argv, Path::new("."), &env_map);
assert_eq!(resolved, argv);
}
}
+45
View File
@@ -370,6 +370,25 @@ fn canonical_existing(paths: &[PathBuf]) -> Vec<PathBuf> {
.collect()
}
fn path_read_roots(env_map: &HashMap<String, String>) -> Vec<PathBuf> {
let path_value = env_map
.get("PATH")
.cloned()
.or_else(|| std::env::var("PATH").ok())
.unwrap_or_default();
path_value
.split(';')
.filter_map(|entry| {
let trimmed = entry.trim().trim_matches('"');
if trimmed.is_empty() {
return None;
}
Some(PathBuf::from(trimmed))
})
.collect()
}
fn profile_read_roots(user_profile: &Path) -> Vec<PathBuf> {
let entries = match std::fs::read_dir(user_profile) {
Ok(entries) => entries,
@@ -417,6 +436,7 @@ fn gather_full_read_roots_for_permissions(
.into_iter()
.map(|root| root.root),
);
roots.extend(path_read_roots(env_map));
canonical_existing(&roots)
}
@@ -444,6 +464,7 @@ pub(crate) fn gather_read_roots(
);
}
roots.extend(permissions.readable_roots_for_cwd(command_cwd));
roots.extend(path_read_roots(env_map));
canonical_existing(&roots)
}
@@ -1593,6 +1614,30 @@ mod tests {
assert!(roots.contains(&expected));
}
#[test]
fn gather_read_roots_include_existing_path_entries() {
let tmp = TempDir::new().expect("tempdir");
let codex_home = tmp.path().join("codex-home");
let command_cwd = tmp.path().join("workspace");
let toolchain_bin = tmp.path().join("toolchain").join("bin");
fs::create_dir_all(&command_cwd).expect("create workspace");
fs::create_dir_all(&toolchain_bin).expect("create toolchain bin");
let permission_profile = PermissionProfile::read_only();
let workspace_roots = workspace_roots_for(command_cwd.as_path());
let permissions = permissions_for(&permission_profile, workspace_roots.as_slice());
let mut env_map = HashMap::new();
env_map.insert(
"PATH".to_string(),
format!("{};{}", toolchain_bin.display(), tmp.path().join("missing").display()),
);
let roots = gather_read_roots(&command_cwd, &permissions, &env_map, &codex_home);
let expected_toolchain =
dunce::canonicalize(&toolchain_bin).expect("canonical toolchain bin");
assert!(roots.contains(&expected_toolchain));
}
#[test]
fn workspace_write_roots_remain_readable() {
let tmp = TempDir::new().expect("tempdir");