Run fs helper through Windows sandbox wrapper (#28359)

## Why

This is the final PR in the Windows fs-helper sandbox stack and contains
the actual bug fix.

The exec-server filesystem helper is a direct-spawn path: it asks
`SandboxManager` for a `SandboxExecRequest`, then launches the returned
argv itself. That works on macOS and Linux because the transformed argv
is already a self-contained sandbox wrapper. On Windows, the transformed
request carried `WindowsRestrictedToken` metadata, but the direct-spawn
fs-helper runner still launched the helper argv directly.

That means Windows filesystem built-ins backed by the fs-helper could
run with the parent Codex process permissions instead of the configured
Windows sandbox. This PR makes the direct-spawn transform produce a
self-contained Windows wrapper argv before fs-helper launches it.

## What Changed

- Added `SandboxManager::transform_for_direct_spawn()` for callers that
launch the returned argv themselves.
- Wrapped Windows restricted-token direct-spawn requests with `codex.exe
--run-as-windows-sandbox` and then marked the outer request as
unsandboxed, matching the macOS/Linux wrapper argv shape.
- Updated `exec-server/src/fs_sandbox.rs` to use the direct-spawn
transform for fs-helper launches.
- Materialized the inner `codex.exe --codex-run-as-fs-helper` executable
into `.sandbox-bin` so the sandboxed user can run it.
- Carried runtime workspace roots through `FileSystemSandboxContext` as
`PathUri` values so `:workspace_roots` policies resolve correctly
without sending native client paths over exec-server JSON.
- Preserved wrapper setup identity environment needed by Windows sandbox
setup without changing the serialized inner helper environment.

## Verification

- `just bazel-lock-update`
- `just bazel-lock-check`
- `just test -p codex-sandboxing transform_for_direct_spawn_windows`
- `just test -p codex-exec-server fs_sandbox::tests`
- `just fix -p codex-windows-sandbox -p codex-sandboxing -p
codex-exec-server -p codex-core -p codex-file-system`

Local note: `just fmt` completed Rust formatting, but this workstation
still fails the non-Rust formatter phases because uv cannot open its
cache and the local buildifier/dotslash path is missing.
This commit is contained in:
iceweasel-oai
2026-06-17 10:00:42 -07:00
committed by GitHub
parent c78911e37f
commit ef75171f18
16 changed files with 861 additions and 399 deletions
+9 -377
View File
@@ -1,9 +1,9 @@
#[cfg(unix)]
use std::os::unix::process::ExitStatusExt;
use std::collections::BTreeSet;
use std::collections::HashMap;
use std::io;
#[cfg(target_os = "windows")]
use std::path::Path;
use std::path::PathBuf;
use std::process::ExitStatus;
@@ -24,7 +24,6 @@ use crate::spawn::SpawnChildRequest;
use crate::spawn::StdioPolicy;
use crate::spawn::spawn_child_async;
use codex_network_proxy::NetworkProxy;
use codex_protocol::config_types::WindowsSandboxLevel;
use codex_protocol::error::CodexErr;
use codex_protocol::error::Result;
use codex_protocol::error::SandboxErr;
@@ -42,7 +41,14 @@ use codex_sandboxing::SandboxManager;
use codex_sandboxing::SandboxTransformRequest;
use codex_sandboxing::SandboxType;
use codex_sandboxing::SandboxablePreference;
use codex_sandboxing::compatibility_sandbox_policy_for_permission_profile;
use codex_sandboxing::WindowsSandboxFilesystemOverrides;
#[cfg(test)]
use codex_sandboxing::permission_profile_supports_windows_restricted_token_sandbox;
use codex_sandboxing::resolve_windows_elevated_filesystem_overrides;
use codex_sandboxing::resolve_windows_restricted_token_filesystem_overrides;
#[cfg(test)]
use codex_sandboxing::unsupported_windows_restricted_token_sandbox_reason;
use codex_sandboxing::windows_sandbox_uses_elevated_backend;
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_path_uri::PathUri;
use codex_utils_pty::DEFAULT_OUTPUT_BYTES_CAP;
@@ -96,34 +102,6 @@ pub struct ExecParams {
pub arg0: Option<String>,
}
/// Resolved filesystem overrides for the Windows sandbox backends.
///
/// The elevated Windows backend consumes extra deny-read paths plus explicit
/// read and write roots during setup/refresh. The unelevated restricted-token
/// backend only consumes extra deny-write carveouts on top of the legacy
/// `WorkspaceWrite` allow set. Read-root overrides are layered on top of the
/// baseline helper roots that the elevated setup path needs to launch the
/// sandboxed command; split policies that opt into platform defaults carry
/// that explicitly with the override.
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct WindowsSandboxFilesystemOverrides {
pub(crate) read_roots_override: Option<Vec<PathBuf>>,
pub(crate) read_roots_include_platform_defaults: bool,
pub(crate) write_roots_override: Option<Vec<PathBuf>>,
pub(crate) additional_deny_read_paths: Vec<AbsolutePathBuf>,
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.
@@ -997,352 +975,6 @@ async fn exec(
consume_output(child, expiration, capture_policy, stdout_stream).await
}
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
fn permission_profile_supports_windows_restricted_token_sandbox(
permission_profile: &PermissionProfile,
) -> bool {
match permission_profile {
PermissionProfile::Managed { file_system, .. } => {
!file_system.to_sandbox_policy().has_full_disk_write_access()
}
PermissionProfile::Disabled | PermissionProfile::External { .. } => false,
}
}
#[cfg_attr(not(test), allow(dead_code))]
pub(crate) fn unsupported_windows_restricted_token_sandbox_reason(
sandbox: SandboxType,
permission_profile: &PermissionProfile,
sandbox_policy_cwd: &AbsolutePathBuf,
windows_sandbox_level: WindowsSandboxLevel,
) -> Option<String> {
if windows_sandbox_level == WindowsSandboxLevel::Elevated {
resolve_windows_elevated_filesystem_overrides(
sandbox,
permission_profile,
sandbox_policy_cwd,
windows_sandbox_level == WindowsSandboxLevel::Elevated,
)
.err()
} else {
resolve_windows_restricted_token_filesystem_overrides(
sandbox,
permission_profile,
sandbox_policy_cwd,
windows_sandbox_level,
)
.err()
}
}
pub(crate) fn resolve_windows_restricted_token_filesystem_overrides(
sandbox: SandboxType,
permission_profile: &PermissionProfile,
sandbox_policy_cwd: &AbsolutePathBuf,
windows_sandbox_level: WindowsSandboxLevel,
) -> std::result::Result<Option<WindowsSandboxFilesystemOverrides>, String> {
if sandbox != SandboxType::WindowsRestrictedToken
|| windows_sandbox_level == WindowsSandboxLevel::Elevated
{
return Ok(None);
}
let (file_system_sandbox_policy, network_sandbox_policy) =
permission_profile.to_runtime_permissions();
let needs_direct_runtime_enforcement = file_system_sandbox_policy
.needs_direct_runtime_enforcement(network_sandbox_policy, sandbox_policy_cwd);
if permission_profile_supports_windows_restricted_token_sandbox(permission_profile)
&& !needs_direct_runtime_enforcement
{
return Ok(None);
}
if !permission_profile_supports_windows_restricted_token_sandbox(permission_profile) {
let permission_profile_name = permission_profile_display_name(permission_profile);
return Err(format!(
"windows sandbox backend cannot enforce file_system={:?}, network={network_sandbox_policy:?}, permission_profile={permission_profile_name}; refusing to run unsandboxed",
file_system_sandbox_policy.kind,
));
}
// The restricted-token backend can still enforce split write restrictions,
// but its WRITE_RESTRICTED token does not make capability SID deny-read ACEs
// participate in read access checks. Read restrictions therefore require the
// elevated backend, even when the filesystem root remains readable.
if !windows_policy_has_root_read_access(&file_system_sandbox_policy, sandbox_policy_cwd) {
return Err(
"windows unelevated restricted-token sandbox cannot enforce split filesystem read restrictions directly; refusing to run unsandboxed"
.to_string(),
);
}
let additional_deny_read_paths = codex_windows_sandbox::resolve_windows_deny_read_paths(
&file_system_sandbox_policy,
sandbox_policy_cwd,
)?;
if !additional_deny_read_paths.is_empty() {
return Err(
"windows unelevated restricted-token sandbox cannot enforce deny-read restrictions directly; refusing to run unsandboxed"
.to_string(),
);
}
let legacy_projection = compatibility_sandbox_policy_for_permission_profile(
permission_profile,
sandbox_policy_cwd.as_path(),
);
let legacy_writable_roots = legacy_projection.get_writable_roots_with_cwd(sandbox_policy_cwd);
let split_writable_roots =
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_override_path(root.root.as_path()))
.collect::<std::result::Result<_, _>>()?;
let split_root_paths: BTreeSet<PathBuf> = split_writable_roots
.iter()
.map(|root| normalize_windows_override_path(root.root.as_path()))
.collect::<std::result::Result<_, _>>()?;
if legacy_root_paths != split_root_paths {
return Err(
"windows unelevated restricted-token sandbox cannot enforce split writable root sets directly; refusing to run unsandboxed"
.to_string(),
);
}
for writable_root in &split_writable_roots {
for read_only_subpath in &writable_root.read_only_subpaths {
if split_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())
}) {
return Err(
"windows unelevated restricted-token sandbox cannot reopen writable descendants under read-only carveouts directly; refusing to run unsandboxed"
.to_string(),
);
}
}
}
let mut additional_deny_write_paths = BTreeSet::new();
for split_root in &split_writable_roots {
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_override_path(candidate.root.as_path())
.is_ok_and(|candidate_path| candidate_path == split_root_path)
}) else {
return Err(
"windows unelevated restricted-token sandbox cannot enforce split writable root sets directly; refusing to run unsandboxed"
.to_string(),
);
};
for read_only_subpath in &split_root.read_only_subpaths {
if !legacy_root
.read_only_subpaths
.iter()
.any(|candidate| candidate == read_only_subpath)
{
additional_deny_write_paths.insert(normalize_windows_override_path(
read_only_subpath.as_path(),
)?);
}
}
}
if additional_deny_read_paths.is_empty() && additional_deny_write_paths.is_empty() {
return Ok(None);
}
Ok(Some(WindowsSandboxFilesystemOverrides {
read_roots_override: None,
read_roots_include_platform_defaults: false,
write_roots_override: None,
additional_deny_read_paths,
additional_deny_write_paths: additional_deny_write_paths
.into_iter()
.map(|path| AbsolutePathBuf::from_absolute_path(path).map_err(|err| err.to_string()))
.collect::<std::result::Result<_, _>>()?,
}))
}
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())
}
fn windows_policy_has_root_read_access(
file_system_sandbox_policy: &FileSystemSandboxPolicy,
cwd: &AbsolutePathBuf,
) -> bool {
let Some(root) = cwd.as_path().ancestors().last() else {
return false;
};
file_system_sandbox_policy.can_read_path_with_cwd(root, cwd.as_path())
}
pub(crate) fn resolve_windows_elevated_filesystem_overrides(
sandbox: SandboxType,
permission_profile: &PermissionProfile,
sandbox_policy_cwd: &AbsolutePathBuf,
use_windows_elevated_backend: bool,
) -> std::result::Result<Option<WindowsSandboxFilesystemOverrides>, String> {
if sandbox != SandboxType::WindowsRestrictedToken || !use_windows_elevated_backend {
return Ok(None);
}
let (file_system_sandbox_policy, network_sandbox_policy) =
permission_profile.to_runtime_permissions();
if !permission_profile_supports_windows_restricted_token_sandbox(permission_profile) {
let permission_profile_name = permission_profile_display_name(permission_profile);
return Err(format!(
"windows sandbox backend cannot enforce file_system={:?}, network={network_sandbox_policy:?}, permission_profile={permission_profile_name}; refusing to run unsandboxed",
file_system_sandbox_policy.kind,
));
}
let additional_deny_read_paths = codex_windows_sandbox::resolve_windows_deny_read_paths(
&file_system_sandbox_policy,
sandbox_policy_cwd,
)?;
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_projection = compatibility_sandbox_policy_for_permission_profile(
permission_profile,
sandbox_policy_cwd.as_path(),
);
let legacy_writable_roots = legacy_projection.get_writable_roots_with_cwd(sandbox_policy_cwd);
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_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();
// `has_full_disk_read_access()` is intentionally false when deny-read
// entries exist. For Windows setup overrides, the important question is
// whether the baseline still reads from the filesystem root and only needs
// additional deny ACLs layered on top.
let split_has_root_read_access =
windows_policy_has_root_read_access(&file_system_sandbox_policy, sandbox_policy_cwd);
let read_roots_override = if split_has_root_read_access {
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_read_paths.is_empty()
&& additional_deny_write_paths.is_empty()
{
return Ok(None);
}
Ok(Some(WindowsSandboxFilesystemOverrides {
read_roots_include_platform_defaults: read_roots_override.is_some()
&& file_system_sandbox_policy.include_platform_defaults(),
read_roots_override,
write_roots_override,
additional_deny_read_paths,
additional_deny_write_paths,
}))
}
fn permission_profile_display_name(permission_profile: &PermissionProfile) -> &'static str {
match permission_profile {
PermissionProfile::Managed { .. } => "Managed",
PermissionProfile::Disabled => "Disabled",
PermissionProfile::External { .. } => "External",
}
}
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(
+1 -1
View File
@@ -10,7 +10,6 @@ ExecRequest for execution.
use crate::exec::ExecCapturePolicy;
use crate::exec::ExecExpiration;
use crate::exec::StdoutStream;
use crate::exec::WindowsSandboxFilesystemOverrides;
use crate::exec::execute_exec_request;
#[cfg(target_os = "macos")]
use crate::spawn::CODEX_SANDBOX_ENV_VAR;
@@ -24,6 +23,7 @@ use codex_protocol::permissions::FileSystemSandboxPolicy;
use codex_protocol::permissions::NetworkSandboxPolicy;
use codex_sandboxing::SandboxExecRequest;
use codex_sandboxing::SandboxType;
use codex_sandboxing::WindowsSandboxFilesystemOverrides;
use codex_utils_absolute_path::AbsolutePathBuf;
use std::collections::HashMap;
@@ -330,6 +330,12 @@ impl TurnContext {
FileSystemSandboxContext {
permissions: permissions.into(),
cwd: Some(cwd.clone()),
workspace_roots: self
.config
.effective_workspace_roots()
.iter()
.map(PathUri::from_abs_path)
.collect(),
windows_sandbox_level: self.windows_sandbox_level,
windows_sandbox_private_desktop: self
.config
@@ -33,6 +33,7 @@ use codex_sandboxing::SandboxType;
use codex_sandboxing::SandboxablePreference;
use codex_sandboxing::policy_transforms::effective_permission_profile;
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_path_uri::PathUri;
use futures::future::BoxFuture;
use std::path::PathBuf;
use std::time::Instant;
@@ -99,6 +100,11 @@ impl ApplyPatchRuntime {
Some(FileSystemSandboxContext {
permissions: permissions.into(),
cwd: Some(attempt.sandbox_cwd.clone()),
workspace_roots: attempt
.workspace_roots
.iter()
.map(PathUri::from_abs_path)
.collect(),
windows_sandbox_level: attempt.windows_sandbox_level,
windows_sandbox_private_desktop: attempt.windows_sandbox_private_desktop,
use_legacy_landlock: attempt.use_legacy_landlock,