Files
codex/codex-rs/core/tests/suite/windows_sandbox.rs
T
Abhinav 0ed2735d19 Use Windows setup marker as completion signal (#26074)
# Why

When an organization requires the elevated Windows sandbox, Codex
launches an elevated helper to provision users, configure firewall and
ACL rules, and lock persistent sandbox directories.

We observed that closing the helper after setup started could leave the
machine partially initialized while the TUI still announced **Sandbox
ready**. Model-only turns continued to work, but the first shell command
retried setup and failed with Windows cancellation error `1223`.

This was not an enforcement bypass; command execution continued to fail
closed. The issue was a false readiness signal: `setup_marker.json` was
written during user provisioning, before the remaining setup stages had
completed.

# What

Treat `setup_marker.json` as the commit record for Windows sandbox
setup:

1. Before full or provisioning setup begins, remove the existing marker
and create the final marker path with a protected ACL.
2. Keep the marker empty and therefore invalid while setup is in
progress. Sandbox users cannot read, modify, or replace it.
3. Run every synchronous setup stage.
4. After setup succeeds, write the valid marker contents without
changing its ACL.
5. After the helper exits successfully, verify the existing readiness
check before enabling the sandbox.

If setup is canceled or fails, the marker remains invalid and Codex
reports setup as incomplete instead of announcing readiness.

Refresh-only and read-ACL-only helper runs continue to leave the marker
untouched. The setup version remains `5` to avoid forcing all existing
Windows users through elevated setup again.

# Verification

- Added coverage confirming sandbox users cannot read or modify the
setup marker after elevated setup.
- Added coverage confirming a successful helper exit without complete
setup artifacts is rejected.
- Ran `just test -p codex-windows-sandbox`.
2026-06-03 15:33:34 -07:00

323 lines
12 KiB
Rust

use anyhow::Context;
use codex_core::exec::ExecCapturePolicy;
use codex_core::exec::ExecParams;
use codex_core::exec::process_exec_tool_call;
use codex_core::sandboxing::SandboxPermissions;
use codex_core::windows_sandbox::sandbox_setup_is_complete;
use codex_protocol::config_types::WindowsSandboxLevel;
use codex_protocol::exec_output::ExecToolCallOutput;
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 core_test_support::PathExt;
use pretty_assertions::assert_eq;
use serial_test::serial;
use std::collections::HashMap;
use std::ffi::OsString;
use std::path::Path;
use std::path::PathBuf;
use tempfile::TempDir;
struct EnvVarGuard {
key: &'static str,
original: Option<OsString>,
}
impl EnvVarGuard {
fn set(key: &'static str, value: &std::ffi::OsStr) -> Self {
let original = std::env::var_os(key);
unsafe {
std::env::set_var(key, value);
}
Self { key, original }
}
}
impl Drop for EnvVarGuard {
fn drop(&mut self) {
unsafe {
match &self.original {
Some(value) => std::env::set_var(self.key, value),
None => std::env::remove_var(self.key),
}
}
}
}
enum TestCodexHome {
Persistent(PathBuf),
Temporary(TempDir),
}
impl TestCodexHome {
fn path(&self) -> &Path {
match self {
Self::Persistent(path) => path.as_path(),
Self::Temporary(temp_dir) => temp_dir.path(),
}
}
}
fn codex_home_for_windows_sandbox_test(name: &str) -> anyhow::Result<TestCodexHome> {
if let Some(test_tmpdir) = std::env::var_os("TEST_TMPDIR") {
// The elevated backend provisions machine-local sandbox users. Bazel
// retries run in the same Windows VM, so keep CODEX_HOME stable within
// the test temp root and let setup reconcile its persisted ACL state.
let codex_home = PathBuf::from(test_tmpdir).join(name);
std::fs::create_dir_all(&codex_home)
.with_context(|| format!("create stable test CODEX_HOME {}", codex_home.display()))?;
return Ok(TestCodexHome::Persistent(codex_home));
}
Ok(TestCodexHome::Temporary(TempDir::new()?))
}
fn stage_windows_sandbox_helpers() -> anyhow::Result<()> {
let test_exe = std::env::current_exe().context("resolve current Windows test executable")?;
let test_exe_dir = test_exe
.parent()
.context("Windows test executable should have a parent directory")?;
let resources_dir = test_exe_dir.join("codex-resources");
match std::fs::create_dir_all(&resources_dir) {
Ok(()) => {}
Err(err)
if err.kind() == std::io::ErrorKind::PermissionDenied && resources_dir.is_dir() => {}
Err(err) => {
return Err(err)
.with_context(|| format!("create resources dir {}", resources_dir.display()));
}
}
for helper_name in ["codex-windows-sandbox-setup", "codex-command-runner"] {
let helper = codex_utils_cargo_bin::cargo_bin(helper_name)?;
let file_name = Path::new(helper_name).with_extension("exe");
let destination = resources_dir.join(file_name);
if let Err(err) = std::fs::copy(&helper, &destination) {
// A sandbox helper can briefly remain alive after the sandboxed
// command exits. Bazel may retry the test while that process still
// has the staged executable open, so keep the already-staged copy.
if err.kind() == std::io::ErrorKind::PermissionDenied && destination.exists() {
continue;
}
return Err(err).with_context(|| {
format!(
"stage Windows sandbox helper {} at {}",
helper.display(),
destination.display()
)
});
}
}
Ok(())
}
#[tokio::test]
#[serial(codex_home)]
async fn windows_restricted_token_rejects_exact_and_glob_deny_read_policy() -> anyhow::Result<()> {
let codex_home =
codex_home_for_windows_sandbox_test("windows-restricted-token-deny-read-codex-home")?;
let _codex_home_guard = EnvVarGuard::set("CODEX_HOME", codex_home.path().as_os_str());
let workspace = TempDir::new()?;
let cwd = dunce::canonicalize(workspace.path())?.abs();
let secret = cwd.join("secret.env");
let future_secret = cwd.join("future.env");
let public = cwd.join("public.txt");
std::fs::write(&secret, "glob secret\n")?;
std::fs::write(&public, "public ok\n")?;
let file_system_sandbox_policy = FileSystemSandboxPolicy::restricted(vec![
FileSystemSandboxEntry {
path: FileSystemPath::Special {
value: FileSystemSpecialPath::Root,
},
access: FileSystemAccessMode::Read,
},
FileSystemSandboxEntry {
path: FileSystemPath::Special {
value: FileSystemSpecialPath::project_roots(/*subpath*/ None),
},
access: FileSystemAccessMode::Write,
},
FileSystemSandboxEntry {
path: FileSystemPath::GlobPattern {
pattern: "**/*.env".to_string(),
},
access: FileSystemAccessMode::Deny,
},
FileSystemSandboxEntry {
path: FileSystemPath::Path {
path: future_secret,
},
access: FileSystemAccessMode::Deny,
},
]);
let permission_profile = PermissionProfile::from_runtime_permissions(
&file_system_sandbox_policy,
NetworkSandboxPolicy::Restricted,
);
let err = process_exec_tool_call(
ExecParams {
command: vec![
"cmd.exe".to_string(),
"/D".to_string(),
"/C".to_string(),
"type secret.env >NUL 2>NUL & echo exact secret 1>future.env 2>NUL & type future.env 2>NUL & type public.txt & exit /B 0"
.to_string(),
],
cwd: cwd.clone(),
expiration: 10_000.into(),
capture_policy: ExecCapturePolicy::ShellTool,
env: HashMap::new(),
network: None,
sandbox_permissions: SandboxPermissions::UseDefault,
windows_sandbox_level: WindowsSandboxLevel::RestrictedToken,
windows_sandbox_private_desktop: false,
justification: None,
arg0: None,
},
&permission_profile,
&cwd,
std::slice::from_ref(&cwd),
&None,
/*use_legacy_landlock*/ false,
/*stdout_stream*/ None,
)
.await
.expect_err("restricted-token sandbox should reject deny-read restrictions");
assert_eq!(
err.to_string(),
"unsupported operation: windows unelevated restricted-token sandbox cannot enforce deny-read restrictions directly; refusing to run unsandboxed"
);
Ok(())
}
#[tokio::test]
#[serial(codex_home)]
async fn windows_elevated_enforces_deny_read_and_protects_setup_marker() -> anyhow::Result<()> {
let codex_home = codex_home_for_windows_sandbox_test("windows-elevated-deny-read-codex-home")?;
let _codex_home_guard = EnvVarGuard::set("CODEX_HOME", codex_home.path().as_os_str());
stage_windows_sandbox_helpers()?;
let workspace = TempDir::new()?;
let cwd = dunce::canonicalize(workspace.path())?.abs();
let glob_secret = cwd.join("secret.env");
let exact_secret = cwd.join("exact-secret.txt");
let public = cwd.join("public.txt");
let setup_marker = codex_home.path().join(".sandbox").join("setup_marker.json");
std::fs::write(&glob_secret, "glob secret\n")?;
std::fs::write(&exact_secret, "exact secret\n")?;
std::fs::write(&public, "public ok\n")?;
let file_system_sandbox_policy = FileSystemSandboxPolicy::restricted(vec![
FileSystemSandboxEntry {
path: FileSystemPath::Special {
value: FileSystemSpecialPath::Root,
},
access: FileSystemAccessMode::Read,
},
FileSystemSandboxEntry {
path: FileSystemPath::Special {
value: FileSystemSpecialPath::project_roots(/*subpath*/ None),
},
access: FileSystemAccessMode::Write,
},
FileSystemSandboxEntry {
path: FileSystemPath::GlobPattern {
pattern: "**/*.env".to_string(),
},
access: FileSystemAccessMode::Deny,
},
FileSystemSandboxEntry {
path: FileSystemPath::Path { path: exact_secret },
access: FileSystemAccessMode::Deny,
},
]);
let permission_profile = PermissionProfile::from_runtime_permissions(
&file_system_sandbox_policy,
NetworkSandboxPolicy::Restricted,
);
let ExecToolCallOutput {
exit_code,
stdout,
..
} = process_exec_tool_call(
ExecParams {
command: vec![
"cmd.exe".to_string(),
"/D".to_string(),
"/C".to_string(),
format!(
"(type secret.env 1>NUL 2>NUL && echo GLOB-READ || echo GLOB-DENIED) & (type exact-secret.txt 1>NUL 2>NUL && echo EXACT-READ || echo EXACT-DENIED) & (type \"{}\" 1>NUL 2>NUL && echo MARKER-READ-ALLOWED || echo MARKER-READ-DENIED) & (echo tampered > \"{}\" 2>NUL && echo MARKER-WRITE-ALLOWED || echo MARKER-WRITE-DENIED) & type public.txt",
setup_marker.display(),
setup_marker.display()
),
],
cwd: cwd.clone(),
expiration: 10_000.into(),
capture_policy: ExecCapturePolicy::ShellTool,
env: HashMap::new(),
network: None,
sandbox_permissions: SandboxPermissions::UseDefault,
windows_sandbox_level: WindowsSandboxLevel::Elevated,
windows_sandbox_private_desktop: false,
justification: None,
arg0: None,
},
&permission_profile,
&cwd,
std::slice::from_ref(&cwd),
&None,
/*use_legacy_landlock*/ false,
/*stdout_stream*/ None,
)
.await?;
assert_eq!(exit_code, 0, "sandboxed command should complete");
assert!(
stdout.text.contains("GLOB-DENIED"),
"glob deny-read should block the secret: {stdout:?}"
);
assert!(
!stdout.text.contains("GLOB-READ"),
"glob deny-read should not allow the secret: {stdout:?}"
);
assert!(
stdout.text.contains("EXACT-DENIED"),
"exact deny-read should block the secret: {stdout:?}"
);
assert!(
!stdout.text.contains("EXACT-READ"),
"exact deny-read should not allow the secret: {stdout:?}"
);
assert!(
stdout.text.contains("public ok"),
"allowed reads should still work: {stdout:?}"
);
assert!(
stdout.text.contains("MARKER-READ-DENIED"),
"sandboxed command should not read setup readiness: {stdout:?}"
);
assert!(
stdout.text.contains("MARKER-WRITE-DENIED"),
"sandboxed command should not modify setup readiness: {stdout:?}"
);
assert!(
!stdout.text.contains("MARKER-READ-ALLOWED"),
"sandboxed command must not read setup readiness: {stdout:?}"
);
assert!(
!stdout.text.contains("MARKER-WRITE-ALLOWED"),
"sandboxed command must not modify setup readiness: {stdout:?}"
);
assert!(
sandbox_setup_is_complete(codex_home.path()),
"setup should remain ready after the tamper attempt"
);
Ok(())
}