From 426fa8cdab4247e5623e9617d531f6917482b947 Mon Sep 17 00:00:00 2001 From: zm-oai Date: Thu, 27 Aug 2026 23:47:18 +0000 Subject: [PATCH] Use compatible PowerShell for elevated Windows sandbox commands (#41227) ## Why Microsoft Store PowerShell executables under `WindowsApps` can be inaccessible to the dedicated elevated sandbox account. ## What changed - For local elevated sandbox commands that target Store PowerShell, select the first compatible `pwsh.exe` or `powershell.exe` from `PATH` or the standard fallback locations. - Continue adding `-NoProfile`, and preserve the selected shell for remote execution and other sandbox modes. ## Testing Added unit coverage for Store-path filtering, fallback ordering, local replacement, and remote-path preservation. GitOrigin-RevId: 8ab3d0db925e886879701445040f13902fb1ad24 --- codex-rs/core/src/tools/runtimes/mod.rs | 132 ++++++++++++++++-- .../core/src/tools/runtimes/unified_exec.rs | 5 +- codex-rs/shell-command/src/shell_detect.rs | 132 ++++++++++++++++++ 3 files changed, 254 insertions(+), 15 deletions(-) diff --git a/codex-rs/core/src/tools/runtimes/mod.rs b/codex-rs/core/src/tools/runtimes/mod.rs index f8c90db3fa..3dc7e18526 100644 --- a/codex-rs/core/src/tools/runtimes/mod.rs +++ b/codex-rs/core/src/tools/runtimes/mod.rs @@ -27,7 +27,6 @@ use codex_protocol::config_types::WindowsSandboxLevel; use codex_protocol::shell_environment::is_non_inheritable_env_var; use codex_utils_absolute_path::AbsolutePathBuf; use std::collections::HashMap; -#[cfg(unix)] use std::path::Path; pub(crate) mod apply_patch; @@ -144,11 +143,34 @@ pub(crate) fn apply_zsh_fork_path_prepend( runtime_path_prepends.prepend(env, zsh_bin_dir); } -pub(crate) fn disable_powershell_profile_for_elevated_windows_sandbox( +pub(crate) fn prepare_powershell_command_for_elevated_windows_sandbox( command: &[String], shell_type: Option<&ShellType>, sandbox_requested: bool, windows_sandbox_level: WindowsSandboxLevel, + environment_is_remote: bool, +) -> Vec { + prepare_powershell_command_for_elevated_windows_sandbox_with_fallback( + command, + shell_type, + sandbox_requested, + windows_sandbox_level, + environment_is_remote, + |path| { + codex_shell_command::shell_detect::fallback_powershell_shell_for_elevated_windows_sandbox( + path, + ) + }, + ) +} + +fn prepare_powershell_command_for_elevated_windows_sandbox_with_fallback( + command: &[String], + shell_type: Option<&ShellType>, + sandbox_requested: bool, + windows_sandbox_level: WindowsSandboxLevel, + environment_is_remote: bool, + find_fallback: impl FnOnce(&Path) -> Option, ) -> Vec { if shell_type != Some(&ShellType::PowerShell) || !sandbox_requested @@ -158,17 +180,21 @@ pub(crate) fn disable_powershell_profile_for_elevated_windows_sandbox( return command.to_vec(); } + let mut command = command.to_vec(); + if !environment_is_remote && let Some(fallback) = find_fallback(Path::new(&command[0])) { + command[0] = fallback.shell_path.to_string_lossy().to_string(); + } + if command[1..] .iter() .any(|arg| arg.eq_ignore_ascii_case("-NoProfile")) { - return command.to_vec(); + return command; } // The elevated Windows sandbox runs as a dedicated sandbox account while // HOME/USERPROFILE may still point at the real user profile. Loading // PowerShell profiles in that mixed context is not a valid login shell. - let mut command = command.to_vec(); command.insert(1, "-NoProfile".to_string()); command } @@ -395,7 +421,7 @@ fn shell_single_quote(input: &str) -> String { } #[cfg(test)] -mod disable_powershell_profile_tests { +mod prepare_powershell_command_tests { use super::*; use pretty_assertions::assert_eq; @@ -407,11 +433,12 @@ mod disable_powershell_profile_tests { "Write-Output ok".to_string(), ]; - let rewritten = disable_powershell_profile_for_elevated_windows_sandbox( + let rewritten = prepare_powershell_command_for_elevated_windows_sandbox( &command, Some(&ShellType::PowerShell), /*sandbox_requested*/ true, WindowsSandboxLevel::Elevated, + /*environment_is_remote*/ false, ); assert_eq!( @@ -433,11 +460,12 @@ mod disable_powershell_profile_tests { "VwByAGkAdABlAC0ATwB1AHQAcAB1AHQAIABvAGsA".to_string(), ]; - let rewritten = disable_powershell_profile_for_elevated_windows_sandbox( + let rewritten = prepare_powershell_command_for_elevated_windows_sandbox( &command, Some(&ShellType::PowerShell), /*sandbox_requested*/ true, WindowsSandboxLevel::Elevated, + /*environment_is_remote*/ false, ); assert_eq!( @@ -460,11 +488,12 @@ mod disable_powershell_profile_tests { "Write-Output ok".to_string(), ]; - let rewritten = disable_powershell_profile_for_elevated_windows_sandbox( + let rewritten = prepare_powershell_command_for_elevated_windows_sandbox( &command, Some(&ShellType::PowerShell), /*sandbox_requested*/ true, WindowsSandboxLevel::Elevated, + /*environment_is_remote*/ false, ); assert_eq!(rewritten, command); @@ -473,16 +502,18 @@ mod disable_powershell_profile_tests { #[test] fn leaves_legacy_restricted_token_backend_alone() { let command = vec![ - "powershell.exe".to_string(), + r"C:\Program Files\WindowsApps\Microsoft.PowerShell_7.6.4.0_x64__8wekyb3d8bbwe\pwsh.exe" + .to_string(), "-Command".to_string(), "Write-Output ok".to_string(), ]; - let rewritten = disable_powershell_profile_for_elevated_windows_sandbox( + let rewritten = prepare_powershell_command_for_elevated_windows_sandbox( &command, Some(&ShellType::PowerShell), /*sandbox_requested*/ true, WindowsSandboxLevel::RestrictedToken, + /*environment_is_remote*/ false, ); assert_eq!(rewritten, command); @@ -491,16 +522,18 @@ mod disable_powershell_profile_tests { #[test] fn leaves_unsandboxed_attempts_alone() { let command = vec![ - "powershell.exe".to_string(), + r"C:\Program Files\WindowsApps\Microsoft.PowerShell_7.6.4.0_x64__8wekyb3d8bbwe\pwsh.exe" + .to_string(), "-Command".to_string(), "Write-Output ok".to_string(), ]; - let rewritten = disable_powershell_profile_for_elevated_windows_sandbox( + let rewritten = prepare_powershell_command_for_elevated_windows_sandbox( &command, Some(&ShellType::PowerShell), /*sandbox_requested*/ false, WindowsSandboxLevel::Elevated, + /*environment_is_remote*/ false, ); assert_eq!(rewritten, command); @@ -514,15 +547,88 @@ mod disable_powershell_profile_tests { "echo ok".to_string(), ]; - let rewritten = disable_powershell_profile_for_elevated_windows_sandbox( + let rewritten = prepare_powershell_command_for_elevated_windows_sandbox( &command, Some(&ShellType::Bash), /*sandbox_requested*/ true, WindowsSandboxLevel::Elevated, + /*environment_is_remote*/ false, ); assert_eq!(rewritten, command); } + + #[test] + fn local_elevated_powershell_uses_discovered_fallback() { + let command = vec![ + r"C:\Program Files\WindowsApps\Microsoft.PowerShell_7.6.4.0_x64__8wekyb3d8bbwe\pwsh.exe" + .to_string(), + "-Command".to_string(), + "Write-Output ok".to_string(), + ]; + let fallback_path = std::path::PathBuf::from(r"C:\Program Files\PowerShell\7\pwsh.exe"); + + let rewritten = prepare_powershell_command_for_elevated_windows_sandbox_with_fallback( + &command, + Some(&ShellType::PowerShell), + /*sandbox_requested*/ true, + WindowsSandboxLevel::Elevated, + /*environment_is_remote*/ false, + |_| { + Some(codex_shell_command::shell_detect::DetectedShell { + shell_type: ShellType::PowerShell, + shell_path: fallback_path.clone(), + }) + }, + ); + + assert_eq!( + rewritten, + vec![ + fallback_path.to_string_lossy().to_string(), + "-NoProfile".to_string(), + "-Command".to_string(), + "Write-Output ok".to_string(), + ] + ); + } + + #[test] + fn remote_elevated_powershell_keeps_remote_store_path_and_no_profile() { + let command = vec![ + r"C:\Program Files\WindowsApps\Microsoft.PowerShell_7.6.4.0_x64__8wekyb3d8bbwe\pwsh.exe" + .to_string(), + "-Command".to_string(), + "Write-Output ok".to_string(), + ]; + + let mut discovery_called = false; + let rewritten = prepare_powershell_command_for_elevated_windows_sandbox_with_fallback( + &command, + Some(&ShellType::PowerShell), + /*sandbox_requested*/ true, + WindowsSandboxLevel::Elevated, + /*environment_is_remote*/ true, + |_| { + discovery_called = true; + Some(codex_shell_command::shell_detect::DetectedShell { + shell_type: ShellType::PowerShell, + shell_path: std::path::PathBuf::from(r"C:\Program Files\PowerShell\7\pwsh.exe"), + }) + }, + ); + + assert!(!discovery_called); + assert_eq!( + rewritten, + vec![ + command[0].clone(), + "-NoProfile".to_string(), + "-Command".to_string(), + "Write-Output ok".to_string(), + ] + ); + } } #[cfg(all(test, unix))] diff --git a/codex-rs/core/src/tools/runtimes/unified_exec.rs b/codex-rs/core/src/tools/runtimes/unified_exec.rs index 1c89697568..8d653962fd 100644 --- a/codex-rs/core/src/tools/runtimes/unified_exec.rs +++ b/codex-rs/core/src/tools/runtimes/unified_exec.rs @@ -20,9 +20,9 @@ use crate::tools::network_approval::NetworkApprovalSpec; use crate::tools::runtimes::RuntimePathPrepends; #[cfg(unix)] use crate::tools::runtimes::apply_zsh_fork_path_prepend; -use crate::tools::runtimes::disable_powershell_profile_for_elevated_windows_sandbox; use crate::tools::runtimes::exec_env_for_sandbox_permissions; use crate::tools::runtimes::maybe_wrap_shell_lc_with_snapshot; +use crate::tools::runtimes::prepare_powershell_command_for_elevated_windows_sandbox; use crate::tools::runtimes::zsh_fork; use crate::tools::sandboxing::Approvable; use crate::tools::sandboxing::ApprovalAction; @@ -399,11 +399,12 @@ impl<'a> ToolRuntime for UnifiedExecRunt *script = format!("{exports}\n{script}"); } } - let command = disable_powershell_profile_for_elevated_windows_sandbox( + let command = prepare_powershell_command_for_elevated_windows_sandbox( &command, Some(&req.shell_type), attempt.sandbox_requested, attempt.windows_sandbox_level, + environment_is_remote, ); let command = if matches!(req.shell_type, ShellType::PowerShell) { prefix_powershell_script_with_utf8(&command) diff --git a/codex-rs/shell-command/src/shell_detect.rs b/codex-rs/shell-command/src/shell_detect.rs index 2545bf09b0..c74532cee3 100644 --- a/codex-rs/shell-command/src/shell_detect.rs +++ b/codex-rs/shell-command/src/shell_detect.rs @@ -132,6 +132,63 @@ fn file_exists(path: &std::path::Path) -> Option { } } +// Store PowerShell can be inaccessible to the elevated sandbox account; +// WindowsApps also contains valid Codex frameworks. +fn is_inaccessible_windows_apps_powershell_path(path: &std::path::Path) -> bool { + path.as_os_str() + .to_string_lossy() + .split(['\\', '/']) + .skip_while(|component| !component.eq_ignore_ascii_case("WindowsApps")) + .nth(1) + .is_some_and(|component| { + component.eq_ignore_ascii_case("pwsh.exe") + || component.eq_ignore_ascii_case("powershell.exe") + || component + .to_ascii_lowercase() + .starts_with("microsoft.powershell") + }) +} + +fn targets_inaccessible_windows_apps_powershell(path: &std::path::Path) -> bool { + is_inaccessible_windows_apps_powershell_path(path) + || std::fs::canonicalize(path) + .ok() + .is_some_and(|resolved| is_inaccessible_windows_apps_powershell_path(&resolved)) +} + +fn is_elevated_sandbox_compatible_powershell_path(path: &std::path::Path) -> bool { + if !path + .extension() + .is_some_and(|extension| extension.eq_ignore_ascii_case("exe")) + { + return false; + } + + !targets_inaccessible_windows_apps_powershell(path) +} + +fn get_elevated_sandbox_compatible_powershell_path( + binary_name: &str, + fallback_paths: &[&str], +) -> Option { + if let Ok(mut paths) = which::which_all(binary_name) + && let Some(path) = paths.find(|path| is_elevated_sandbox_compatible_powershell_path(path)) + { + return Some(path); + } + + for path in fallback_paths { + let path = std::path::Path::new(path); + if is_elevated_sandbox_compatible_powershell_path(path) + && let Some(path) = file_exists(path) + { + return Some(path); + } + } + + None +} + fn get_shell_path( shell_type: ShellType, binary_name: &str, @@ -223,6 +280,29 @@ fn get_powershell_shell() -> Option { }) } +/// Returns a replacement only when shell_path targets Store PowerShell and +/// an elevated sandbox-compatible PowerShell executable can be discovered. +/// +/// The caller owns the elevated-sandbox policy decision. Normal shell discovery +/// intentionally keeps the user's ordered PATH selection unchanged. +pub fn fallback_powershell_shell_for_elevated_windows_sandbox( + shell_path: &std::path::Path, +) -> Option { + if !cfg!(windows) || !targets_inaccessible_windows_apps_powershell(shell_path) { + return None; + } + + let shell_path = get_elevated_sandbox_compatible_powershell_path("pwsh", PWSH_FALLBACK_PATHS) + .or_else(|| { + get_elevated_sandbox_compatible_powershell_path("powershell", POWERSHELL_FALLBACK_PATHS) + })?; + + Some(DetectedShell { + shell_type: ShellType::PowerShell, + shell_path, + }) +} + fn get_cmd_shell() -> Option { let shell_path = get_shell_path(ShellType::Cmd, "cmd", &[]); @@ -294,6 +374,58 @@ mod tests { use super::*; use pretty_assertions::assert_eq; + #[test] + fn elevated_sandbox_filter_rejects_store_and_script_powershell_paths() { + for path in [ + r"C:\Program Files\WindowsApps\Microsoft.PowerShell_7.6.4.0_x64__8wekyb3d8bbwe\pwsh.exe", + r"C:\Program Files\WindowsApps\Microsoft.PowerShellPreview_8wekyb3d8bbwe\pwsh.exe", + r"C:\Users\user\AppData\Local\Microsoft\WindowsApps\pwsh.exe", + r"C:\Users\user\AppData\Local\Microsoft\WindowsApps\powershell.exe", + r"C:\Users\user\AppData\Local\Microsoft\WindowsApps\Microsoft.PowerShell_8wekyb3d8bbwe\pwsh.exe", + r"C:\PROGRAM FILES\WINDOWSAPPS\MICROSOFT.POWERSHELL\PWSH.EXE", + r"C:\portable\pwsh.cmd", + ] { + assert!(!is_elevated_sandbox_compatible_powershell_path( + std::path::Path::new(path) + )); + } + + for path in [ + r"C:\Program Files\PowerShell\7\pwsh.exe", + r"C:\Program Files\WindowsApps\OpenAI.CodexPrimaryRuntime.v26-813-10124-0_26.813.10124.0_x64__3k8sg7r9htsxt\dependencies\native\powershell\pwsh.exe", + r"C:\Program Files\WindowsApps\OpenAI.CodexPrimaryRuntime.v26-813-10124-0_26.813.10124.0_arm64__3k8sg7r9htsxt\dependencies\native\powershell\pwsh.exe", + r"C:\PROGRAM FILES\WINDOWSAPPS\OPENAI.CODEXPRIMARYRUNTIME.V26-813-10124-0\DEPENDENCIES\NATIVE\POWERSHELL\PWSH.EXE", + r"\\?\C:\Program Files\WindowsApps\OpenAI.CodexPrimaryRuntime.v26-813-10124-0\dependencies\native\powershell\pwsh.exe", + r"C:\Users\user\.cache\codex-runtimes\codex-primary-runtime\dependencies\native\powershell\pwsh.exe", + r"C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe", + r"C:\portable\NotWindowsApps\pwsh.EXE", + ] { + assert!(is_elevated_sandbox_compatible_powershell_path( + std::path::Path::new(path) + )); + } + } + + #[test] + fn elevated_sandbox_filter_preserves_ordered_fallback() { + let portable = PathBuf::from( + r"C:\Users\user\.cache\codex-runtimes\codex-primary-runtime\dependencies\native\powershell\pwsh.exe", + ); + let found = [ + PathBuf::from( + r"C:\Program Files\WindowsApps\Microsoft.PowerShell_7.6.4.0_x64__8wekyb3d8bbwe\pwsh.exe", + ), + PathBuf::from(r"C:\Users\user\AppData\Local\Microsoft\WindowsApps\pwsh.exe"), + PathBuf::from(r"C:\runtime\dependencies\bin\fallback\pwsh.cmd"), + portable.clone(), + PathBuf::from(r"C:\Program Files\PowerShell\7\pwsh.exe"), + ] + .into_iter() + .find(|path| is_elevated_sandbox_compatible_powershell_path(path)); + + assert_eq!(found, Some(portable)); + } + #[test] fn test_detect_shell_type() { assert_eq!(