Files
codex/codex-rs/core/tests/suite/windows_sandbox.rs
T
viyatb-oai 3c76081876 Make deny canonical for filesystem permission entries (#23493)
## Why
Filesystem permission profiles used `none` for deny-read entries, which
is less direct than the action the entry actually represents. This
change makes `deny` the canonical filesystem permission spelling while
preserving compatibility for older configs that still send `none`.

## What changed
- rename `FileSystemAccessMode::None` to `Deny`
- serialize and generate schemas with `deny` as the canonical value
- retain `none` only as a legacy input alias for temporary config
compatibility
- update filesystem glob diagnostics and regression coverage to use the
canonical spelling
- refresh config and app-server schema fixtures to match the new wire
shape

## Validation
- `cargo test -p codex-protocol`
- `cargo test -p codex-app-server-protocol`
- `cargo test -p codex-core config_toml_deserializes_permission_profiles
--lib`
- `cargo test -p codex-core
read_write_glob_patterns_still_reject_non_subpath_globs --lib`

Earlier in the session, a broad `cargo test -p codex-core` run reached
unrelated pre-existing failures in timing/snapshot/git-info tests under
this environment; the targeted surfaces touched by this PR passed
cleanly.
2026-05-19 11:03:47 -07:00

244 lines
8.4 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_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 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),
}
}
}
}
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");
std::fs::create_dir_all(&resources_dir)?;
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");
std::fs::copy(helper, resources_dir.join(file_name))?;
}
Ok(())
}
#[tokio::test]
#[serial(codex_home)]
async fn windows_restricted_token_rejects_exact_and_glob_deny_read_policy() -> anyhow::Result<()> {
let temp_home = TempDir::new()?;
let _codex_home_guard = EnvVarGuard::set("CODEX_HOME", temp_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,
&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_exact_and_glob_deny_read_policy() -> anyhow::Result<()> {
let temp_home = TempDir::new()?;
let _codex_home_guard = EnvVarGuard::set("CODEX_HOME", temp_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");
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,
stderr,
..
} = process_exec_tool_call(
ExecParams {
command: vec![
"cmd.exe".to_string(),
"/D".to_string(),
"/C".to_string(),
"(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 public.txt".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::Elevated,
windows_sandbox_private_desktop: false,
justification: None,
arg0: None,
},
&permission_profile,
&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_eq!(stderr.text, "");
Ok(())
}