fix: support split carveouts in windows elevated sandbox (#14568)

## Summary
- preserve legacy Windows elevated sandbox behavior for existing
policies
- add elevated-only support for split filesystem policies that can be
represented as readable-root overrides, writable-root overrides, and
extra deny-write carveouts
- resolve those elevated filesystem overrides during sandbox transform
and thread them through setup and policy refresh
- keep failing closed for explicit unreadable (`none`) carveouts and
reopened writable descendants under read-only carveouts
- for explicit read-only-under-writable-root carveouts, materialize
missing carveout directories during elevated setup before applying the
deny-write ACL
- document the elevated vs restricted-token support split in the core
README

## Example
Given a split filesystem policy like:

```toml
":root" = "read"
":cwd" = "write"
"./docs" = "read"
"C:/scratch" = "write"
```

the elevated backend now provisions the readable-root overrides,
writable-root overrides, and extra deny-write carveouts during setup and
refresh instead of collapsing back to the legacy workspace-only shape.

If a read-only carveout under a writable root is missing at setup time,
elevated setup creates that carveout as an empty directory before
applying its deny-write ACE; otherwise the sandboxed command could
create it later and bypass the carveout. This is only for explicit
policy carveouts. Best-effort workspace protections like `.codex/` and
`.agents/` still skip missing directories.

A policy like:

```toml
"/workspace" = "write"
"/workspace/docs" = "read"
"/workspace/docs/tmp" = "write"
```

still fails closed, because the elevated backend does not reopen
writable descendants under read-only carveouts yet.

---------

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
viyatb-oai
2026-04-09 17:34:52 -07:00
committed by GitHub
co-authored by Codex
parent 32224878b3
commit b976e701a8
11 changed files with 744 additions and 95 deletions
+207 -9
View File
@@ -278,7 +278,7 @@ async fn exec_full_buffer_capture_ignores_expiration() -> Result<()> {
SandboxType::None,
&SandboxPolicy::DangerFullAccess,
&FileSystemSandboxPolicy::unrestricted(),
/*windows_restricted_token_filesystem_overlay*/ None,
/*windows_sandbox_filesystem_overrides*/ None,
NetworkSandboxPolicy::Enabled,
/*stdout_stream*/ None,
/*after_spawn*/ None,
@@ -318,7 +318,7 @@ async fn exec_full_buffer_capture_keeps_io_drain_timeout_when_descendant_holds_p
SandboxType::None,
&SandboxPolicy::DangerFullAccess,
&FileSystemSandboxPolicy::unrestricted(),
/*windows_restricted_token_filesystem_overlay*/ None,
/*windows_sandbox_filesystem_overrides*/ None,
NetworkSandboxPolicy::Enabled,
/*stdout_stream*/ None,
/*after_spawn*/ None,
@@ -414,6 +414,22 @@ fn windows_restricted_token_runs_for_legacy_restricted_policies() {
);
}
#[test]
fn windows_proxy_enforcement_uses_elevated_backend() {
assert!(!windows_sandbox_uses_elevated_backend(
WindowsSandboxLevel::RestrictedToken,
/*proxy_enforced*/ false,
));
assert!(windows_sandbox_uses_elevated_backend(
WindowsSandboxLevel::RestrictedToken,
/*proxy_enforced*/ true,
));
assert!(windows_sandbox_uses_elevated_backend(
WindowsSandboxLevel::Elevated,
/*proxy_enforced*/ false,
));
}
#[test]
fn windows_restricted_token_rejects_network_only_restrictions() {
let policy = SandboxPolicy::ExternalSandbox {
@@ -481,6 +497,36 @@ fn windows_restricted_token_allows_legacy_workspace_write_policies() {
);
}
#[test]
fn windows_elevated_allows_legacy_restricted_read_policies() {
let temp_dir = tempfile::TempDir::new().expect("tempdir");
let docs = codex_utils_absolute_path::AbsolutePathBuf::from_absolute_path(
temp_dir.path().join("docs"),
)
.expect("absolute docs");
std::fs::create_dir_all(docs.as_path()).expect("create docs");
let policy = SandboxPolicy::ReadOnly {
access: codex_protocol::protocol::ReadOnlyAccess::Restricted {
readable_roots: vec![docs],
include_platform_defaults: false,
},
network_access: false,
};
let file_system_policy = FileSystemSandboxPolicy::from(&policy);
assert_eq!(
unsupported_windows_restricted_token_sandbox_reason(
SandboxType::WindowsRestrictedToken,
&policy,
&file_system_policy,
NetworkSandboxPolicy::Restricted,
temp_dir.path(),
WindowsSandboxLevel::Elevated,
),
None
);
}
#[test]
fn windows_restricted_token_rejects_split_only_filesystem_policies() {
let temp_dir = tempfile::TempDir::new().expect("tempdir");
@@ -618,7 +664,7 @@ fn windows_restricted_token_supports_full_read_split_write_read_carveouts() {
];
assert_eq!(
resolve_windows_restricted_token_filesystem_overlay(
resolve_windows_restricted_token_filesystem_overrides(
SandboxType::WindowsRestrictedToken,
&policy,
&file_system_policy,
@@ -626,23 +672,63 @@ fn windows_restricted_token_supports_full_read_split_write_read_carveouts() {
&cwd,
WindowsSandboxLevel::RestrictedToken,
),
Ok(Some(WindowsRestrictedTokenFilesystemOverlay {
Ok(Some(WindowsSandboxFilesystemOverrides {
read_roots_override: None,
write_roots_override: None,
additional_deny_write_paths: expected_deny_write_paths,
}))
);
}
#[test]
fn windows_elevated_rejects_split_write_read_carveouts() {
fn windows_elevated_supports_split_restricted_read_roots() {
let temp_dir = tempfile::TempDir::new().expect("tempdir");
let docs = temp_dir.path().join("docs");
std::fs::create_dir_all(&docs).expect("create docs");
let expected_docs = dunce::canonicalize(&docs).expect("canonical docs");
let policy = SandboxPolicy::ReadOnly {
access: codex_protocol::protocol::ReadOnlyAccess::FullAccess,
network_access: false,
};
let file_system_policy = FileSystemSandboxPolicy::restricted(vec![
codex_protocol::permissions::FileSystemSandboxEntry {
path: codex_protocol::permissions::FileSystemPath::Path {
path: codex_utils_absolute_path::AbsolutePathBuf::from_absolute_path(&docs)
.expect("absolute docs"),
},
access: codex_protocol::permissions::FileSystemAccessMode::Read,
},
]);
assert_eq!(
resolve_windows_elevated_filesystem_overrides(
SandboxType::WindowsRestrictedToken,
&policy,
&file_system_policy,
NetworkSandboxPolicy::Restricted,
temp_dir.path(),
/*use_windows_elevated_backend*/ true,
),
Ok(Some(WindowsSandboxFilesystemOverrides {
read_roots_override: Some(vec![expected_docs]),
write_roots_override: None,
additional_deny_write_paths: vec![],
}))
);
}
#[test]
fn windows_elevated_supports_split_write_read_carveouts() {
let temp_dir = tempfile::TempDir::new().expect("tempdir");
let docs = temp_dir.path().join("docs");
std::fs::create_dir_all(&docs).expect("create docs");
let expected_docs = dunce::canonicalize(&docs).expect("canonical docs");
let policy = SandboxPolicy::WorkspaceWrite {
writable_roots: vec![],
read_only_access: codex_protocol::protocol::ReadOnlyAccess::FullAccess,
network_access: false,
exclude_tmpdir_env_var: false,
exclude_slash_tmp: false,
exclude_tmpdir_env_var: true,
exclude_slash_tmp: true,
};
let file_system_policy = FileSystemSandboxPolicy::restricted(vec![
codex_protocol::permissions::FileSystemSandboxEntry {
@@ -666,6 +752,60 @@ fn windows_elevated_rejects_split_write_read_carveouts() {
},
]);
assert_eq!(
resolve_windows_elevated_filesystem_overrides(
SandboxType::WindowsRestrictedToken,
&policy,
&file_system_policy,
NetworkSandboxPolicy::Restricted,
temp_dir.path(),
/*use_windows_elevated_backend*/ true,
),
Ok(Some(WindowsSandboxFilesystemOverrides {
read_roots_override: None,
write_roots_override: None,
additional_deny_write_paths: vec![
codex_utils_absolute_path::AbsolutePathBuf::from_absolute_path(expected_docs)
.expect("absolute docs"),
],
}))
);
}
#[test]
fn windows_elevated_rejects_unreadable_split_carveouts() {
let temp_dir = tempfile::TempDir::new().expect("tempdir");
let blocked = temp_dir.path().join("blocked");
std::fs::create_dir_all(&blocked).expect("create blocked");
let policy = SandboxPolicy::WorkspaceWrite {
writable_roots: vec![],
read_only_access: codex_protocol::protocol::ReadOnlyAccess::FullAccess,
network_access: false,
exclude_tmpdir_env_var: true,
exclude_slash_tmp: true,
};
let file_system_policy = FileSystemSandboxPolicy::restricted(vec![
codex_protocol::permissions::FileSystemSandboxEntry {
path: codex_protocol::permissions::FileSystemPath::Special {
value: codex_protocol::permissions::FileSystemSpecialPath::Root,
},
access: codex_protocol::permissions::FileSystemAccessMode::Read,
},
codex_protocol::permissions::FileSystemSandboxEntry {
path: codex_protocol::permissions::FileSystemPath::Special {
value: codex_protocol::permissions::FileSystemSpecialPath::CurrentWorkingDirectory,
},
access: codex_protocol::permissions::FileSystemAccessMode::Write,
},
codex_protocol::permissions::FileSystemSandboxEntry {
path: codex_protocol::permissions::FileSystemPath::Path {
path: codex_utils_absolute_path::AbsolutePathBuf::from_absolute_path(&blocked)
.expect("absolute blocked"),
},
access: codex_protocol::permissions::FileSystemAccessMode::None,
},
]);
assert_eq!(
unsupported_windows_restricted_token_sandbox_reason(
SandboxType::WindowsRestrictedToken,
@@ -676,7 +816,65 @@ fn windows_elevated_rejects_split_write_read_carveouts() {
WindowsSandboxLevel::Elevated,
),
Some(
"windows elevated sandbox backend cannot enforce split filesystem permissions directly; refusing to run unsandboxed"
"windows elevated sandbox cannot enforce unreadable split filesystem carveouts directly; refusing to run unsandboxed"
.to_string()
)
);
}
#[test]
fn windows_elevated_rejects_reopened_writable_descendants() {
let temp_dir = tempfile::TempDir::new().expect("tempdir");
let docs = temp_dir.path().join("docs");
let nested = docs.join("nested");
std::fs::create_dir_all(&nested).expect("create nested");
let policy = SandboxPolicy::WorkspaceWrite {
writable_roots: vec![],
read_only_access: codex_protocol::protocol::ReadOnlyAccess::FullAccess,
network_access: false,
exclude_tmpdir_env_var: true,
exclude_slash_tmp: true,
};
let file_system_policy = FileSystemSandboxPolicy::restricted(vec![
codex_protocol::permissions::FileSystemSandboxEntry {
path: codex_protocol::permissions::FileSystemPath::Special {
value: codex_protocol::permissions::FileSystemSpecialPath::Root,
},
access: codex_protocol::permissions::FileSystemAccessMode::Read,
},
codex_protocol::permissions::FileSystemSandboxEntry {
path: codex_protocol::permissions::FileSystemPath::Special {
value: codex_protocol::permissions::FileSystemSpecialPath::CurrentWorkingDirectory,
},
access: codex_protocol::permissions::FileSystemAccessMode::Write,
},
codex_protocol::permissions::FileSystemSandboxEntry {
path: codex_protocol::permissions::FileSystemPath::Path {
path: codex_utils_absolute_path::AbsolutePathBuf::from_absolute_path(&docs)
.expect("absolute docs"),
},
access: codex_protocol::permissions::FileSystemAccessMode::Read,
},
codex_protocol::permissions::FileSystemSandboxEntry {
path: codex_protocol::permissions::FileSystemPath::Path {
path: codex_utils_absolute_path::AbsolutePathBuf::from_absolute_path(&nested)
.expect("absolute nested"),
},
access: codex_protocol::permissions::FileSystemAccessMode::Write,
},
]);
assert_eq!(
unsupported_windows_restricted_token_sandbox_reason(
SandboxType::WindowsRestrictedToken,
&policy,
&file_system_policy,
NetworkSandboxPolicy::Restricted,
temp_dir.path(),
WindowsSandboxLevel::Elevated,
),
Some(
"windows elevated sandbox cannot reopen writable descendants under read-only carveouts directly; refusing to run unsandboxed"
.to_string()
)
);
@@ -744,7 +942,7 @@ async fn kill_child_process_group_kills_grandchildren_on_timeout() -> Result<()>
SandboxType::None,
&SandboxPolicy::new_read_only_policy(),
&FileSystemSandboxPolicy::from(&SandboxPolicy::new_read_only_policy()),
None,
/*windows_sandbox_filesystem_overrides*/ None,
NetworkSandboxPolicy::Restricted,
/*stdout_stream*/ None,
/*after_spawn*/ None,