Compare commits

...

1 Commits

Author SHA1 Message Date
David Wiesen
90650003d9 Resolve Windows sandbox command paths before spawn 2026-05-01 10:22:17 -07:00
5 changed files with 270 additions and 8 deletions

View File

@@ -1,5 +1,9 @@
use crate::shell_detect::detect_shell_type;
use crate::shell_snapshot::ShellSnapshot;
#[cfg(windows)]
use codex_shell_command::powershell::try_find_powershell_executable_blocking;
#[cfg(windows)]
use codex_shell_command::powershell::try_find_pwsh_executable_blocking;
use serde::Deserialize;
use serde::Serialize;
use std::path::PathBuf;
@@ -250,6 +254,29 @@ const POWERSHELL_FALLBACK_PATHS: &[&str] =
const POWERSHELL_FALLBACK_PATHS: &[&str] = &[];
fn get_powershell_shell(path: Option<&PathBuf>) -> Option<Shell> {
#[cfg(windows)]
let shell_path = path
.and_then(file_exists)
.or_else(|| try_find_pwsh_executable_blocking().map(|path| path.into_path_buf()))
.or_else(|| try_find_powershell_executable_blocking().map(|path| path.into_path_buf()))
.or_else(|| {
get_shell_path(
ShellType::PowerShell,
/* provided_path */ None,
"pwsh",
PWSH_FALLBACK_PATHS,
)
})
.or_else(|| {
get_shell_path(
ShellType::PowerShell,
/* provided_path */ None,
"powershell",
POWERSHELL_FALLBACK_PATHS,
)
});
#[cfg(not(windows))]
let shell_path = get_shell_path(ShellType::PowerShell, path, "pwsh", PWSH_FALLBACK_PATHS)
.or_else(|| {
get_shell_path(

View File

@@ -10,6 +10,7 @@ mod proc_thread_attr;
use self::proc_thread_attr::ProcThreadAttributeList;
use crate::desktop::LaunchDesktop;
use crate::program_resolution::resolve_spawn_command;
use crate::winutil::format_last_error;
use crate::winutil::quote_windows_arg;
use crate::winutil::to_wide;
@@ -94,12 +95,15 @@ 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_command = resolve_spawn_command(argv, cwd, env_map);
let spawn_argv = resolved_command.argv;
let cmdline_str = spawn_argv
.iter()
.map(|arg| quote_windows_arg(arg))
.collect::<Vec<_>>()
.join(" ");
let mut cmdline: Vec<u16> = to_wide(&cmdline_str);
let application_name = resolved_command.application_name.map(to_wide);
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;
@@ -119,7 +123,10 @@ pub fn spawn_conpty_process_as_user(
let ok = unsafe {
CreateProcessAsUserW(
h_token,
std::ptr::null(),
application_name
.as_ref()
.map(|path| path.as_ptr())
.unwrap_or(std::ptr::null()),
cmdline.as_mut_ptr(),
std::ptr::null_mut(),
std::ptr::null_mut(),

View File

@@ -31,6 +31,9 @@ windows_modules!(
workspace_acl
);
#[cfg(any(target_os = "windows", test))]
mod program_resolution;
#[cfg(target_os = "windows")]
#[path = "conpty/mod.rs"]
mod conpty;

View File

@@ -1,28 +1,29 @@
use crate::desktop::LaunchDesktop;
use crate::logging;
use crate::program_resolution::resolve_spawn_command;
use crate::winutil::format_last_error;
use crate::winutil::quote_windows_arg;
use crate::winutil::to_wide;
use anyhow::anyhow;
use anyhow::Result;
use anyhow::anyhow;
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::GetLastError;
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::Foundation::SetHandleInformation;
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::CreateProcessAsUserW;
use windows_sys::Win32::System::Threading::PROCESS_INFORMATION;
use windows_sys::Win32::System::Threading::STARTF_USESTDHANDLES;
use windows_sys::Win32::System::Threading::STARTUPINFOW;
@@ -81,12 +82,15 @@ pub unsafe fn create_process_as_user(
stdio: Option<(HANDLE, HANDLE, HANDLE)>,
use_private_desktop: bool,
) -> Result<CreatedProcess> {
let cmdline_str = argv
let resolved_command = resolve_spawn_command(argv, cwd, env_map);
let spawn_argv = resolved_command.argv;
let cmdline_str = spawn_argv
.iter()
.map(|a| quote_windows_arg(a))
.collect::<Vec<_>>()
.join(" ");
let mut cmdline: Vec<u16> = to_wide(&cmdline_str);
let application_name = resolved_command.application_name.map(to_wide);
let env_block = make_env_block(env_map);
let mut si: STARTUPINFOW = std::mem::zeroed();
si.cb = std::mem::size_of::<STARTUPINFOW>() as u32;
@@ -125,7 +129,10 @@ pub unsafe fn create_process_as_user(
let ok = CreateProcessAsUserW(
h_token,
std::ptr::null(),
application_name
.as_ref()
.map(|path| path.as_ptr())
.unwrap_or(std::ptr::null()),
cmdline.as_mut_ptr(),
std::ptr::null_mut(),
std::ptr::null_mut(),

View File

@@ -0,0 +1,218 @@
use std::collections::HashMap;
use std::env;
use std::path::Path;
use std::path::PathBuf;
pub(crate) struct ResolvedSpawnCommand {
pub(crate) argv: Vec<String>,
pub(crate) application_name: Option<String>,
}
pub(crate) fn resolve_spawn_command(
argv: &[String],
cwd: &Path,
env_map: &HashMap<String, String>,
) -> ResolvedSpawnCommand {
let Some(program) = argv.first() else {
return ResolvedSpawnCommand {
argv: Vec::new(),
application_name: None,
};
};
let Some(resolved_program) = resolve_program_path(program, cwd, env_map) else {
return ResolvedSpawnCommand {
argv: argv.to_vec(),
application_name: None,
};
};
let resolved_str = resolved_program.to_string_lossy().to_string();
let mut resolved_argv = argv.to_vec();
resolved_argv[0] = resolved_str.clone();
ResolvedSpawnCommand {
argv: resolved_argv,
application_name: Some(resolved_str),
}
}
fn resolve_program_path(
program: &str,
cwd: &Path,
env_map: &HashMap<String, String>,
) -> Option<PathBuf> {
let program_path = Path::new(program);
if has_path_qualifier(program_path) {
return resolve_explicit_program_path(program_path, cwd, env_map);
}
if is_cmd_program(program) {
let comspec = env_map
.get("ComSpec")
.cloned()
.or_else(|| env::var("ComSpec").ok());
if let Some(comspec) = comspec {
let comspec_path = PathBuf::from(comspec);
if comspec_path.is_file() && !is_windowsapps_path(&comspec_path) {
return Some(comspec_path);
}
}
}
search_path_for_program(program_path, env_map)
}
fn resolve_explicit_program_path(
program_path: &Path,
cwd: &Path,
env_map: &HashMap<String, String>,
) -> Option<PathBuf> {
let candidate = if program_path.is_absolute() {
program_path.to_path_buf()
} else {
cwd.join(program_path)
};
if candidate.is_file() && !is_windowsapps_path(&candidate) {
return Some(candidate);
}
if is_windowsapps_path(&candidate) {
let file_name = program_path.file_name()?.to_string_lossy().to_string();
return resolve_program_path(&file_name, cwd, env_map);
}
None
}
fn search_path_for_program(
program_path: &Path,
env_map: &HashMap<String, String>,
) -> Option<PathBuf> {
let search_path = env_map
.get("PATH")
.cloned()
.or_else(|| env::var("PATH").ok())?;
let path_exts = path_extensions(env_map);
let has_extension = program_path.extension().is_some();
for dir in env::split_paths(&search_path) {
let direct_candidate = dir.join(program_path);
if direct_candidate.is_file() && !is_windowsapps_path(&direct_candidate) {
return Some(direct_candidate);
}
if has_extension {
continue;
}
for ext in &path_exts {
let candidate = dir.join(program_path).with_extension(ext);
if candidate.is_file() && !is_windowsapps_path(&candidate) {
return Some(candidate);
}
}
}
None
}
fn path_extensions(env_map: &HashMap<String, String>) -> Vec<String> {
env_map
.get("PATHEXT")
.cloned()
.or_else(|| env::var("PATHEXT").ok())
.unwrap_or_else(|| ".COM;.EXE;.BAT;.CMD".to_string())
.split(';')
.filter(|ext| !ext.is_empty())
.map(|ext| ext.trim_start_matches('.').to_string())
.collect()
}
fn has_path_qualifier(path: &Path) -> bool {
path.is_absolute() || path.parent().is_some()
}
fn is_cmd_program(program: &str) -> bool {
program.eq_ignore_ascii_case("cmd") || program.eq_ignore_ascii_case("cmd.exe")
}
fn is_windowsapps_path(path: &Path) -> bool {
let normalized = path
.to_string_lossy()
.replace('/', "\\")
.to_ascii_lowercase();
normalized.contains("\\program files\\windowsapps\\")
|| normalized.contains("\\appdata\\local\\microsoft\\windowsapps\\")
}
#[cfg(test)]
mod tests {
use super::is_windowsapps_path;
use super::resolve_spawn_command;
use pretty_assertions::assert_eq;
use std::collections::HashMap;
use std::env;
use std::fs;
use tempfile::TempDir;
#[test]
fn windowsapps_path_detection_handles_forward_slashes() {
assert!(is_windowsapps_path(std::path::Path::new(
"C:/Users/alice/AppData/Local/Microsoft/WindowsApps/pwsh.exe"
)));
assert!(is_windowsapps_path(std::path::Path::new(
"C:/Program Files/WindowsApps/Foo/tool.exe"
)));
assert!(!is_windowsapps_path(std::path::Path::new(
"C:/Windows/System32/cmd.exe"
)));
}
#[test]
fn resolve_spawn_command_skips_windowsapps_aliases() {
let temp = TempDir::new().expect("tempdir");
let alias_dir = temp.path().join("AppData/Local/Microsoft/WindowsApps");
let real_dir = temp.path().join("real-bin");
fs::create_dir_all(&alias_dir).expect("create alias dir");
fs::create_dir_all(&real_dir).expect("create real dir");
fs::write(alias_dir.join("tool.cmd"), "@echo off\r\n").expect("write alias");
fs::write(real_dir.join("tool.cmd"), "@echo off\r\n").expect("write real");
let mut env_map = HashMap::new();
let path = env::join_paths([alias_dir.as_path(), real_dir.as_path()])
.expect("join path")
.to_string_lossy()
.to_string();
env_map.insert("PATH".to_string(), path);
env_map.insert("PATHEXT".to_string(), ".EXE;.CMD".to_string());
let resolved = resolve_spawn_command(
&["tool".to_string(), "/c".to_string()],
temp.path(),
&env_map,
);
assert_eq!(
resolved.argv[0],
real_dir.join("tool.cmd").to_string_lossy().to_string()
);
assert_eq!(resolved.application_name, Some(resolved.argv[0].clone()));
}
#[test]
fn resolve_spawn_command_uses_comspec_for_cmd() {
let temp = TempDir::new().expect("tempdir");
let system32 = temp.path().join("Windows/System32");
fs::create_dir_all(&system32).expect("create system32");
let cmd = system32.join("cmd.exe");
fs::write(&cmd, "stub").expect("write cmd");
let mut env_map = HashMap::new();
env_map.insert("ComSpec".to_string(), cmd.to_string_lossy().to_string());
let resolved = resolve_spawn_command(
&["cmd".to_string(), "/c".to_string()],
temp.path(),
&env_map,
);
assert_eq!(resolved.argv[0], cmd.to_string_lossy().to_string());
assert_eq!(resolved.application_name, Some(resolved.argv[0].clone()));
}
}