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
Unverified
parent c78911e37f
commit ef75171f18
16 changed files with 861 additions and 399 deletions
+2
View File
@@ -3792,7 +3792,9 @@ dependencies = [
"codex-network-proxy",
"codex-protocol",
"codex-utils-absolute-path",
"codex-utils-home-dir",
"codex-utils-path-uri",
"codex-windows-sandbox",
"dunce",
"libc",
"pretty_assertions",
+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,
+46 -17
View File
@@ -9,6 +9,7 @@ use codex_protocol::permissions::FileSystemSandboxPolicy;
use codex_protocol::permissions::FileSystemSpecialPath;
use codex_protocol::permissions::NetworkSandboxPolicy;
use codex_sandboxing::SandboxCommand;
use codex_sandboxing::SandboxDirectSpawnTransformRequest;
use codex_sandboxing::SandboxExecRequest;
use codex_sandboxing::SandboxManager;
use codex_sandboxing::SandboxTransformRequest;
@@ -88,7 +89,7 @@ impl FileSystemSandboxRunner {
&file_system_policy,
network_policy,
);
let command = self.sandbox_exec_request(&permission_profile, &cwd.uri, sandbox)?;
let command = self.sandbox_exec_request(&permission_profile, &cwd, sandbox)?;
let request_json = serde_json::to_vec(&request).map_err(json_error)?;
run_command(command, request_json).await
}
@@ -96,7 +97,7 @@ impl FileSystemSandboxRunner {
fn sandbox_exec_request(
&self,
permission_profile: &PermissionProfile,
cwd: &PathUri,
cwd: &SandboxCwd,
sandbox_context: &FileSystemSandboxContext,
) -> Result<SandboxExecRequest, JSONRPCErrorError> {
let helper = &self.runtime_paths.codex_self_exe;
@@ -112,22 +113,36 @@ impl FileSystemSandboxRunner {
let command = SandboxCommand {
program: helper.as_path().as_os_str().to_owned(),
args: vec![CODEX_FS_HELPER_ARG1.to_string()],
cwd: cwd.clone(),
cwd: cwd.uri.clone(),
env: self.helper_env.clone(),
additional_permissions: None,
};
let native_workspace_roots = sandbox_context
.workspace_roots
.iter()
.map(native_workspace_root)
.collect::<Result<Vec<_>, _>>()?;
let workspace_roots = if native_workspace_roots.is_empty() {
std::slice::from_ref(&cwd.native)
} else {
native_workspace_roots.as_slice()
};
sandbox_manager
.transform(SandboxTransformRequest {
command,
permissions: permission_profile,
sandbox,
enforce_managed_network: false,
network: None,
sandbox_policy_cwd: cwd,
codex_linux_sandbox_exe: self.runtime_paths.codex_linux_sandbox_exe.as_deref(),
use_legacy_landlock: sandbox_context.use_legacy_landlock,
windows_sandbox_level: sandbox_context.windows_sandbox_level,
windows_sandbox_private_desktop: sandbox_context.windows_sandbox_private_desktop,
.transform_for_direct_spawn(SandboxDirectSpawnTransformRequest {
workspace_roots,
transform: SandboxTransformRequest {
command,
permissions: permission_profile,
sandbox,
enforce_managed_network: false,
network: None,
sandbox_policy_cwd: &cwd.uri,
codex_linux_sandbox_exe: self.runtime_paths.codex_linux_sandbox_exe.as_deref(),
use_legacy_landlock: sandbox_context.use_legacy_landlock,
windows_sandbox_level: sandbox_context.windows_sandbox_level,
windows_sandbox_private_desktop: sandbox_context
.windows_sandbox_private_desktop,
},
})
.map_err(|err| invalid_request(format!("failed to prepare fs sandbox: {err}")))
}
@@ -158,6 +173,14 @@ fn native_sandbox_cwd(cwd: &PathUri) -> Result<AbsolutePathBuf, JSONRPCErrorErro
.map_err(|err| invalid_request(err.to_string()))
}
fn native_workspace_root(root: &PathUri) -> Result<AbsolutePathBuf, JSONRPCErrorError> {
root.to_abs_path().map_err(|err| {
invalid_request(format!(
"file system sandbox workspace root is not native to this exec-server host: {err}"
))
})
}
fn helper_read_roots(runtime_paths: &ExecServerRuntimePaths) -> Vec<AbsolutePathBuf> {
let mut roots = Vec::new();
for path in std::iter::once(runtime_paths.codex_self_exe.as_path())
@@ -517,15 +540,21 @@ mod tests {
let runner = FileSystemSandboxRunner::new(runtime_paths);
let native_cwd = AbsolutePathBuf::current_dir().expect("cwd");
let cwd = PathUri::from_abs_path(&native_cwd);
let file_system_policy =
restricted_policy(vec![path_entry(native_cwd, FileSystemAccessMode::Write)]);
let file_system_policy = restricted_policy(vec![path_entry(
native_cwd.clone(),
FileSystemAccessMode::Write,
)]);
let network_policy = NetworkSandboxPolicy::Restricted;
let permission_profile =
PermissionProfile::from_runtime_permissions(&file_system_policy, network_policy);
let sandbox_context = sandbox_context_with_cwd(&file_system_policy, cwd.clone());
let sandbox_cwd = SandboxCwd {
uri: cwd,
native: native_cwd,
};
let request = runner
.sandbox_exec_request(&permission_profile, &cwd, &sandbox_context)
.sandbox_exec_request(&permission_profile, &sandbox_cwd, &sandbox_context)
.expect("sandbox exec request");
assert_eq!(request.env.get(&path_key), Some(&path));
+4
View File
@@ -19,6 +19,7 @@ pub(crate) mod exec_server;
pub(crate) const DELAYED_OUTPUT_AFTER_EXIT_PARENT_ARG: &str =
"--codex-test-delayed-output-after-exit-parent";
const CODEX_WINDOWS_SANDBOX_ARG1: &str = "--run-as-windows-sandbox";
const DELAYED_OUTPUT_AFTER_EXIT_CHILD_ARG: &str = "--codex-test-delayed-output-after-exit-child";
#[ctor]
@@ -27,6 +28,9 @@ pub static TEST_BINARY_DISPATCH_GUARD: Option<TestBinaryDispatchGuard> = {
if argv1 == Some(CODEX_FS_HELPER_ARG1) {
return TestBinaryDispatchMode::DispatchArg0Only;
}
if argv1 == Some(CODEX_WINDOWS_SANDBOX_ARG1) {
return TestBinaryDispatchMode::DispatchArg0Only;
}
if exe_name == CODEX_LINUX_SANDBOX_ARG0 {
return TestBinaryDispatchMode::DispatchArg0Only;
}
@@ -12,9 +12,14 @@ use std::path::Path;
use std::process::Command;
use anyhow::Result;
use codex_exec_server::FileSystemSandboxContext;
use codex_protocol::config_types::WindowsSandboxLevel;
use codex_protocol::protocol::SandboxPolicy;
use codex_utils_path_uri::PathUri;
use test_case::test_case;
use crate::support::FileSystemImplementation;
use crate::support::create_file_system_context;
fn create_directory_junction(target: &Path, alias: &Path) -> Result<()> {
let output = Command::new("cmd")
@@ -54,3 +59,58 @@ async fn file_system_sandboxed_canonicalize_resolves_directory_junction(
)
.await
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn file_system_remote_fs_helper_respects_windows_sandbox_write_policy() -> Result<()> {
let context = create_file_system_context(FileSystemImplementation::Remote).await?;
let file_system = context.file_system;
let tmp = tempfile::TempDir::new()?;
let readonly_dir = tmp.path().join("readonly");
std::fs::create_dir_all(&readonly_dir)?;
let mut sandbox = read_only_sandbox_for_cwd(readonly_dir.clone())?;
sandbox.windows_sandbox_level = WindowsSandboxLevel::RestrictedToken;
let readable_file = readonly_dir.join("readable.txt");
std::fs::write(&readable_file, b"readable")?;
let read_result = file_system
.read_file(&PathUri::from_path(&readable_file)?, Some(&sandbox))
.await;
// Some local Windows hosts cannot create restricted tokens. Reaching that
// error still proves the remote fs helper went through the Windows sandbox
// launcher; before the wrapper fix this read would have run unsandboxed.
if is_unsupported_restricted_token_host(&read_result) {
return Ok(());
}
assert_eq!(read_result?, b"readable");
let blocked_file = readonly_dir.join("blocked.txt");
let error = file_system
.write_file(
&PathUri::from_path(&blocked_file)?,
b"blocked".to_vec(),
Some(&sandbox),
)
.await
.expect_err("write outside the sandbox should fail");
assert!(
!blocked_file.exists(),
"sandboxed fs helper must not create blocked file after error: {error}"
);
Ok(())
}
fn read_only_sandbox_for_cwd(cwd: std::path::PathBuf) -> Result<FileSystemSandboxContext> {
Ok(FileSystemSandboxContext::from_legacy_sandbox_policy(
SandboxPolicy::new_read_only_policy(),
PathUri::from_path(cwd)?,
)?)
}
fn is_unsupported_restricted_token_host<T>(result: &std::io::Result<T>) -> bool {
result.as_ref().err().is_some_and(|err| {
err.to_string()
.contains("windows sandbox failed: CreateRestrictedToken failed: 87")
})
}
+4
View File
@@ -62,6 +62,8 @@ pub struct FileSystemSandboxContext {
pub permissions: PermissionProfile<PathUri>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cwd: Option<PathUri>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub workspace_roots: Vec<PathUri>,
pub windows_sandbox_level: WindowsSandboxLevel,
#[serde(default)]
pub windows_sandbox_private_desktop: bool,
@@ -109,6 +111,7 @@ impl FileSystemSandboxContext {
Self {
permissions: permissions.into(),
cwd,
workspace_roots: Vec::new(),
windows_sandbox_level: WindowsSandboxLevel::Disabled,
windows_sandbox_private_desktop: false,
use_legacy_landlock: false,
@@ -151,6 +154,7 @@ impl FileSystemSandboxContext {
pub fn drop_cwd_if_unused(mut self) -> Self {
if !self.has_cwd_dependent_permissions() {
self.cwd = None;
self.workspace_roots.clear();
}
self
}
+4
View File
@@ -17,6 +17,7 @@ codex-network-proxy = { workspace = true }
codex-protocol = { workspace = true }
codex-utils-absolute-path = { workspace = true }
codex-utils-path-uri = { workspace = true }
codex-windows-sandbox = { workspace = true }
dunce = { workspace = true }
libc = { workspace = true }
serde_json = { workspace = true }
@@ -25,6 +26,9 @@ tracing = { workspace = true, features = ["log"] }
url = { workspace = true }
which = { workspace = true }
[target.'cfg(windows)'.dependencies]
codex-utils-home-dir = { workspace = true }
[dev-dependencies]
anyhow = { workspace = true }
pretty_assertions = { workspace = true }
+12
View File
@@ -5,12 +5,14 @@ mod manager;
pub mod policy_transforms;
#[cfg(target_os = "macos")]
pub mod seatbelt;
mod windows;
#[cfg(target_os = "linux")]
pub use bwrap::find_system_bwrap_in_path;
#[cfg(target_os = "linux")]
pub use bwrap::system_bwrap_warning;
pub use manager::SandboxCommand;
pub use manager::SandboxDirectSpawnTransformRequest;
pub use manager::SandboxExecRequest;
pub use manager::SandboxManager;
pub use manager::SandboxTransformError;
@@ -20,6 +22,12 @@ pub use manager::SandboxablePreference;
pub use manager::compatibility_sandbox_policy_for_permission_profile;
pub use manager::get_platform_sandbox;
pub use manager::with_managed_mitm_ca_readable_root;
pub use windows::WindowsSandboxFilesystemOverrides;
pub use windows::permission_profile_supports_windows_restricted_token_sandbox;
pub use windows::resolve_windows_elevated_filesystem_overrides;
pub use windows::resolve_windows_restricted_token_filesystem_overrides;
pub use windows::unsupported_windows_restricted_token_sandbox_reason;
pub use windows::windows_sandbox_uses_elevated_backend;
use codex_protocol::error::CodexErr;
@@ -48,6 +56,10 @@ impl From<SandboxTransformError> for CodexErr {
SandboxTransformError::SeatbeltUnavailable => CodexErr::UnsupportedOperation(
"seatbelt sandbox is only available on macOS".to_string(),
),
#[cfg(target_os = "windows")]
SandboxTransformError::WindowsSandboxPreparation(message) => {
CodexErr::UnsupportedOperation(message)
}
}
}
}
+163
View File
@@ -7,6 +7,12 @@ use crate::landlock::allow_network_for_proxy;
use crate::landlock::create_linux_sandbox_command_args_for_permission_profile;
use crate::policy_transforms::effective_permission_profile;
use crate::policy_transforms::should_require_platform_sandbox;
#[cfg(target_os = "windows")]
use crate::resolve_windows_elevated_filesystem_overrides;
#[cfg(target_os = "windows")]
use crate::resolve_windows_restricted_token_filesystem_overrides;
#[cfg(target_os = "windows")]
use crate::windows_sandbox_uses_elevated_backend;
use codex_network_proxy::NetworkProxy;
use codex_protocol::config_types::WindowsSandboxLevel;
use codex_protocol::models::AdditionalPermissionProfile;
@@ -21,6 +27,9 @@ use std::ffi::OsString;
use std::io;
use std::path::Path;
#[cfg(target_os = "windows")]
const WINDOWS_SANDBOX_WRAPPER_SETUP_ENV_ALLOWLIST: &[&str] = &["USERNAME", "USERPROFILE"];
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SandboxType {
None,
@@ -131,6 +140,16 @@ pub struct SandboxTransformRequest<'a> {
pub windows_sandbox_private_desktop: bool,
}
/// Bundled arguments for a sandbox transformation whose result will be spawned
/// directly from argv.
///
/// Direct-spawn callers will not run a later platform-specific launcher, so the
/// returned command must encode any sandbox wrapper it needs.
pub struct SandboxDirectSpawnTransformRequest<'a> {
pub transform: SandboxTransformRequest<'a>,
pub workspace_roots: &'a [AbsolutePathBuf],
}
#[derive(Debug)]
pub enum SandboxTransformError {
InvalidCommandCwd {
@@ -146,6 +165,8 @@ pub enum SandboxTransformError {
Wsl1UnsupportedForBubblewrap,
#[cfg(not(target_os = "macos"))]
SeatbeltUnavailable,
#[cfg(target_os = "windows")]
WindowsSandboxPreparation(String),
}
impl std::fmt::Display for SandboxTransformError {
@@ -168,6 +189,10 @@ impl std::fmt::Display for SandboxTransformError {
Self::Wsl1UnsupportedForBubblewrap => write!(f, "{WSL1_BWRAP_WARNING}"),
#[cfg(not(target_os = "macos"))]
Self::SeatbeltUnavailable => write!(f, "seatbelt sandbox is only available on macOS"),
#[cfg(target_os = "windows")]
Self::WindowsSandboxPreparation(err) => {
write!(f, "failed to prepare windows sandbox wrapper: {err}")
}
}
}
}
@@ -182,6 +207,8 @@ impl std::error::Error for SandboxTransformError {
Self::Wsl1UnsupportedForBubblewrap => None,
#[cfg(not(target_os = "macos"))]
Self::SeatbeltUnavailable => None,
#[cfg(target_os = "windows")]
Self::WindowsSandboxPreparation(_) => None,
}
}
}
@@ -336,6 +363,142 @@ impl SandboxManager {
arg0: arg0_override,
})
}
pub fn transform_for_direct_spawn(
&self,
request: SandboxDirectSpawnTransformRequest<'_>,
) -> Result<SandboxExecRequest, SandboxTransformError> {
#[cfg(target_os = "windows")]
{
let codex_home = codex_utils_home_dir::find_codex_home()
.map_err(|err| SandboxTransformError::WindowsSandboxPreparation(err.to_string()))?;
self.transform_for_direct_spawn_with_codex_home(request, codex_home.as_path())
}
#[cfg(not(target_os = "windows"))]
{
self.transform(request.transform)
}
}
#[cfg(target_os = "windows")]
fn transform_for_direct_spawn_with_codex_home(
&self,
request: SandboxDirectSpawnTransformRequest<'_>,
codex_home: &Path,
) -> Result<SandboxExecRequest, SandboxTransformError> {
let workspace_roots = request.workspace_roots;
let mut request = self.transform(request.transform)?;
if request.sandbox == SandboxType::WindowsRestrictedToken {
wrap_windows_sandbox_exec_request_for_direct_spawn(
&mut request,
workspace_roots,
codex_home,
)?;
}
Ok(request)
}
}
#[cfg(target_os = "windows")]
fn wrap_windows_sandbox_exec_request_for_direct_spawn(
request: &mut SandboxExecRequest,
workspace_roots: &[AbsolutePathBuf],
codex_home: &Path,
) -> Result<(), SandboxTransformError> {
let Some(program) = request.command.first_mut() else {
return Err(SandboxTransformError::WindowsSandboxPreparation(
"sandbox command was empty".to_string(),
));
};
let source = std::path::PathBuf::from(&program);
let helper = codex_windows_sandbox::resolve_exe_for_launch(source.as_path(), codex_home);
*program = helper.to_string_lossy().into_owned();
let inner_command = std::mem::take(&mut request.command);
let proxy_enforced = request.network.is_some();
let use_elevated =
windows_sandbox_uses_elevated_backend(request.windows_sandbox_level, proxy_enforced);
let overrides = if use_elevated {
resolve_windows_elevated_filesystem_overrides(
request.sandbox,
&request.permission_profile,
&request.sandbox_policy_cwd,
use_elevated,
)
} else {
resolve_windows_restricted_token_filesystem_overrides(
request.sandbox,
&request.permission_profile,
&request.sandbox_policy_cwd,
request.windows_sandbox_level,
)
}
.map_err(SandboxTransformError::WindowsSandboxPreparation)?;
let empty_paths: &[AbsolutePathBuf] = &[];
let read_roots_override = overrides
.as_ref()
.and_then(|overrides| overrides.read_roots_override.as_deref());
let read_roots_include_platform_defaults = overrides
.as_ref()
.is_some_and(|overrides| overrides.read_roots_include_platform_defaults);
let write_roots_override = overrides
.as_ref()
.and_then(|overrides| overrides.write_roots_override.as_deref());
let deny_read_paths_override = overrides.as_ref().map_or(empty_paths, |overrides| {
overrides.additional_deny_read_paths.as_slice()
});
let deny_write_paths_override = overrides.as_ref().map_or(empty_paths, |overrides| {
overrides.additional_deny_write_paths.as_slice()
});
let mut wrapper_args =
codex_windows_sandbox::create_windows_sandbox_command_args_for_permission_profile(
inner_command,
&request.cwd,
workspace_roots,
&request.env,
&request.permission_profile,
request.windows_sandbox_level,
request.windows_sandbox_private_desktop,
proxy_enforced,
read_roots_override,
read_roots_include_platform_defaults,
write_roots_override,
deny_read_paths_override,
deny_write_paths_override,
codex_home,
);
request.command = Vec::with_capacity(1 + wrapper_args.len());
request.command.push(source.to_string_lossy().into_owned());
request.command.append(&mut wrapper_args);
request.sandbox = SandboxType::None;
request.arg0 = None;
add_windows_sandbox_wrapper_setup_env(&mut request.env);
Ok(())
}
#[cfg(target_os = "windows")]
fn add_windows_sandbox_wrapper_setup_env(env: &mut HashMap<String, String>) {
add_windows_sandbox_wrapper_setup_env_from_vars(env, std::env::vars_os());
}
#[cfg(target_os = "windows")]
fn add_windows_sandbox_wrapper_setup_env_from_vars(
env: &mut HashMap<String, String>,
vars: impl IntoIterator<Item = (std::ffi::OsString, std::ffi::OsString)>,
) {
for (key, value) in vars {
let key = key.to_string_lossy().into_owned();
if !WINDOWS_SANDBOX_WRAPPER_SETUP_ENV_ALLOWLIST
.iter()
.any(|allowed| key.eq_ignore_ascii_case(allowed))
{
continue;
}
env.retain(|existing, _| !existing.eq_ignore_ascii_case(&key));
env.insert(key, value.to_string_lossy().into_owned());
}
}
pub fn compatibility_sandbox_policy_for_permission_profile(
+152
View File
@@ -1,4 +1,6 @@
use super::SandboxCommand;
#[cfg(target_os = "windows")]
use super::SandboxDirectSpawnTransformRequest;
use super::SandboxManager;
use super::SandboxTransformRequest;
use super::SandboxType;
@@ -410,3 +412,153 @@ fn transform_linux_seccomp_uses_helper_alias_when_launcher_is_not_helper_path()
assert_eq!(exec_request.arg0, Some("codex-linux-sandbox".to_string()));
}
#[cfg(target_os = "windows")]
#[test]
fn transform_for_direct_spawn_windows_preserves_only_wrapper_setup_identity() {
let mut env = HashMap::from([
("Path".to_string(), r"C:\Windows\System32".to_string()),
("username".to_string(), "wrong-user".to_string()),
("UserProfile".to_string(), r"C:\wrong".to_string()),
]);
super::add_windows_sandbox_wrapper_setup_env_from_vars(
&mut env,
[
("USERNAME", "alice"),
("USERPROFILE", r"C:\Users\alice"),
("OPENAI_API_KEY", "secret"),
]
.map(|(key, value)| {
(
std::ffi::OsString::from(key),
std::ffi::OsString::from(value),
)
}),
);
assert_eq!(
env,
HashMap::from([
("Path".to_string(), r"C:\Windows\System32".to_string()),
("USERNAME".to_string(), "alice".to_string()),
("USERPROFILE".to_string(), r"C:\Users\alice".to_string()),
])
);
}
#[cfg(target_os = "windows")]
#[test]
fn transform_for_direct_spawn_windows_materializes_inner_helper() {
let codex_home = tempfile::TempDir::new().expect("codex home");
let helper_dir = tempfile::TempDir::new().expect("helper dir");
let configured_helper = helper_dir.path().join("configured-codex-helper.exe");
std::fs::write(&configured_helper, b"helper").expect("write configured helper");
let cwd = AbsolutePathBuf::from_absolute_path(helper_dir.path()).expect("absolute cwd");
let cwd_uri = PathUri::from_abs_path(&cwd);
let blocked = cwd.join("blocked");
std::fs::create_dir_all(blocked.as_path()).expect("create blocked path");
let permissions = PermissionProfile::from_runtime_permissions(
&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::Path { path: blocked },
access: FileSystemAccessMode::Deny,
},
]),
NetworkSandboxPolicy::Restricted,
);
let other_workspace = tempfile::TempDir::new().expect("other workspace");
let other_workspace_root = AbsolutePathBuf::from_absolute_path(other_workspace.path())
.expect("absolute other workspace");
let workspace_roots = vec![cwd, other_workspace_root];
let manager = SandboxManager::new();
let exec_request = manager
.transform_for_direct_spawn_with_codex_home(
SandboxDirectSpawnTransformRequest {
workspace_roots: workspace_roots.as_slice(),
transform: SandboxTransformRequest {
command: SandboxCommand {
program: configured_helper.as_os_str().to_owned(),
args: vec!["--codex-run-as-fs-helper".to_string()],
cwd: cwd_uri.clone(),
env: HashMap::from([(
"Path".to_string(),
r"C:\Windows\System32".to_string(),
)]),
additional_permissions: None,
},
permissions: &permissions,
sandbox: SandboxType::WindowsRestrictedToken,
enforce_managed_network: false,
network: None,
sandbox_policy_cwd: &cwd_uri,
codex_linux_sandbox_exe: None,
use_legacy_landlock: false,
windows_sandbox_level: WindowsSandboxLevel::Elevated,
windows_sandbox_private_desktop: false,
},
},
codex_home.path(),
)
.expect("transform for direct spawn");
let separator_index = exec_request
.command
.iter()
.position(|arg| arg == "--")
.expect("wrapper argv separator");
let materialized_helper = std::path::PathBuf::from(&exec_request.command[separator_index + 1]);
assert_eq!(exec_request.sandbox, SandboxType::None);
assert_eq!(
exec_request.command.first(),
Some(&configured_helper.display().to_string())
);
assert!(
exec_request
.command
.iter()
.any(|arg| arg == "--run-as-windows-sandbox")
);
assert!(
exec_request
.command
.iter()
.any(|arg| arg == "--deny-read-paths-json")
);
assert_eq!(
exec_request.command[separator_index + 2],
"--codex-run-as-fs-helper"
);
assert_eq!(
exec_request
.command
.windows(2)
.filter_map(|args| {
(args[0] == "--workspace-root").then_some(std::path::PathBuf::from(&args[1]))
})
.collect::<Vec<_>>(),
workspace_roots
.iter()
.map(|root| root.as_path().to_path_buf())
.collect::<Vec<_>>()
);
assert_eq!(
materialized_helper
.parent()
.and_then(std::path::Path::file_name),
Some(std::ffi::OsStr::new(".sandbox-bin"))
);
assert!(materialized_helper.exists());
}
+382
View File
@@ -0,0 +1,382 @@
use std::collections::BTreeSet;
use std::path::Path;
use std::path::PathBuf;
use codex_protocol::config_types::WindowsSandboxLevel;
use codex_protocol::models::PermissionProfile;
use codex_protocol::permissions::FileSystemSandboxPolicy;
use codex_protocol::protocol::WritableRoot;
use codex_utils_absolute_path::AbsolutePathBuf;
use crate::SandboxType;
use crate::compatibility_sandbox_policy_for_permission_profile;
/// 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 struct WindowsSandboxFilesystemOverrides {
pub read_roots_override: Option<Vec<PathBuf>>,
pub read_roots_include_platform_defaults: bool,
pub write_roots_override: Option<Vec<PathBuf>>,
pub additional_deny_read_paths: Vec<AbsolutePathBuf>,
pub additional_deny_write_paths: Vec<AbsolutePathBuf>,
}
pub 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)
}
pub 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,
}
}
pub 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 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<_, _>>()?,
}))
}
pub 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(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 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())
}
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: &[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())
})
})
})
}
@@ -96,22 +96,26 @@ pub fn resolve_current_exe_for_launch(codex_home: &Path, fallback_executable: &s
Ok(path) => path,
Err(_) => return PathBuf::from(fallback_executable),
};
resolve_exe_for_launch(&source, codex_home)
}
pub fn resolve_exe_for_launch(source: &Path, codex_home: &Path) -> PathBuf {
let Some(file_name) = source.file_name() else {
return source;
return source.to_path_buf();
};
let destination = helper_bin_dir(codex_home).join(file_name);
match copy_from_source_if_needed(&source, &destination) {
match copy_from_source_if_needed(source, &destination) {
Ok(_) => destination,
Err(err) => {
let sandbox_log_dir = crate::sandbox_dir(codex_home);
log_note(
&format!(
"helper copy failed for current executable: {err:#}; falling back to legacy path {}",
"helper copy failed for executable: {err:#}; falling back to legacy path {}",
source.display()
),
Some(&sandbox_log_dir),
);
source
source.to_path_buf()
}
}
}
+2
View File
@@ -174,6 +174,8 @@ pub use elevated_impl::run_windows_sandbox_capture_for_permission_profile as run
#[cfg(target_os = "windows")]
pub use helper_materialization::resolve_current_exe_for_launch;
#[cfg(target_os = "windows")]
pub use helper_materialization::resolve_exe_for_launch;
#[cfg(target_os = "windows")]
pub use hide_users::hide_current_user_profile_dir;
#[cfg(target_os = "windows")]
pub use hide_users::hide_newly_created_users;