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
Unverified
parent 32224878b3
commit b976e701a8
11 changed files with 744 additions and 95 deletions
+251 -48
View File
@@ -94,17 +94,30 @@ pub struct ExecParams {
pub arg0: Option<String>,
}
/// Extra filesystem deny-write carveouts for the non-elevated Windows
/// restricted-token backend.
/// Resolved filesystem overrides for the Windows sandbox backends.
///
/// These are applied on top of the legacy `WorkspaceWrite` allow set, so we
/// can support a narrow split-policy subset without changing legacy Windows
/// sandbox semantics.
/// The unelevated restricted-token backend only consumes extra deny-write
/// carveouts on top of the legacy `WorkspaceWrite` allow set. The elevated
/// backend can also consume explicit read and write roots during setup/refresh.
/// Read-root overrides are layered on top of the baseline helper/platform roots
/// that the elevated setup path needs to launch the sandboxed command.
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct WindowsRestrictedTokenFilesystemOverlay {
pub(crate) struct WindowsSandboxFilesystemOverrides {
pub(crate) read_roots_override: Option<Vec<PathBuf>>,
pub(crate) write_roots_override: Option<Vec<PathBuf>>,
pub(crate) additional_deny_write_paths: Vec<AbsolutePathBuf>,
}
fn windows_sandbox_uses_elevated_backend(
sandbox_level: WindowsSandboxLevel,
proxy_enforced: bool,
) -> bool {
// Windows firewall enforcement is tied to the logon-user sandbox identities, so
// proxy-enforced sessions must use that backend even when the configured mode is
// the default restricted-token sandbox.
proxy_enforced || matches!(sandbox_level, WindowsSandboxLevel::Elevated)
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum ExecCapturePolicy {
/// Shell-like execs keep the historical output cap and timeout behavior.
@@ -300,8 +313,21 @@ pub fn build_exec_request(
})
.map(|request| ExecRequest::from_sandbox_exec_request(request, options))
.map_err(CodexErr::from)?;
exec_req.windows_restricted_token_filesystem_overlay =
resolve_windows_restricted_token_filesystem_overlay(
let use_windows_elevated_backend = windows_sandbox_uses_elevated_backend(
exec_req.windows_sandbox_level,
exec_req.network.is_some(),
);
exec_req.windows_sandbox_filesystem_overrides = if use_windows_elevated_backend {
resolve_windows_elevated_filesystem_overrides(
exec_req.sandbox,
&exec_req.sandbox_policy,
&exec_req.file_system_sandbox_policy,
exec_req.network_sandbox_policy,
sandbox_cwd,
use_windows_elevated_backend,
)
} else {
resolve_windows_restricted_token_filesystem_overrides(
exec_req.sandbox,
&exec_req.sandbox_policy,
&exec_req.file_system_sandbox_policy,
@@ -309,7 +335,8 @@ pub fn build_exec_request(
sandbox_cwd,
exec_req.windows_sandbox_level,
)
.map_err(CodexErr::UnsupportedOperation)?;
}
.map_err(CodexErr::UnsupportedOperation)?;
Ok(exec_req)
}
@@ -331,7 +358,7 @@ pub(crate) async fn execute_exec_request(
sandbox_policy,
file_system_sandbox_policy,
network_sandbox_policy,
windows_restricted_token_filesystem_overlay,
windows_sandbox_filesystem_overrides,
arg0,
} = exec_request;
@@ -355,7 +382,7 @@ pub(crate) async fn execute_exec_request(
sandbox,
&sandbox_policy,
&file_system_sandbox_policy,
windows_restricted_token_filesystem_overlay.as_ref(),
windows_sandbox_filesystem_overrides.as_ref(),
network_sandbox_policy,
stdout_stream,
after_spawn,
@@ -435,7 +462,7 @@ fn record_windows_sandbox_spawn_failure(
async fn exec_windows_sandbox(
params: ExecParams,
sandbox_policy: &SandboxPolicy,
windows_restricted_token_filesystem_overlay: Option<&WindowsRestrictedTokenFilesystemOverlay>,
windows_sandbox_filesystem_overrides: Option<&WindowsSandboxFilesystemOverrides>,
) -> Result<RawExecToolCallOutput> {
use crate::config::find_codex_home;
use codex_windows_sandbox::run_windows_sandbox_capture_elevated;
@@ -478,13 +505,23 @@ async fn exec_windows_sandbox(
let command_path = command.first().cloned();
let sandbox_level = windows_sandbox_level;
let proxy_enforced = network.is_some();
// Windows firewall enforcement is tied to the logon-user sandbox identities, so
// proxy-enforced sessions must use that backend even when the configured mode is
// the default restricted-token sandbox.
let use_elevated = proxy_enforced || matches!(sandbox_level, WindowsSandboxLevel::Elevated);
let additional_deny_write_paths = windows_restricted_token_filesystem_overlay
.map(|overlay| {
overlay
let use_elevated = windows_sandbox_uses_elevated_backend(sandbox_level, proxy_enforced);
let additional_deny_write_paths = windows_sandbox_filesystem_overrides
.map(|overrides| {
overrides
.additional_deny_write_paths
.iter()
.map(AbsolutePathBuf::to_path_buf)
.collect::<Vec<_>>()
})
.unwrap_or_default();
let elevated_read_roots_override = windows_sandbox_filesystem_overrides
.and_then(|overrides| overrides.read_roots_override.clone());
let elevated_write_roots_override = windows_sandbox_filesystem_overrides
.and_then(|overrides| overrides.write_roots_override.clone());
let elevated_deny_write_paths = windows_sandbox_filesystem_overrides
.map(|overrides| {
overrides
.additional_deny_write_paths
.iter()
.map(AbsolutePathBuf::to_path_buf)
@@ -504,6 +541,9 @@ async fn exec_windows_sandbox(
timeout_ms,
use_private_desktop: windows_sandbox_private_desktop,
proxy_enforced,
read_roots_override: elevated_read_roots_override.as_deref(),
write_roots_override: elevated_write_roots_override.as_deref(),
deny_write_paths_override: &elevated_deny_write_paths,
},
)
} else {
@@ -764,7 +804,7 @@ async fn exec(
_sandbox: SandboxType,
_sandbox_policy: &SandboxPolicy,
_file_system_sandbox_policy: &FileSystemSandboxPolicy,
_windows_restricted_token_filesystem_overlay: Option<&WindowsRestrictedTokenFilesystemOverlay>,
_windows_sandbox_filesystem_overrides: Option<&WindowsSandboxFilesystemOverrides>,
network_sandbox_policy: NetworkSandboxPolicy,
stdout_stream: Option<StdoutStream>,
after_spawn: Option<Box<dyn FnOnce() + Send>>,
@@ -774,7 +814,7 @@ async fn exec(
return exec_windows_sandbox(
params,
_sandbox_policy,
_windows_restricted_token_filesystem_overlay,
_windows_sandbox_filesystem_overrides,
)
.await;
}
@@ -843,26 +883,40 @@ pub(crate) fn unsupported_windows_restricted_token_sandbox_reason(
sandbox_policy_cwd: &Path,
windows_sandbox_level: WindowsSandboxLevel,
) -> Option<String> {
resolve_windows_restricted_token_filesystem_overlay(
sandbox,
sandbox_policy,
file_system_sandbox_policy,
network_sandbox_policy,
sandbox_policy_cwd,
windows_sandbox_level,
)
.err()
if windows_sandbox_level == WindowsSandboxLevel::Elevated {
resolve_windows_elevated_filesystem_overrides(
sandbox,
sandbox_policy,
file_system_sandbox_policy,
network_sandbox_policy,
sandbox_policy_cwd,
windows_sandbox_level == WindowsSandboxLevel::Elevated,
)
.err()
} else {
resolve_windows_restricted_token_filesystem_overrides(
sandbox,
sandbox_policy,
file_system_sandbox_policy,
network_sandbox_policy,
sandbox_policy_cwd,
windows_sandbox_level,
)
.err()
}
}
pub(crate) fn resolve_windows_restricted_token_filesystem_overlay(
pub(crate) fn resolve_windows_restricted_token_filesystem_overrides(
sandbox: SandboxType,
sandbox_policy: &SandboxPolicy,
file_system_sandbox_policy: &FileSystemSandboxPolicy,
network_sandbox_policy: NetworkSandboxPolicy,
sandbox_policy_cwd: &Path,
windows_sandbox_level: WindowsSandboxLevel,
) -> std::result::Result<Option<WindowsRestrictedTokenFilesystemOverlay>, String> {
if sandbox != SandboxType::WindowsRestrictedToken {
) -> std::result::Result<Option<WindowsSandboxFilesystemOverrides>, String> {
if sandbox != SandboxType::WindowsRestrictedToken
|| windows_sandbox_level == WindowsSandboxLevel::Elevated
{
return Ok(None);
}
@@ -889,13 +943,6 @@ pub(crate) fn resolve_windows_restricted_token_filesystem_overlay(
));
}
if windows_sandbox_level != WindowsSandboxLevel::RestrictedToken {
return Err(
"windows elevated sandbox backend cannot enforce split filesystem permissions directly; refusing to run unsandboxed"
.to_string(),
);
}
if !file_system_sandbox_policy.has_full_disk_read_access() {
return Err(
"windows unelevated restricted-token sandbox cannot enforce split filesystem read restrictions directly; refusing to run unsandboxed"
@@ -918,11 +965,11 @@ pub(crate) fn resolve_windows_restricted_token_filesystem_overlay(
file_system_sandbox_policy.get_writable_roots_with_cwd(sandbox_policy_cwd);
let legacy_root_paths: BTreeSet<PathBuf> = legacy_writable_roots
.iter()
.map(|root| normalize_windows_overlay_path(root.root.as_path()))
.map(|root| normalize_windows_override_path(root.root.as_path()))
.collect::<std::result::Result<_, _>>()?;
let split_root_paths: BTreeSet<PathBuf> = split_writable_roots
.iter()
.map(|root| normalize_windows_overlay_path(root.root.as_path()))
.map(|root| normalize_windows_override_path(root.root.as_path()))
.collect::<std::result::Result<_, _>>()?;
if legacy_root_paths != split_root_paths {
@@ -951,9 +998,9 @@ pub(crate) fn resolve_windows_restricted_token_filesystem_overlay(
let mut additional_deny_write_paths = BTreeSet::new();
for split_root in &split_writable_roots {
let split_root_path = normalize_windows_overlay_path(split_root.root.as_path())?;
let split_root_path = normalize_windows_override_path(split_root.root.as_path())?;
let Some(legacy_root) = legacy_writable_roots.iter().find(|candidate| {
normalize_windows_overlay_path(candidate.root.as_path())
normalize_windows_override_path(candidate.root.as_path())
.is_ok_and(|candidate_path| candidate_path == split_root_path)
}) else {
return Err(
@@ -968,8 +1015,9 @@ pub(crate) fn resolve_windows_restricted_token_filesystem_overlay(
.iter()
.any(|candidate| candidate == read_only_subpath)
{
additional_deny_write_paths
.insert(normalize_windows_overlay_path(read_only_subpath.as_path())?);
additional_deny_write_paths.insert(normalize_windows_override_path(
read_only_subpath.as_path(),
)?);
}
}
}
@@ -978,7 +1026,9 @@ pub(crate) fn resolve_windows_restricted_token_filesystem_overlay(
return Ok(None);
}
Ok(Some(WindowsRestrictedTokenFilesystemOverlay {
Ok(Some(WindowsSandboxFilesystemOverrides {
read_roots_override: None,
write_roots_override: None,
additional_deny_write_paths: additional_deny_write_paths
.into_iter()
.map(|path| AbsolutePathBuf::from_absolute_path(path).map_err(|err| err.to_string()))
@@ -986,12 +1036,165 @@ pub(crate) fn resolve_windows_restricted_token_filesystem_overlay(
}))
}
fn normalize_windows_overlay_path(path: &Path) -> std::result::Result<PathBuf, String> {
fn normalize_windows_override_path(path: &Path) -> std::result::Result<PathBuf, String> {
AbsolutePathBuf::from_absolute_path(dunce::simplified(path))
.map(AbsolutePathBuf::into_path_buf)
.map_err(|err| err.to_string())
}
pub(crate) fn resolve_windows_elevated_filesystem_overrides(
sandbox: SandboxType,
sandbox_policy: &SandboxPolicy,
file_system_sandbox_policy: &FileSystemSandboxPolicy,
network_sandbox_policy: NetworkSandboxPolicy,
sandbox_policy_cwd: &Path,
use_windows_elevated_backend: bool,
) -> std::result::Result<Option<WindowsSandboxFilesystemOverrides>, String> {
if sandbox != SandboxType::WindowsRestrictedToken || !use_windows_elevated_backend {
return Ok(None);
}
if !should_use_windows_restricted_token_sandbox(
sandbox,
sandbox_policy,
file_system_sandbox_policy,
) {
return Err(format!(
"windows sandbox backend cannot enforce file_system={:?}, network={network_sandbox_policy:?}, legacy_policy={sandbox_policy:?}; refusing to run unsandboxed",
file_system_sandbox_policy.kind,
));
}
if !file_system_sandbox_policy
.get_unreadable_roots_with_cwd(sandbox_policy_cwd)
.is_empty()
{
return Err(
"windows elevated sandbox cannot enforce unreadable split filesystem carveouts directly; refusing to run unsandboxed"
.to_string(),
);
}
let split_writable_roots =
file_system_sandbox_policy.get_writable_roots_with_cwd(sandbox_policy_cwd);
if has_reopened_writable_descendant(&split_writable_roots) {
return Err(
"windows elevated sandbox cannot reopen writable descendants under read-only carveouts directly; refusing to run unsandboxed"
.to_string(),
);
}
let needs_direct_runtime_enforcement = file_system_sandbox_policy
.needs_direct_runtime_enforcement(network_sandbox_policy, sandbox_policy_cwd);
let normalize_path = |path: PathBuf| dunce::canonicalize(&path).unwrap_or(path);
let legacy_writable_roots = sandbox_policy.get_writable_roots_with_cwd(sandbox_policy_cwd);
let legacy_readable_root_set: BTreeSet<PathBuf> = sandbox_policy
.get_readable_roots_with_cwd(sandbox_policy_cwd)
.into_iter()
.map(codex_utils_absolute_path::AbsolutePathBuf::into_path_buf)
.map(&normalize_path)
.collect();
let legacy_root_paths: BTreeSet<PathBuf> = legacy_writable_roots
.iter()
.map(|root| normalize_path(root.root.to_path_buf()))
.collect();
let split_readable_roots: Vec<PathBuf> = file_system_sandbox_policy
.get_readable_roots_with_cwd(sandbox_policy_cwd)
.into_iter()
.map(codex_utils_absolute_path::AbsolutePathBuf::into_path_buf)
.map(&normalize_path)
.collect();
let split_readable_root_set: BTreeSet<PathBuf> = split_readable_roots.iter().cloned().collect();
let split_root_paths: Vec<PathBuf> = split_writable_roots
.iter()
.map(|root| normalize_path(root.root.to_path_buf()))
.collect();
let split_root_path_set: BTreeSet<PathBuf> = split_root_paths.iter().cloned().collect();
let matches_legacy_read_access = file_system_sandbox_policy.has_full_disk_read_access()
== sandbox_policy.has_full_disk_read_access();
let read_roots_override = if matches_legacy_read_access
&& (file_system_sandbox_policy.has_full_disk_read_access()
|| split_readable_root_set == legacy_readable_root_set)
{
None
} else {
Some(split_readable_roots)
};
let write_roots_override = if split_root_path_set == legacy_root_paths {
None
} else {
Some(split_root_paths)
};
let additional_deny_write_paths = if needs_direct_runtime_enforcement {
let mut deny_paths = BTreeSet::new();
for writable_root in &split_writable_roots {
let writable_root_path = normalize_path(writable_root.root.to_path_buf());
let legacy_root = legacy_writable_roots.iter().find(|candidate| {
normalize_path(candidate.root.to_path_buf()) == writable_root_path
});
for read_only_subpath in &writable_root.read_only_subpaths {
let read_only_subpath_suffix = read_only_subpath
.as_path()
.strip_prefix(writable_root.root.as_path())
.ok();
let already_denied_by_legacy = legacy_root.is_some_and(|legacy_root| {
legacy_root.read_only_subpaths.iter().any(|candidate| {
candidate
.as_path()
.strip_prefix(legacy_root.root.as_path())
.ok()
== read_only_subpath_suffix
})
});
if !already_denied_by_legacy {
deny_paths.insert(normalize_path(read_only_subpath.to_path_buf()));
}
}
}
deny_paths
.into_iter()
.map(|path| AbsolutePathBuf::from_absolute_path(path).map_err(|err| err.to_string()))
.collect::<std::result::Result<_, _>>()?
} else {
Vec::new()
};
if read_roots_override.is_none()
&& write_roots_override.is_none()
&& additional_deny_write_paths.is_empty()
{
return Ok(None);
}
Ok(Some(WindowsSandboxFilesystemOverrides {
read_roots_override,
write_roots_override,
additional_deny_write_paths,
}))
}
fn has_reopened_writable_descendant(
writable_roots: &[codex_protocol::protocol::WritableRoot],
) -> bool {
writable_roots.iter().any(|writable_root| {
writable_root
.read_only_subpaths
.iter()
.any(|read_only_subpath| {
writable_roots.iter().any(|candidate| {
candidate.root.as_path() != writable_root.root.as_path()
&& candidate
.root
.as_path()
.starts_with(read_only_subpath.as_path())
})
})
})
}
/// Consumes the output of a child process according to the configured capture
/// policy.
async fn consume_output(
+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,
+4 -5
View File
@@ -10,7 +10,7 @@ ExecRequest for execution.
use crate::exec::ExecCapturePolicy;
use crate::exec::ExecExpiration;
use crate::exec::StdoutStream;
use crate::exec::WindowsRestrictedTokenFilesystemOverlay;
use crate::exec::WindowsSandboxFilesystemOverrides;
use crate::exec::execute_exec_request;
#[cfg(target_os = "macos")]
use crate::spawn::CODEX_SANDBOX_ENV_VAR;
@@ -47,8 +47,7 @@ pub struct ExecRequest {
pub sandbox_policy: SandboxPolicy,
pub file_system_sandbox_policy: FileSystemSandboxPolicy,
pub network_sandbox_policy: NetworkSandboxPolicy,
pub(crate) windows_restricted_token_filesystem_overlay:
Option<WindowsRestrictedTokenFilesystemOverlay>,
pub(crate) windows_sandbox_filesystem_overrides: Option<WindowsSandboxFilesystemOverrides>,
pub arg0: Option<String>,
}
@@ -82,7 +81,7 @@ impl ExecRequest {
sandbox_policy,
file_system_sandbox_policy,
network_sandbox_policy,
windows_restricted_token_filesystem_overlay: None,
windows_sandbox_filesystem_overrides: None,
arg0,
}
}
@@ -131,7 +130,7 @@ impl ExecRequest {
sandbox_policy,
file_system_sandbox_policy,
network_sandbox_policy,
windows_restricted_token_filesystem_overlay: None,
windows_sandbox_filesystem_overrides: None,
arg0,
}
}
+1 -1
View File
@@ -176,7 +176,7 @@ pub(crate) async fn execute_user_shell_command(
sandbox_policy: sandbox_policy.clone(),
file_system_sandbox_policy: FileSystemSandboxPolicy::from(&sandbox_policy),
network_sandbox_policy: NetworkSandboxPolicy::from(&sandbox_policy),
windows_restricted_token_filesystem_overlay: None,
windows_sandbox_filesystem_overrides: None,
arg0: None,
};
@@ -133,7 +133,7 @@ pub(super) async fn try_run_zsh_fork(
sandbox_policy,
file_system_sandbox_policy,
network_sandbox_policy,
windows_restricted_token_filesystem_overlay: _windows_restricted_token_filesystem_overlay,
windows_sandbox_filesystem_overrides: _windows_sandbox_filesystem_overrides,
arg0,
} = sandbox_exec_request;
let ParsedShellCommand { script, login, .. } = extract_shell_script(&command)?;
@@ -722,7 +722,7 @@ impl ShellCommandExecutor for CoreShellCommandExecutor {
sandbox_policy: self.sandbox_policy.clone(),
file_system_sandbox_policy: self.file_system_sandbox_policy.clone(),
network_sandbox_policy: self.network_sandbox_policy,
windows_restricted_token_filesystem_overlay: None,
windows_sandbox_filesystem_overrides: None,
arg0: self.arg0.clone(),
},
/*stdout_stream*/ None,