Files
codex/codex-rs/exec-server/tests/exec_process/windows_sandbox.rs
T
zm-oai f5430515a8 Resolve Windows sandbox PowerShell fallbacks on the exec server (#49098)
## Why

Remote controllers cannot resolve a sandbox-compatible PowerShell executable on the execution host.

## What changed

Apply the existing PowerShell fallback during exec-server request preparation for the `Elevated` and `Mxc` Windows sandbox backends. Match `pwsh` and `powershell` executable names case-insensitively and replace inaccessible Store PowerShell paths when a compatible executable is available.

## Testing

Add Windows RPC regression tests for lowercase and mixed-case PowerShell aliases with a restricted server `PATH`, verifying successful command execution in the `Mxc` sandbox.

GitOrigin-RevId: 365541af2f026a662b1988429f5d182c98e4a236
2026-09-28 23:21:26 +00:00

177 lines
6.6 KiB
Rust

//! 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;
/// Windows alias casing must not prevent fallback when the primary runtime is absent from PATH.
#[test_case::test_case("pwsh.exe"; "lowercase_pwsh")]
#[test_case::test_case("PWSH.exe"; "uppercase_pwsh")]
#[test_case::test_case("powershell.exe"; "lowercase_powershell")]
#[test_case::test_case("PowerShell.exe"; "mixed_case_powershell")]
#[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 powershell_alias_falls_back_without_primary_runtime_over_rpc(
shell_name: &str,
) -> Result<()> {
crate::skip_if_mxc_unavailable!(Ok(()));
let root = TempDir::new()?;
let system32 = std::path::PathBuf::from(std::env::var("SystemRoot")?).join("System32");
let server =
common::exec_server::exec_server_with_env([("PATH", system32.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 mut sandbox = FileSystemSandboxContext::from_permission_profile(
PermissionProfile::read_only(),
cwd.clone(),
);
sandbox.windows_sandbox_selection = codex_exec_server::WindowsSandboxSelection::Mxc;
let started = environment
.get_exec_backend()
.start(ExecParams {
process_id: ProcessId::from("windows-powershell-fallback"),
metadata: None,
argv: vec![
root.path()
.join("WindowsApps")
.join(shell_name)
.to_string_lossy()
.into_owned(),
"-NoProfile".to_owned(),
"-Command".to_owned(),
"Write-Output 'fallback-ok'".to_owned(),
],
cwd,
env_policy: None,
shell_snapshot: None,
env: HashMap::new(),
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)
);
let (stdout, stderr, exit_code, exited) = collect_process_output_from_events_with_timeout(
started.process,
Duration::from_secs(/*secs*/ 30),
)
.await?;
assert_eq!(
(stdout.trim(), stderr, exit_code, exited),
("fallback-ok", String::new(), Some(0), true)
);
Ok(())
}
#[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<()> {
crate::skip_if_mxc_unavailable!(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(
PermissionProfile::from_runtime_permissions(&fs, NetworkSandboxPolicy::Restricted),
cwd.clone(),
);
sandbox.windows_sandbox_selection = codex_exec_server::WindowsSandboxSelection::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(())
}