mirror of
https://github.com/openai/codex.git
synced 2026-11-13 06:45:07 +00:00
feat(sandbox): add Windows deny-read parity (#18202)
## Why The split filesystem policy stack already supports exact and glob `access = none` read restrictions on macOS and Linux. Windows still needed subprocess handling for those deny-read policies without claiming enforcement from a backend that cannot provide it. ## Key finding The unelevated restricted-token backend cannot safely enforce deny-read overlays. Its `WRITE_RESTRICTED` token model is authoritative for write checks, not read denials, so this PR intentionally fails that backend closed when deny-read overrides are present instead of claiming unsupported enforcement. ## What changed This PR adds the Windows deny-read enforcement layer and makes the backend split explicit: - Resolves Windows deny-read filesystem policy entries into concrete ACL targets. - Preserves exact missing paths so they can be materialized and denied before an enforceable sandboxed process starts. - Snapshot-expands existing glob matches into ACL targets for Windows subprocess enforcement. - Honors `glob_scan_max_depth` when expanding Windows deny-read globs. - Plans both the configured lexical path and the canonical target for existing paths so reparse-point aliases are covered. - Threads deny-read overrides through the elevated/logon-user Windows sandbox backend and unified exec. - Applies elevated deny-read ACLs synchronously before command launch rather than delegating them to the background read-grant helper. - Reconciles persistent deny-read ACEs per sandbox principal so policy changes do not leave stale deny-read ACLs behind. - Fails closed on the unelevated restricted-token backend when deny-read overrides are present, because its `WRITE_RESTRICTED` token model is not authoritative for read denials. ## Landed prerequisites These prerequisite PRs are already on `main`: 1. #15979 `feat(permissions): add glob deny-read policy support` 2. #18096 `feat(sandbox): add glob deny-read platform enforcement` 3. #17740 `feat(config): support managed deny-read requirements` This PR targets `main` directly and contains only the Windows deny-read enforcement layer. ## Implementation notes - Exact deny-read paths remain enforceable on the elevated path even when they do not exist yet: Windows materializes the missing path before applying the deny ACE, so the sandboxed command cannot create and read it during the same run. - Existing exact deny paths are preserved lexically until the ACL planner, which then adds the canonical target as a second ACL target when needed. That keeps both the configured alias and the resolved object covered. - Windows ACLs do not consume Codex glob syntax directly, so glob deny-read entries are expanded to the concrete matches that exist before process launch. - Glob traversal deduplicates directory visits within each pattern walk to avoid cycles, without collapsing distinct lexical roots that happen to resolve to the same target. - Persistent deny-read ACL state is keyed by sandbox principal SID, so cleanup only removes ACEs owned by the same backend principal. - Deny-read ACEs are fail-closed on the elevated path: setup aborts if mandatory deny-read ACL application fails. - Unelevated restricted-token sessions reject deny-read overrides early instead of running with a silently unenforceable read policy. ## Verification - `cargo test -p codex-core windows_restricted_token_rejects_unreadable_split_carveouts` - `just fmt` - `just fix -p codex-core` - `just fix -p codex-windows-sandbox` - GitHub Actions rerun is in progress on the pushed head. --------- Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
1 parent
c9e46ed639
commit
46f30d0282
24 files changed
+1548
-176
No files matched your search
@@ -10,10 +10,12 @@ use crate::ipc_framed::SpawnRequest;
|
||||
use crate::runner_client::spawn_runner_transport;
|
||||
use crate::spawn_prep::prepare_elevated_spawn_context;
|
||||
use anyhow::Result;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use codex_utils_pty::ProcessDriver;
|
||||
use codex_utils_pty::SpawnedProcess;
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use tokio::sync::broadcast;
|
||||
use tokio::sync::mpsc;
|
||||
use tokio::sync::oneshot;
|
||||
@@ -27,10 +29,23 @@ pub(crate) async fn spawn_windows_sandbox_session_elevated(
|
||||
cwd: &Path,
|
||||
mut env_map: HashMap<String, String>,
|
||||
timeout_ms: Option<u64>,
|
||||
read_roots_override: Option<&[PathBuf]>,
|
||||
read_roots_include_platform_defaults: bool,
|
||||
write_roots_override: Option<&[PathBuf]>,
|
||||
deny_read_paths_override: &[AbsolutePathBuf],
|
||||
deny_write_paths_override: &[AbsolutePathBuf],
|
||||
tty: bool,
|
||||
stdin_open: bool,
|
||||
use_private_desktop: bool,
|
||||
) -> Result<SpawnedProcess> {
|
||||
let deny_read_paths_override = deny_read_paths_override
|
||||
.iter()
|
||||
.map(AbsolutePathBuf::to_path_buf)
|
||||
.collect::<Vec<_>>();
|
||||
let deny_write_paths_override = deny_write_paths_override
|
||||
.iter()
|
||||
.map(AbsolutePathBuf::to_path_buf)
|
||||
.collect::<Vec<_>>();
|
||||
let elevated = prepare_elevated_spawn_context(
|
||||
policy_json_or_preset,
|
||||
sandbox_policy_cwd,
|
||||
@@ -38,6 +53,11 @@ pub(crate) async fn spawn_windows_sandbox_session_elevated(
|
||||
cwd,
|
||||
&mut env_map,
|
||||
&command,
|
||||
read_roots_override,
|
||||
read_roots_include_platform_defaults,
|
||||
write_roots_override,
|
||||
&deny_read_paths_override,
|
||||
&deny_write_paths_override,
|
||||
)?;
|
||||
|
||||
let spawn_request = SpawnRequest {
|
||||
|
||||
@@ -16,6 +16,7 @@ use crate::spawn_prep::apply_legacy_session_acl_rules;
|
||||
use crate::spawn_prep::prepare_legacy_session_security;
|
||||
use crate::spawn_prep::prepare_legacy_spawn_context;
|
||||
use anyhow::Result;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use codex_utils_pty::ProcessDriver;
|
||||
use codex_utils_pty::SpawnedProcess;
|
||||
use codex_utils_pty::TerminalSize;
|
||||
@@ -287,6 +288,8 @@ pub(crate) async fn spawn_windows_sandbox_session_legacy(
|
||||
cwd: &Path,
|
||||
mut env_map: HashMap<String, String>,
|
||||
timeout_ms: Option<u64>,
|
||||
additional_deny_read_paths: &[AbsolutePathBuf],
|
||||
additional_deny_write_paths: &[AbsolutePathBuf],
|
||||
tty: bool,
|
||||
stdin_open: bool,
|
||||
use_private_desktop: bool,
|
||||
@@ -303,6 +306,19 @@ pub(crate) async fn spawn_windows_sandbox_session_legacy(
|
||||
if !common.policy.has_full_disk_read_access() {
|
||||
anyhow::bail!("Restricted read-only access requires the elevated Windows sandbox backend");
|
||||
}
|
||||
// WRITE_RESTRICTED tokens consult restricting SIDs only for writes, so this
|
||||
// backend cannot make capability-SID deny-read ACLs authoritative.
|
||||
if !additional_deny_read_paths.is_empty() {
|
||||
anyhow::bail!("deny-read overrides require the elevated Windows sandbox backend");
|
||||
}
|
||||
let additional_deny_read_paths = additional_deny_read_paths
|
||||
.iter()
|
||||
.map(AbsolutePathBuf::to_path_buf)
|
||||
.collect::<Vec<_>>();
|
||||
let additional_deny_write_paths = additional_deny_write_paths
|
||||
.iter()
|
||||
.map(AbsolutePathBuf::to_path_buf)
|
||||
.collect::<Vec<_>>();
|
||||
let security = prepare_legacy_session_security(&common.policy, codex_home, cwd)?;
|
||||
allow_null_device_for_workspace_write(common.is_workspace_write);
|
||||
|
||||
@@ -310,12 +326,16 @@ pub(crate) async fn spawn_windows_sandbox_session_legacy(
|
||||
let guards = apply_legacy_session_acl_rules(
|
||||
&common.policy,
|
||||
sandbox_policy_cwd,
|
||||
codex_home,
|
||||
&common.current_dir,
|
||||
&env_map,
|
||||
&security.psid_generic,
|
||||
security.psid_workspace.as_ref(),
|
||||
&security.cap_sid_str,
|
||||
&additional_deny_read_paths,
|
||||
&additional_deny_write_paths,
|
||||
persist_aces,
|
||||
);
|
||||
)?;
|
||||
|
||||
let (writer_tx, writer_rx) = mpsc::channel::<Vec<u8>>(128);
|
||||
let (stdout_tx, stdout_rx) = broadcast::channel::<Vec<u8>>(256);
|
||||
|
||||
@@ -10,9 +10,11 @@
|
||||
mod backends;
|
||||
|
||||
use anyhow::Result;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use codex_utils_pty::SpawnedProcess;
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn spawn_windows_sandbox_session_legacy(
|
||||
@@ -23,6 +25,8 @@ pub async fn spawn_windows_sandbox_session_legacy(
|
||||
cwd: &Path,
|
||||
env_map: HashMap<String, String>,
|
||||
timeout_ms: Option<u64>,
|
||||
additional_deny_read_paths: &[AbsolutePathBuf],
|
||||
additional_deny_write_paths: &[AbsolutePathBuf],
|
||||
tty: bool,
|
||||
stdin_open: bool,
|
||||
use_private_desktop: bool,
|
||||
@@ -35,6 +39,8 @@ pub async fn spawn_windows_sandbox_session_legacy(
|
||||
cwd,
|
||||
env_map,
|
||||
timeout_ms,
|
||||
additional_deny_read_paths,
|
||||
additional_deny_write_paths,
|
||||
tty,
|
||||
stdin_open,
|
||||
use_private_desktop,
|
||||
@@ -51,6 +57,11 @@ pub async fn spawn_windows_sandbox_session_elevated(
|
||||
cwd: &Path,
|
||||
env_map: HashMap<String, String>,
|
||||
timeout_ms: Option<u64>,
|
||||
read_roots_override: Option<&[PathBuf]>,
|
||||
read_roots_include_platform_defaults: bool,
|
||||
write_roots_override: Option<&[PathBuf]>,
|
||||
deny_read_paths_override: &[AbsolutePathBuf],
|
||||
deny_write_paths_override: &[AbsolutePathBuf],
|
||||
tty: bool,
|
||||
stdin_open: bool,
|
||||
use_private_desktop: bool,
|
||||
@@ -63,6 +74,11 @@ pub async fn spawn_windows_sandbox_session_elevated(
|
||||
cwd,
|
||||
env_map,
|
||||
timeout_ms,
|
||||
read_roots_override,
|
||||
read_roots_include_platform_defaults,
|
||||
write_roots_override,
|
||||
deny_read_paths_override,
|
||||
deny_write_paths_override,
|
||||
tty,
|
||||
stdin_open,
|
||||
use_private_desktop,
|
||||
|
||||
@@ -5,6 +5,7 @@ use crate::ipc_framed::Message;
|
||||
use crate::ipc_framed::decode_bytes;
|
||||
use crate::ipc_framed::read_frame;
|
||||
use crate::run_windows_sandbox_capture;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use codex_utils_pty::ProcessDriver;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::collections::HashMap;
|
||||
@@ -160,6 +161,8 @@ fn legacy_non_tty_cmd_emits_output() {
|
||||
cwd.as_path(),
|
||||
HashMap::new(),
|
||||
Some(5_000),
|
||||
&[],
|
||||
&[],
|
||||
/*tty*/ false,
|
||||
/*stdin_open*/ false,
|
||||
/*use_private_desktop*/ true,
|
||||
@@ -176,6 +179,44 @@ fn legacy_non_tty_cmd_emits_output() {
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_non_tty_cmd_rejects_deny_read_overrides() {
|
||||
let _guard = legacy_process_test_guard();
|
||||
let runtime = current_thread_runtime();
|
||||
runtime.block_on(async move {
|
||||
let cwd = sandbox_cwd();
|
||||
let codex_home = sandbox_home("legacy-non-tty-deny-read");
|
||||
let secret_path =
|
||||
AbsolutePathBuf::from_absolute_path(cwd.join("legacy-non-tty-deny-read-secret.env"))
|
||||
.expect("absolute deny-read fixture path");
|
||||
let err = spawn_windows_sandbox_session_legacy(
|
||||
"workspace-write",
|
||||
cwd.as_path(),
|
||||
codex_home.path(),
|
||||
vec![
|
||||
"C:\\Windows\\System32\\cmd.exe".to_string(),
|
||||
"/c".to_string(),
|
||||
"echo deny-read".to_string(),
|
||||
],
|
||||
cwd.as_path(),
|
||||
HashMap::new(),
|
||||
Some(5_000),
|
||||
std::slice::from_ref(&secret_path),
|
||||
&[],
|
||||
/*tty*/ false,
|
||||
/*stdin_open*/ false,
|
||||
/*use_private_desktop*/ true,
|
||||
)
|
||||
.await
|
||||
.expect_err("legacy deny-read should require the elevated backend");
|
||||
assert!(
|
||||
err.to_string()
|
||||
.contains("deny-read overrides require the elevated Windows sandbox backend"),
|
||||
"unexpected error: {err:#}"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_non_tty_powershell_emits_output() {
|
||||
let Some(pwsh) = pwsh_path() else {
|
||||
@@ -200,6 +241,8 @@ fn legacy_non_tty_powershell_emits_output() {
|
||||
cwd.as_path(),
|
||||
HashMap::new(),
|
||||
Some(5_000),
|
||||
&[],
|
||||
&[],
|
||||
/*tty*/ false,
|
||||
/*stdin_open*/ false,
|
||||
/*use_private_desktop*/ true,
|
||||
@@ -424,6 +467,8 @@ fn legacy_tty_powershell_emits_output_and_accepts_input() {
|
||||
cwd.as_path(),
|
||||
HashMap::new(),
|
||||
Some(10_000),
|
||||
&[],
|
||||
&[],
|
||||
/*tty*/ true,
|
||||
/*stdin_open*/ true,
|
||||
/*use_private_desktop*/ true,
|
||||
@@ -472,6 +517,8 @@ fn legacy_tty_cmd_emits_output_and_accepts_input() {
|
||||
cwd.as_path(),
|
||||
HashMap::new(),
|
||||
Some(10_000),
|
||||
&[],
|
||||
&[],
|
||||
/*tty*/ true,
|
||||
/*stdin_open*/ true,
|
||||
/*use_private_desktop*/ true,
|
||||
@@ -523,6 +570,8 @@ fn legacy_tty_cmd_default_desktop_emits_output_and_accepts_input() {
|
||||
cwd.as_path(),
|
||||
HashMap::new(),
|
||||
Some(10_000),
|
||||
&[],
|
||||
&[],
|
||||
/*tty*/ true,
|
||||
/*stdin_open*/ true,
|
||||
/*use_private_desktop*/ false,
|
||||
|
||||
Reference in new issue
Block a user