From 42cd1ec497dd8829f39bcde216df062fb86fb25f Mon Sep 17 00:00:00 2001 From: iceweasel-oai Date: Fri, 11 Sep 2026 20:00:37 +0000 Subject: [PATCH] Wire up the native Windows MXC helper entry point (#44903) ## What changed - Dispatch `--__codex-windows-mxc` before normal CLI parsing, launch the decoded request, and propagate the child exit code. - Build the native request from the helper's environment and working directory, discovered Windows platform directories, and volume roots, including UNC working-directory roots. Reject non-Unicode environment values and empty child environments. - Allow Win32k calls and desktop handles needed for PowerShell startup while retaining clipboard, input-injection, and desktop/system-control restrictions. - Keep policy, transport, and native launch internals private, and report an explicit error when the helper is invoked outside Windows. ## Testing Add a wrapper regression test covering exact argument preservation, Windows quoting, separate policy and command working directories, and explicit environment forwarding. GitOrigin-RevId: 674d0d61a3265bd517b77da9f9de2cd9fad62917 --- codex-rs/arg0/src/lib.rs | 3 + codex-rs/mxc-sandbox/README.md | 2 + codex-rs/mxc-sandbox/src/lib.rs | 52 ++++++--- codex-rs/mxc-sandbox/src/native.rs | 14 +-- codex-rs/mxc-sandbox/src/policy.rs | 14 ++- codex-rs/mxc-sandbox/src/policy_tests.rs | 44 ++++++++ codex-rs/mxc-sandbox/src/transport.rs | 4 +- codex-rs/mxc-sandbox/src/windows.rs | 128 +++++++++++++++++++++++ codex-rs/sandboxing/Cargo.toml | 2 +- codex-rs/sandboxing/src/lib.rs | 2 + 10 files changed, 232 insertions(+), 33 deletions(-) create mode 100644 codex-rs/mxc-sandbox/src/windows.rs diff --git a/codex-rs/arg0/src/lib.rs b/codex-rs/arg0/src/lib.rs index 4673f84e42..6b0d99a437 100644 --- a/codex-rs/arg0/src/lib.rs +++ b/codex-rs/arg0/src/lib.rs @@ -100,6 +100,9 @@ pub fn arg0_dispatch() -> Option { } let argv1 = args.next().unwrap_or_default(); + if argv1 == codex_sandboxing::CODEX_WINDOWS_MXC_ARG1 { + codex_sandboxing::run_windows_mxc_main(); + } #[cfg(unix)] if argv1 == CODEX_ARG0_EXEC_HELPER_ARG1 { codex_exec_server::run_arg0_exec_helper_main(); diff --git a/codex-rs/mxc-sandbox/README.md b/codex-rs/mxc-sandbox/README.md index 291d504177..e1c4380fcf 100644 --- a/codex-rs/mxc-sandbox/README.md +++ b/codex-rs/mxc-sandbox/README.md @@ -24,6 +24,8 @@ from the canonical Codex permission profile, including protected metadata carveouts. Supported managed network access allows IPv4 and IPv6 loopback clients and servers, including the dedicated proxy listeners, while denying direct non-loopback egress and general inbound network access. +Win32k calls and desktop handles remain available for PowerShell startup; +clipboard, input-injection, and desktop/system-control restrictions remain. ## Launch contract diff --git a/codex-rs/mxc-sandbox/src/lib.rs b/codex-rs/mxc-sandbox/src/lib.rs index c3f2e55cc5..4be53b323c 100644 --- a/codex-rs/mxc-sandbox/src/lib.rs +++ b/codex-rs/mxc-sandbox/src/lib.rs @@ -1,24 +1,27 @@ -//! Native Windows process security environment availability and launch. +//! Native Windows MXC helper. Policy conversion is portable; execution requires +//! a usable process security environment and never enters MXC's ACL fallbacks. #[cfg(windows)] -pub mod native; +mod native; +#[cfg(any(windows, test))] +mod policy; +mod transport; +#[cfg(windows)] +mod windows; + +use std::collections::HashMap; +use std::path::Path; +use std::path::PathBuf; use anyhow::Context; use anyhow::Result; use anyhow::ensure; use codex_network_proxy::ManagedNetworkSandboxContext; use codex_protocol::models::PermissionProfile; -use std::collections::HashMap; -use std::path::Path; -use std::path::PathBuf; - -/// Typed inputs for the native policy adapter. -pub mod transport; use serde::Deserialize; use serde::Serialize; pub const CODEX_WINDOWS_MXC_ARG1: &str = "--__codex-windows-mxc"; - const CLIENT_ONLY_LOOPBACK_UNSUPPORTED: &str = "MXC cannot enforce managed networking with allow_local_binding=false: native host-loopback access is bidirectional"; fn validate_managed_network(network: &ManagedNetworkSandboxContext) -> Result<()> { @@ -38,15 +41,13 @@ fn validate_managed_network(network: &ManagedNetworkSandboxContext) -> Result<() } #[derive(Serialize, Deserialize)] -pub struct MxcCommand { - pub permissions: PermissionProfile, - pub sandbox_policy_cwd: PathBuf, - pub managed_network: Option, - pub command: Vec, +struct MxcCommand { + permissions: PermissionProfile, + sandbox_policy_cwd: PathBuf, + managed_network: Option, + command: Vec, } -pub mod policy; - /// Inputs used to build an MXC helper invocation. #[derive(Debug)] pub struct CreateMxcCommandArgsParams<'a> { @@ -98,6 +99,25 @@ pub fn is_available() -> bool { } } +/// Entry point dispatched before ordinary Codex CLI parsing. +pub fn run_main() -> ! { + #[cfg(windows)] + { + match windows::run() { + Ok(exit_code) => std::process::exit(exit_code), + Err(error) => { + eprintln!("MXC sandbox: {error:#}"); + std::process::exit(1); + } + } + } + #[cfg(not(windows))] + { + eprintln!("MXC sandbox execution requires Windows"); + std::process::exit(1); + } +} + #[cfg(test)] #[path = "policy_tests.rs"] mod tests; diff --git a/codex-rs/mxc-sandbox/src/native.rs b/codex-rs/mxc-sandbox/src/native.rs index f3cc57a698..2121711e4c 100644 --- a/codex-rs/mxc-sandbox/src/native.rs +++ b/codex-rs/mxc-sandbox/src/native.rs @@ -16,8 +16,7 @@ use wxc_common::sandbox_process::StdioMode; #[link(name = "advapi32")] unsafe extern "system" {} -/// Launch a native-only request and wait for its exit status. -pub fn launch(request: &ExecutionRequest) -> Result { +pub(super) fn launch(request: &ExecutionRequest) -> Result { ensure!( !request .policy @@ -26,17 +25,6 @@ pub fn launch(request: &ExecutionRequest) -> Result { .any(|capability| capability.eq_ignore_ascii_case("permissiveLearningMode")), "MXC native launch does not support permissiveLearningMode" ); - ensure!( - crate::is_available(), - "native MXC is unavailable on this Windows build" - ); - ensure!( - !request.policy.least_privilege_mode - && !request.policy.network_proxy.is_enabled() - && request.policy.capture_denials.is_none() - && !request.policy.fallback.allow_dacl_mutation, - "MXC native launch does not support fallback policies" - ); // With no least-privilege mode, legacy proxy, or capture enabled, this // probe guarantees BaseContainerRunner chooses PSEC rather than SBOX. if !request.policy.denied_paths.is_empty() { diff --git a/codex-rs/mxc-sandbox/src/policy.rs b/codex-rs/mxc-sandbox/src/policy.rs index 0831bf37b2..5d5b427c7f 100644 --- a/codex-rs/mxc-sandbox/src/policy.rs +++ b/codex-rs/mxc-sandbox/src/policy.rs @@ -21,6 +21,7 @@ use wxc_common::filesystem_object::compare_existing_filesystem_objects; use wxc_common::filesystem_object::normalize_object_conflicts; use wxc_common::logger::Logger; use wxc_common::logger::Mode; +use wxc_common::models::BaseProcessUiConfig; use wxc_common::models::ContainerPolicy; use wxc_common::models::ExecutionRequest; use wxc_common::models::FallbackPolicy; @@ -31,6 +32,7 @@ use wxc_common::models::NetworkIngressPolicy; use wxc_common::models::NetworkPeer; use wxc_common::models::NetworkPolicy; use wxc_common::models::NetworkRule; +use wxc_common::models::UiPolicy; use crate::MxcCommand; @@ -67,7 +69,7 @@ pub enum PolicyError { CommandLine(#[from] CommandLineError), } -pub fn build_request( +pub(super) fn build_request( command: &MxcCommand, command_cwd: &Path, env: Vec, @@ -290,6 +292,16 @@ pub fn build_request( }), network_specified: true, network_mode_specified: true, + // PowerShell needs Win32k and desktop handles during DLL startup. + // Keep clipboard, input injection, and system-control restrictions. + ui: UiPolicy { + disable: false, + ..Default::default() + }, + base_process_ui: BaseProcessUiConfig { + isolation: "desktop".to_owned(), + ..Default::default() + }, ..Default::default() }, ..Default::default() diff --git a/codex-rs/mxc-sandbox/src/policy_tests.rs b/codex-rs/mxc-sandbox/src/policy_tests.rs index 4603fe49fb..f698b84f1c 100644 --- a/codex-rs/mxc-sandbox/src/policy_tests.rs +++ b/codex-rs/mxc-sandbox/src/policy_tests.rs @@ -106,6 +106,50 @@ fn command(permissions: &PermissionProfile, cwd: &Path) -> MxcCommand { } } +#[test] +fn wrapper_preserves_exact_argv_and_separate_command_cwd() -> Result<()> { + let root = tempfile::tempdir()?; + let cwd = root.path().join("command-cwd"); + let profile = PermissionProfile::from_runtime_permissions( + &FileSystemSandboxPolicy::restricted(Vec::new()), + NetworkSandboxPolicy::Restricted, + ); + let argv = vec![ + r"C:\Program Files\tool.exe".to_owned(), + "--permissions".to_owned(), + String::new(), + "quotes\" and slash\\".to_owned(), + ]; + let mut env = HashMap::new(); + let wrapped = create_command_args(CreateMxcCommandArgsParams { + command: argv.clone(), + permission_profile: &profile, + sandbox_policy_cwd: root.path(), + managed_network: None, + env: &mut env, + })?; + assert_eq!(wrapped, vec![crate::CODEX_WINDOWS_MXC_ARG1]); + let parsed = crate::transport::decode(&mut env)?; + assert_eq!( + ( + &parsed.permissions, + parsed.sandbox_policy_cwd.as_path(), + &parsed.command + ), + (&profile, root.path(), &argv) + ); + let request = build_request(&parsed, &cwd, vec!["CUSTOM=value".to_owned()], &[], &[])?; + assert_eq!( + (request.script_code, request.working_directory, request.env), + ( + r#""C:\Program Files\tool.exe" --permissions "" "quotes\" and slash\\""#.to_owned(), + cwd.to_str().unwrap().to_owned(), + vec!["CUSTOM=value".to_owned()], + ) + ); + Ok(()) +} + #[test] fn empty_command_is_rejected_before_native_launch() -> Result<()> { let root = tempfile::tempdir()?; diff --git a/codex-rs/mxc-sandbox/src/transport.rs b/codex-rs/mxc-sandbox/src/transport.rs index 65e9191f91..24bdb0a7e5 100644 --- a/codex-rs/mxc-sandbox/src/transport.rs +++ b/codex-rs/mxc-sandbox/src/transport.rs @@ -23,7 +23,7 @@ fn is_transport_key(key: &str) -> bool { .is_some_and(|start| start.eq_ignore_ascii_case(PREFIX.as_bytes())) } -pub fn encode(command: &MxcCommand, env: &mut HashMap) -> Result<()> { +pub(super) fn encode(command: &MxcCommand, env: &mut HashMap) -> Result<()> { let payload = serde_json::to_string(command)?; ensure!( payload.len() <= MAX_BYTES, @@ -53,7 +53,7 @@ pub fn encode(command: &MxcCommand, env: &mut HashMap) -> Result } #[cfg(any(windows, test))] -pub fn decode(env: &mut HashMap) -> Result { +pub(super) fn decode(env: &mut HashMap) -> Result { let mut encoded = HashMap::new(); let mut duplicate = false; let mut oversized = false; diff --git a/codex-rs/mxc-sandbox/src/windows.rs b/codex-rs/mxc-sandbox/src/windows.rs new file mode 100644 index 0000000000..871921289d --- /dev/null +++ b/codex-rs/mxc-sandbox/src/windows.rs @@ -0,0 +1,128 @@ +//! Resolve executor-local state and launch the decoded MXC helper request. + +use std::collections::HashMap; +use std::path::PathBuf; + +use anyhow::Result; +use anyhow::ensure; +use windows_sys::Win32::Foundation::RPC_E_CHANGED_MODE; +use windows_sys::Win32::Storage::FileSystem::GetLogicalDrives; +use windows_sys::Win32::System::Com::COINIT_MULTITHREADED; +use windows_sys::Win32::System::Com::CoInitializeEx; +use windows_sys::Win32::System::Com::CoTaskMemFree; +use windows_sys::Win32::System::Com::CoUninitialize; +use windows_sys::Win32::System::SystemInformation::GetWindowsDirectoryW; +use windows_sys::Win32::UI::Shell::FOLDERID_ProgramData; +use windows_sys::Win32::UI::Shell::FOLDERID_ProgramFiles; +use windows_sys::Win32::UI::Shell::FOLDERID_ProgramFilesX86; +use windows_sys::Win32::UI::Shell::SHGetKnownFolderPath; + +struct ComUninitializeGuard; + +impl Drop for ComUninitializeGuard { + fn drop(&mut self) { + unsafe { CoUninitialize() }; + } +} + +pub(super) fn run() -> Result { + ensure!( + crate::is_available(), + "native MXC is unavailable on this Windows build" + ); + // Read the already-filtered wrapper environment, rejecting lossy values + // instead of inheriting a different environment inside the sandbox. + let mut env = std::env::vars_os() + .map(|(key, value)| { + let key = key + .into_string() + .map_err(|_| anyhow::anyhow!("MXC requires Unicode environment variable names"))?; + let value = value + .into_string() + .map_err(|_| anyhow::anyhow!("MXC requires Unicode environment variable values"))?; + Ok((key, value)) + }) + .collect::>>()?; + let command = crate::transport::decode(&mut env)?; + ensure!( + !env.is_empty(), + "MXC requires an explicit child environment" + ); + let mask = unsafe { GetLogicalDrives() }; + ensure!( + mask != 0, + "cannot enumerate Windows volumes: {}", + std::io::Error::last_os_error() + ); + let mut volumes: Vec = (0..26) + .filter(|index| mask & (1 << index) != 0) + .map(|index| PathBuf::from(format!("{}:\\", (b'A' + index as u8) as char))) + .collect(); + let command_cwd = std::env::current_dir()?; + // An explicit UNC cwd has no drive letter and is absent from GetLogicalDrives. + for cwd in [&command.sandbox_policy_cwd, &command_cwd] { + if let Some(root) = cwd.ancestors().last() { + volumes.push(root.to_path_buf()); + } + } + let request = crate::policy::build_request( + &command, + &command_cwd, + env.into_iter() + .map(|(key, value)| format!("{key}={value}")) + .collect(), + &volumes, + &platform_read_roots()?, + )?; + crate::native::launch(&request) +} + +fn platform_read_roots() -> Result> { + use std::os::windows::ffi::OsStringExt; + + let mut buffer = vec![0u16; 32768]; + let length = unsafe { GetWindowsDirectoryW(buffer.as_mut_ptr(), buffer.len() as u32) }; + ensure!( + length > 0 && (length as usize) < buffer.len(), + "cannot locate the Windows directory" + ); + let com_status = unsafe { CoInitializeEx(std::ptr::null(), COINIT_MULTITHREADED as u32) }; + ensure!( + com_status >= 0 || com_status == RPC_E_CHANGED_MODE, + "cannot initialize COM: HRESULT {com_status:#010x}" + ); + let _com = (com_status >= 0).then(|| ComUninitializeGuard); + let mut roots = vec![PathBuf::from(std::ffi::OsString::from_wide( + &buffer[..length as usize], + ))]; + for folder in [ + FOLDERID_ProgramFiles, + FOLDERID_ProgramFilesX86, + FOLDERID_ProgramData, + ] { + let mut path = std::ptr::null_mut(); + let status = unsafe { + SHGetKnownFolderPath(&folder, /*dwflags*/ 0, std::ptr::null_mut(), &mut path) + }; + if status >= 0 && !path.is_null() { + let mut length = 0; + unsafe { + while *path.add(length) != 0 { + length += 1; + } + roots.push(PathBuf::from(std::ffi::OsString::from_wide( + std::slice::from_raw_parts(path, length), + ))); + CoTaskMemFree(path.cast()); + } + } else { + if !path.is_null() { + unsafe { CoTaskMemFree(path.cast()) }; + } + anyhow::bail!( + "cannot locate required Windows platform directory: HRESULT {status:#010x}" + ); + } + } + Ok(roots) +} diff --git a/codex-rs/sandboxing/Cargo.toml b/codex-rs/sandboxing/Cargo.toml index 3cdd2e8c9b..3950fa44d7 100644 --- a/codex-rs/sandboxing/Cargo.toml +++ b/codex-rs/sandboxing/Cargo.toml @@ -14,6 +14,7 @@ workspace = true [dependencies] anyhow = { workspace = true } +codex-mxc-sandbox = { workspace = true } codex-network-proxy = { workspace = true } codex-protocol = { workspace = true } codex-utils-absolute-path = { workspace = true } @@ -30,7 +31,6 @@ url = { workspace = true } which = { workspace = true } [target.'cfg(windows)'.dependencies] -codex-mxc-sandbox = { workspace = true } codex-otel = { workspace = true } codex-utils-home-dir = { workspace = true } diff --git a/codex-rs/sandboxing/src/lib.rs b/codex-rs/sandboxing/src/lib.rs index fc390fdf95..f85396dc78 100644 --- a/codex-rs/sandboxing/src/lib.rs +++ b/codex-rs/sandboxing/src/lib.rs @@ -17,6 +17,8 @@ mod windows_mxc; pub use bwrap::find_system_bwrap_in_path; #[cfg(target_os = "linux")] pub use bwrap::system_bwrap_warning; +pub use codex_mxc_sandbox::CODEX_WINDOWS_MXC_ARG1; +pub use codex_mxc_sandbox::run_main as run_windows_mxc_main; pub use codex_windows_sandbox::WindowsSandboxProxySettingsMode; pub use denial::is_likely_executor_managed_sandbox_denied; pub use denial::is_likely_sandbox_denied;