mirror of
https://github.com/openai/codex.git
synced 2026-10-30 19:18:49 +00:00
Wire the Windows MXC sandbox into command execution (#45176)
## What changed - Add explicit MXC backend selection and carry its identity through exec-server process reporting and sandbox violation classification. - Launch MXC through the Codex executable with the effective permission profile and command environment. - Reject exec-server MXC requests when native MXC is unavailable or when they request a TTY, an `arg0` override, or managed networking. Reject private desktop isolation during MXC preparation. - Allow an explicitly empty child environment and avoid exposing request payload values in launcher decode errors. ## Testing Add coverage for sandbox selection and unsupported-request rejection, plus Windows RPC tests for stdin writes and temporary-directory permissions derived from the command environment. Native MXC tests skip when MXC is unavailable. GitOrigin-RevId: 3626ff0f9ad7f9b812ce09b68c31ea9a5a9c72b1
This commit is contained in:
@@ -176,7 +176,11 @@ impl FileSystemSandboxRunner {
|
||||
environment_id: None,
|
||||
network: None,
|
||||
sandbox_policy_cwd: &cwd.uri,
|
||||
codex_linux_sandbox_exe: self.runtime_paths.codex_linux_sandbox_exe.as_deref(),
|
||||
sandbox_exe: if cfg!(windows) {
|
||||
Some(self.runtime_paths.codex_self_exe.as_path())
|
||||
} else {
|
||||
self.runtime_paths.codex_linux_sandbox_exe.as_deref()
|
||||
},
|
||||
use_legacy_landlock: sandbox_context.use_legacy_landlock,
|
||||
windows_sandbox_level: sandbox_context.windows_sandbox_level,
|
||||
windows_sandbox_private_desktop: sandbox_context
|
||||
|
||||
@@ -396,6 +396,7 @@ impl LocalProcess {
|
||||
SandboxType::MacosSeatbelt => Some(ProcessSandboxType::MacosSeatbelt),
|
||||
SandboxType::LinuxSeccomp => Some(ProcessSandboxType::LinuxSeccomp),
|
||||
SandboxType::WindowsRestrictedToken => Some(ProcessSandboxType::WindowsRestrictedToken),
|
||||
SandboxType::WindowsMxc => Some(ProcessSandboxType::WindowsMxc),
|
||||
};
|
||||
|
||||
let start = Arc::new(ProcessStart);
|
||||
|
||||
@@ -35,6 +35,7 @@ pub(crate) fn sandbox_type_from_protocol(
|
||||
Some(ProcessSandboxType::WindowsRestrictedToken) => {
|
||||
Some(SandboxType::WindowsRestrictedToken)
|
||||
}
|
||||
Some(ProcessSandboxType::WindowsMxc) => Some(SandboxType::WindowsMxc),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -84,6 +84,28 @@ pub(crate) async fn prepare_exec_request_with_telemetry(
|
||||
network_policy_audit_observer: Option<NetworkPolicyAuditObserver>,
|
||||
telemetry: &ProcessTelemetry,
|
||||
) -> Result<PreparedExecRequest, JSONRPCErrorError> {
|
||||
if let Some(sandbox) = params.sandbox.as_ref()
|
||||
&& sandbox.windows_sandbox_level == codex_protocol::config_types::WindowsSandboxLevel::Mxc
|
||||
{
|
||||
if params.tty || params.arg0.is_some() {
|
||||
return Err(invalid_params(
|
||||
"MXC currently supports ordinary pipe launches only".to_owned(),
|
||||
));
|
||||
}
|
||||
if params.enforce_managed_network
|
||||
|| params.managed_network.is_some()
|
||||
|| params.network_proxy.is_some()
|
||||
{
|
||||
return Err(invalid_params(
|
||||
"MXC managed networking is not supported yet".to_owned(),
|
||||
));
|
||||
}
|
||||
if !codex_sandboxing::windows_mxc_available() {
|
||||
return Err(invalid_params(
|
||||
"native MXC is unavailable on this executor".to_owned(),
|
||||
));
|
||||
}
|
||||
}
|
||||
#[cfg(target_os = "windows")]
|
||||
let mut env = env;
|
||||
#[cfg(target_os = "windows")]
|
||||
@@ -241,7 +263,11 @@ pub(crate) async fn prepare_exec_request_with_telemetry(
|
||||
environment_id: None,
|
||||
network: None,
|
||||
sandbox_policy_cwd,
|
||||
codex_linux_sandbox_exe: runtime_paths.codex_linux_sandbox_exe.as_deref(),
|
||||
sandbox_exe: if cfg!(windows) {
|
||||
Some(runtime_paths.codex_self_exe.as_path())
|
||||
} else {
|
||||
runtime_paths.codex_linux_sandbox_exe.as_deref()
|
||||
},
|
||||
use_legacy_landlock: sandbox_context.use_legacy_landlock,
|
||||
windows_sandbox_level: sandbox_context.windows_sandbox_level,
|
||||
windows_sandbox_private_desktop: sandbox_context.windows_sandbox_private_desktop,
|
||||
|
||||
@@ -129,6 +129,33 @@ async fn sandbox_request_wraps_native_argv_on_executor() {
|
||||
prepared.command.first().map(String::as_str),
|
||||
Some("/usr/bin/sandbox-exec")
|
||||
);
|
||||
|
||||
let mut params = params;
|
||||
params.sandbox.as_mut().unwrap().windows_sandbox_level =
|
||||
codex_protocol::config_types::WindowsSandboxLevel::Mxc;
|
||||
for (tty, managed, message) in [
|
||||
(false, false, "native MXC is unavailable on this executor"),
|
||||
(
|
||||
true,
|
||||
false,
|
||||
"MXC currently supports ordinary pipe launches only",
|
||||
),
|
||||
(false, true, "MXC managed networking is not supported yet"),
|
||||
] {
|
||||
params.tty = tty;
|
||||
params.enforce_managed_network = managed;
|
||||
let error = prepare_exec_request(
|
||||
¶ms,
|
||||
HashMap::new(),
|
||||
Some(&runtime_paths),
|
||||
/*network_policy_decider*/ None,
|
||||
/*network_policy_audit_observer*/ None,
|
||||
)
|
||||
.await
|
||||
.err()
|
||||
.expect("unsupported MXC must fail closed");
|
||||
assert_eq!(error.message, message);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
|
||||
@@ -44,6 +44,9 @@ pub static TEST_BINARY_DISPATCH_GUARD: Option<TestBinaryDispatchGuard> = {
|
||||
if argv1 == Some(CODEX_WINDOWS_SANDBOX_ARG1) {
|
||||
return TestBinaryDispatchMode::DispatchArg0Only;
|
||||
}
|
||||
if argv1 == Some(codex_sandboxing::CODEX_WINDOWS_MXC_ARG1) {
|
||||
return TestBinaryDispatchMode::DispatchArg0Only;
|
||||
}
|
||||
if exe_name == CODEX_LINUX_SANDBOX_ARG0 {
|
||||
return TestBinaryDispatchMode::DispatchArg0Only;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
mod common;
|
||||
#[path = "exec_process/windows_sandbox.rs"]
|
||||
mod windows_sandbox;
|
||||
|
||||
use std::collections::HashMap;
|
||||
#[cfg(unix)]
|
||||
@@ -102,6 +104,10 @@ async fn create_process_context(use_remote: bool) -> Result<ProcessContext> {
|
||||
}
|
||||
}
|
||||
|
||||
fn selected_windows_sandbox_available(windows_sandbox_level: WindowsSandboxLevel) -> bool {
|
||||
windows_sandbox_level != WindowsSandboxLevel::Mxc || codex_sandboxing::windows_mxc_available()
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn codex_home_symlink_opt_out_respects_host_config_and_scope() -> Result<()> {
|
||||
@@ -914,13 +920,20 @@ async fn collect_process_output_from_reads(
|
||||
|
||||
async fn collect_process_output_from_events(
|
||||
session: Arc<dyn ExecProcess>,
|
||||
) -> Result<(String, String, Option<i32>, bool)> {
|
||||
collect_process_output_from_events_with_timeout(session, Duration::from_secs(2)).await
|
||||
}
|
||||
|
||||
async fn collect_process_output_from_events_with_timeout(
|
||||
session: Arc<dyn ExecProcess>,
|
||||
event_timeout: Duration,
|
||||
) -> Result<(String, String, Option<i32>, bool)> {
|
||||
let mut events = session.subscribe_events();
|
||||
let mut stdout = String::new();
|
||||
let mut stderr = String::new();
|
||||
let mut exit_code = None;
|
||||
loop {
|
||||
match timeout(Duration::from_secs(2), events.recv()).await?? {
|
||||
match timeout(event_timeout, events.recv()).await?? {
|
||||
ExecProcessEvent::Output(chunk) => match chunk.stream {
|
||||
ExecOutputStream::Stdout | ExecOutputStream::Pty => {
|
||||
stdout.push_str(&String::from_utf8_lossy(&chunk.chunk.into_inner()));
|
||||
@@ -1281,7 +1294,14 @@ async fn assert_exec_process_write_then_read_without_tty(use_remote: bool) -> Re
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn assert_remote_windows_sandbox_process_write() -> Result<()> {
|
||||
async fn assert_remote_windows_sandbox_process_write(
|
||||
windows_sandbox_level: WindowsSandboxLevel,
|
||||
expected_sandbox_type: codex_sandboxing::SandboxType,
|
||||
) -> Result<()> {
|
||||
if !selected_windows_sandbox_available(windows_sandbox_level) {
|
||||
eprintln!("skipping MXC enforcement test: native MXC is unavailable on this host");
|
||||
return Ok(());
|
||||
}
|
||||
let context = create_process_context(/*use_remote*/ true).await?;
|
||||
let workspace = TempDir::new()?;
|
||||
let blocked_file = workspace.path().join("blocked.txt");
|
||||
@@ -1290,7 +1310,7 @@ async fn assert_remote_windows_sandbox_process_write() -> Result<()> {
|
||||
SandboxPolicy::new_read_only_policy(),
|
||||
cwd.clone(),
|
||||
)?;
|
||||
sandbox.windows_sandbox_level = WindowsSandboxLevel::RestrictedToken;
|
||||
sandbox.windows_sandbox_level = windows_sandbox_level;
|
||||
|
||||
let session = match context
|
||||
.backend
|
||||
@@ -1325,6 +1345,7 @@ async fn assert_remote_windows_sandbox_process_write() -> Result<()> {
|
||||
Ok(session) => session,
|
||||
Err(err) => return Err(err.into()),
|
||||
};
|
||||
assert_eq!(session.sandbox_type, Some(expected_sandbox_type));
|
||||
|
||||
let write_response = session.process.write(b"hello\n".to_vec()).await?;
|
||||
assert_eq!(write_response.status, WriteStatus::Accepted);
|
||||
@@ -1743,11 +1764,24 @@ async fn exec_process_write_then_read_without_tty(use_remote: bool) -> Result<()
|
||||
assert_exec_process_write_then_read_without_tty(use_remote).await
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
#[test_case(
|
||||
WindowsSandboxLevel::RestrictedToken,
|
||||
codex_sandboxing::SandboxType::WindowsRestrictedToken;
|
||||
"restricted_token"
|
||||
)]
|
||||
#[test_case(
|
||||
WindowsSandboxLevel::Mxc,
|
||||
codex_sandboxing::SandboxType::WindowsMxc;
|
||||
"mxc"
|
||||
)]
|
||||
#[cfg_attr(not(windows), ignore = "Windows-only exec-server sandbox process test")]
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
#[serial_test::serial(remote_exec_server)]
|
||||
async fn remote_windows_sandbox_process_accepts_process_write() -> Result<()> {
|
||||
assert_remote_windows_sandbox_process_write().await
|
||||
async fn remote_windows_sandbox_process_accepts_process_write(
|
||||
windows_sandbox_level: WindowsSandboxLevel,
|
||||
expected_sandbox_type: codex_sandboxing::SandboxType,
|
||||
) -> Result<()> {
|
||||
assert_remote_windows_sandbox_process_write(windows_sandbox_level, expected_sandbox_type).await
|
||||
}
|
||||
|
||||
#[test_case(false ; "local")]
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
//! Shared Windows sandbox behavior over the real exec-server RPC connection.
|
||||
|
||||
use super::*;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::permissions::FileSystemAccessMode;
|
||||
use codex_protocol::permissions::FileSystemPath;
|
||||
use codex_protocol::permissions::FileSystemSandboxEntry;
|
||||
use codex_protocol::permissions::FileSystemSandboxPolicy;
|
||||
use codex_protocol::permissions::FileSystemSpecialPath;
|
||||
use codex_protocol::permissions::NetworkSandboxPolicy;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[cfg_attr(not(windows), ignore = "requires a native Windows sandbox")]
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
#[serial_test::serial(remote_exec_server)]
|
||||
async fn mxc_tmpdir_uses_command_environment_over_rpc() -> Result<()> {
|
||||
if !selected_windows_sandbox_available(WindowsSandboxLevel::Mxc) {
|
||||
eprintln!("skipping MXC enforcement test: native MXC is unavailable on this host");
|
||||
return Ok(());
|
||||
}
|
||||
let root = TempDir::new()?;
|
||||
let command_temp = root.path().join("command temp");
|
||||
let server_temp = root.path().join("server temp");
|
||||
std::fs::create_dir(&command_temp)?;
|
||||
std::fs::create_dir(&server_temp)?;
|
||||
let outside = server_temp.join("outside.txt");
|
||||
std::fs::write(&outside, "original")?;
|
||||
let server = common::exec_server::exec_server_with_env(
|
||||
[
|
||||
("TEMP", server_temp.as_os_str()),
|
||||
("TMP", server_temp.as_os_str()),
|
||||
],
|
||||
&[],
|
||||
)
|
||||
.await?;
|
||||
let environment = Environment::create_for_tests(Some(server.websocket_url().to_owned()))?;
|
||||
let cwd = PathUri::from_host_native_path(root.path())?;
|
||||
let fs = FileSystemSandboxPolicy::restricted(vec![
|
||||
FileSystemSandboxEntry::new(
|
||||
FileSystemPath::Special {
|
||||
value: FileSystemSpecialPath::Root,
|
||||
},
|
||||
FileSystemAccessMode::Read,
|
||||
),
|
||||
FileSystemSandboxEntry::new(
|
||||
FileSystemPath::Special {
|
||||
value: FileSystemSpecialPath::Tmpdir,
|
||||
},
|
||||
FileSystemAccessMode::Write,
|
||||
),
|
||||
]);
|
||||
let mut sandbox = FileSystemSandboxContext::from_permission_profile_with_cwd(
|
||||
PermissionProfile::from_runtime_permissions(&fs, NetworkSandboxPolicy::Restricted),
|
||||
cwd.clone(),
|
||||
);
|
||||
sandbox.windows_sandbox_level = WindowsSandboxLevel::Mxc;
|
||||
let command_temp = command_temp.to_string_lossy().into_owned();
|
||||
let started = environment
|
||||
.get_exec_backend()
|
||||
.start(ExecParams {
|
||||
process_id: ProcessId::from("windows-sandbox-temp"),
|
||||
metadata: None,
|
||||
argv: vec![
|
||||
r"C:\Windows\System32\cmd.exe".to_owned(),
|
||||
"/D".to_owned(),
|
||||
"/S".to_owned(),
|
||||
"/C".to_owned(),
|
||||
format!(
|
||||
"echo allowed>\"%TEMP%\\allowed.txt\" & 2>\"%TEMP%\\denied.txt\" echo modified>\"{}\" & exit /b 0",
|
||||
outside.display()
|
||||
),
|
||||
],
|
||||
cwd,
|
||||
env_policy: None,
|
||||
shell_snapshot: None,
|
||||
env: HashMap::from([
|
||||
("SystemRoot".to_owned(), std::env::var("SystemRoot")?),
|
||||
("TEMP".to_owned(), command_temp.clone()),
|
||||
("TMP".to_owned(), command_temp),
|
||||
]),
|
||||
tty: false,
|
||||
pipe_stdin: false,
|
||||
arg0: None,
|
||||
sandbox: Some(sandbox),
|
||||
enforce_managed_network: false,
|
||||
managed_network: None,
|
||||
network_proxy: None,
|
||||
})
|
||||
.await?;
|
||||
assert_eq!(
|
||||
started.sandbox_type,
|
||||
Some(codex_sandboxing::SandboxType::WindowsMxc)
|
||||
);
|
||||
assert_eq!(
|
||||
collect_process_output_from_events_with_timeout(
|
||||
started.process,
|
||||
Duration::from_secs(/*secs*/ 30),
|
||||
)
|
||||
.await?,
|
||||
(String::new(), String::new(), Some(0), true)
|
||||
);
|
||||
assert_eq!(
|
||||
(
|
||||
std::fs::read_to_string(root.path().join("command temp").join("allowed.txt"))?
|
||||
.trim_end()
|
||||
.to_owned(),
|
||||
std::fs::read_to_string(outside)?
|
||||
),
|
||||
("allowed".to_owned(), "original".to_owned())
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user