From 627b579219d492b5445d6d433d3110eb53e507dc Mon Sep 17 00:00:00 2001 From: malsamiri-oai Date: Tue, 10 Nov 2026 16:32:50 +0000 Subject: [PATCH] Preserve script quotes in Windows sandbox cmd invocations (#60624) ## Why CRT argument escaping adds literal backslashes before quotes in `cmd.exe /c` scripts, breaking quoted paths and executable names. ## What changed Serialize single-script `cmd /c` invocations with `/s /c` and an outer quote pair, preserving the script's quotes. Share this handling across Windows sandbox process creation, ConPTY, and MXC execution requests. ## Testing Add a regression test for `certutil -hashfile` with a quoted path containing spaces, and update the command-line test for a quoted PowerShell executable. GitOrigin-RevId: 32b4b7b55f6af4dba206813eac150310f37ae1d4 --- codex-rs/mxc-sandbox/src/policy.rs | 12 ++++++---- codex-rs/windows-sandbox-rs/src/cmd_script.rs | 23 +++++++++++++++++++ .../src/cmd_script_tests.rs | 19 +++++++++++++++ codex-rs/windows-sandbox-rs/src/conpty/mod.rs | 8 ++----- codex-rs/windows-sandbox-rs/src/lib.rs | 3 +++ codex-rs/windows-sandbox-rs/src/winutil.rs | 7 ++++-- 6 files changed, 60 insertions(+), 12 deletions(-) create mode 100644 codex-rs/windows-sandbox-rs/src/cmd_script.rs create mode 100644 codex-rs/windows-sandbox-rs/src/cmd_script_tests.rs diff --git a/codex-rs/mxc-sandbox/src/policy.rs b/codex-rs/mxc-sandbox/src/policy.rs index 4fb07c5e9d..be227e1ced 100644 --- a/codex-rs/mxc-sandbox/src/policy.rs +++ b/codex-rs/mxc-sandbox/src/policy.rs @@ -15,6 +15,7 @@ use codex_protocol::protocol::FileSystemSandboxPolicy; use codex_protocol::protocol::FileSystemSpecialPath; use codex_utils_absolute_path::AbsolutePathBuf; use codex_utils_path_uri::PathUri; +use codex_windows_sandbox::cmd_script_command_line; use codex_windows_sandbox::expand_windows_deny_read_paths; use codex_windows_sandbox::resolve_windows_deny_read_paths; use mxc_sdk::mxc_common::cmdline::CommandLineContext; @@ -320,10 +321,13 @@ pub(super) fn build_request( }); } Ok(ExecutionRequest { - script_code: cmdline_from_argv_for_context( - &command.command, - CommandLineContext::WindowsCreateProcess, - )?, + script_code: match cmd_script_command_line(&command.command) { + Some(script) => script, + None => cmdline_from_argv_for_context( + &command.command, + CommandLineContext::WindowsCreateProcess, + )?, + }, working_directory: command_cwd .to_str() .ok_or(PolicyError::NonUnicodeCommandCwd)? diff --git a/codex-rs/windows-sandbox-rs/src/cmd_script.rs b/codex-rs/windows-sandbox-rs/src/cmd_script.rs new file mode 100644 index 0000000000..2048bb81bc --- /dev/null +++ b/codex-rs/windows-sandbox-rs/src/cmd_script.rs @@ -0,0 +1,23 @@ +//! Serialize the single script argument used by the Windows command interpreter. + +/// Build the native command line for Codex's `cmd.exe /c SCRIPT` invocation. +/// The script is shell syntax. CRT argument escaping would introduce literal +/// backslashes before its quotes. With /s, cmd strips exactly the outer pair. +pub fn cmd_script_command_line(argv: &[String]) -> Option { + let [program, switch, script] = argv else { + return None; + }; + let name = program.rsplit(['/', '\\']).next()?; + if !(name.eq_ignore_ascii_case("cmd.exe") || name.eq_ignore_ascii_case("cmd")) + || !switch.eq_ignore_ascii_case("/c") + || program.contains(['"', '\0']) + || script.contains('\0') + { + return None; + } + Some(format!(r#""{program}" /s /c "{script}""#)) +} + +#[cfg(test)] +#[path = "cmd_script_tests.rs"] +mod tests; diff --git a/codex-rs/windows-sandbox-rs/src/cmd_script_tests.rs b/codex-rs/windows-sandbox-rs/src/cmd_script_tests.rs new file mode 100644 index 0000000000..2cd211c97b --- /dev/null +++ b/codex-rs/windows-sandbox-rs/src/cmd_script_tests.rs @@ -0,0 +1,19 @@ +#![cfg(test)] + +use super::cmd_script_command_line; + +#[test] +fn preserves_quotes_in_the_hash_command() { + let argv = [ + r"C:\Windows\System32\CMD.EXE", + "/C", + r#"certutil -hashfile "C:\New folder\long name.pftrace" SHA256"#, + ] + .map(str::to_owned); + assert_eq!( + cmd_script_command_line(&argv).as_deref(), + Some( + r#""C:\Windows\System32\CMD.EXE" /s /c "certutil -hashfile "C:\New folder\long name.pftrace" SHA256""# + ) + ); +} diff --git a/codex-rs/windows-sandbox-rs/src/conpty/mod.rs b/codex-rs/windows-sandbox-rs/src/conpty/mod.rs index 86839955e8..63584654c6 100644 --- a/codex-rs/windows-sandbox-rs/src/conpty/mod.rs +++ b/codex-rs/windows-sandbox-rs/src/conpty/mod.rs @@ -8,8 +8,8 @@ use crate::desktop::LaunchDesktop; use crate::proc_thread_attr::ProcThreadAttributeList; +use crate::winutil::argv_to_command_line; use crate::winutil::format_last_error; -use crate::winutil::quote_windows_arg; use crate::winutil::to_wide; use anyhow::Context; use anyhow::Result; @@ -112,11 +112,7 @@ pub fn spawn_conpty_process_as_user( env_map: &HashMap, desktop: LaunchDesktop, ) -> Result<(PROCESS_INFORMATION, ConptyInstance)> { - let cmdline_str = argv - .iter() - .map(|arg| quote_windows_arg(arg)) - .collect::>() - .join(" "); + let cmdline_str = argv_to_command_line(argv); let mut cmdline: Vec = to_wide(&cmdline_str); let env_block = make_env_block(env_map); let mut si: STARTUPINFOEXW = unsafe { std::mem::zeroed() }; diff --git a/codex-rs/windows-sandbox-rs/src/lib.rs b/codex-rs/windows-sandbox-rs/src/lib.rs index ed2917bf71..987f765342 100644 --- a/codex-rs/windows-sandbox-rs/src/lib.rs +++ b/codex-rs/windows-sandbox-rs/src/lib.rs @@ -155,6 +155,9 @@ mod winutil; #[cfg(target_os = "windows")] mod workspace_acl; +mod cmd_script; +pub use cmd_script::cmd_script_command_line; + mod deny_read_resolver; #[cfg(target_os = "windows")] mod uninstall_windows; diff --git a/codex-rs/windows-sandbox-rs/src/winutil.rs b/codex-rs/windows-sandbox-rs/src/winutil.rs index 0899362684..b09662ae04 100644 --- a/codex-rs/windows-sandbox-rs/src/winutil.rs +++ b/codex-rs/windows-sandbox-rs/src/winutil.rs @@ -114,6 +114,9 @@ pub fn quote_windows_arg(arg: &str) -> String { /// Build a Windows command line for CreateProcess-style APIs. #[cfg(target_os = "windows")] pub fn argv_to_command_line(argv: &[String]) -> String { + if let Some(command_line) = crate::cmd_script_command_line(argv) { + return command_line; + } argv.iter() .map(|arg| quote_windows_arg(arg)) .collect::>() @@ -330,7 +333,7 @@ mod tests { use pretty_assertions::assert_eq; #[test] - fn argv_to_command_line_quotes_each_argument_independently() { + fn argv_to_command_line_preserves_quoted_executable_in_cmd_script() { let argv = vec![ "cmd.exe".to_string(), "/c".to_string(), @@ -340,7 +343,7 @@ mod tests { assert_eq!( argv_to_command_line(&argv), - "cmd.exe /c \"\\\"C:\\Program Files\\PowerShell\\7\\pwsh.exe\\\" -NoProfile -EncodedCommand abc==\"" + r#""cmd.exe" /s /c ""C:\Program Files\PowerShell\7\pwsh.exe" -NoProfile -EncodedCommand abc==""# ); }