mirror of
https://github.com/openai/codex.git
synced 2026-09-14 11:57:03 +00:00
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
This commit is contained in:
@@ -100,6 +100,9 @@ pub fn arg0_dispatch() -> Option<Arg0PathEntryGuard> {
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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<ManagedNetworkSandboxContext>,
|
||||
pub command: Vec<String>,
|
||||
struct MxcCommand {
|
||||
permissions: PermissionProfile,
|
||||
sandbox_policy_cwd: PathBuf,
|
||||
managed_network: Option<ManagedNetworkSandboxContext>,
|
||||
command: Vec<String>,
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
@@ -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<i32> {
|
||||
pub(super) fn launch(request: &ExecutionRequest) -> Result<i32> {
|
||||
ensure!(
|
||||
!request
|
||||
.policy
|
||||
@@ -26,17 +25,6 @@ pub fn launch(request: &ExecutionRequest) -> Result<i32> {
|
||||
.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() {
|
||||
|
||||
@@ -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<String>,
|
||||
@@ -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()
|
||||
|
||||
@@ -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()?;
|
||||
|
||||
@@ -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<String, String>) -> Result<()> {
|
||||
pub(super) fn encode(command: &MxcCommand, env: &mut HashMap<String, String>) -> 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<String, String>) -> Result
|
||||
}
|
||||
|
||||
#[cfg(any(windows, test))]
|
||||
pub fn decode(env: &mut HashMap<String, String>) -> Result<MxcCommand> {
|
||||
pub(super) fn decode(env: &mut HashMap<String, String>) -> Result<MxcCommand> {
|
||||
let mut encoded = HashMap::new();
|
||||
let mut duplicate = false;
|
||||
let mut oversized = false;
|
||||
|
||||
128
codex-rs/mxc-sandbox/src/windows.rs
Normal file
128
codex-rs/mxc-sandbox/src/windows.rs
Normal file
@@ -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<i32> {
|
||||
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::<Result<HashMap<_, _>>>()?;
|
||||
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<PathBuf> = (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<Vec<PathBuf>> {
|
||||
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)
|
||||
}
|
||||
@@ -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 }
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user