mirror of
https://github.com/openai/codex.git
synced 2026-11-05 20:10:33 +00:00
Move Windows sandbox filesystem override resolution
This commit is contained in:
Generated
+1
@@ -4281,6 +4281,7 @@ dependencies = [
|
||||
"chrono",
|
||||
"codex-otel",
|
||||
"codex-protocol",
|
||||
"codex-sandboxing",
|
||||
"codex-utils-absolute-path",
|
||||
"codex-utils-pty",
|
||||
"codex-utils-string",
|
||||
|
||||
+38
-349
@@ -1,7 +1,6 @@
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::process::ExitStatusExt;
|
||||
|
||||
use std::collections::BTreeSet;
|
||||
use std::collections::HashMap;
|
||||
use std::io;
|
||||
use std::path::Path;
|
||||
@@ -42,7 +41,6 @@ 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_utils_absolute_path::AbsolutePathBuf;
|
||||
use codex_utils_pty::DEFAULT_OUTPUT_BYTES_CAP;
|
||||
use codex_utils_pty::process_group::kill_child_process_group;
|
||||
@@ -95,23 +93,8 @@ 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>,
|
||||
}
|
||||
pub(crate) type WindowsSandboxFilesystemOverrides =
|
||||
codex_windows_sandbox::WindowsSandboxFilesystemOverrides;
|
||||
|
||||
fn windows_sandbox_uses_elevated_backend(
|
||||
sandbox_level: WindowsSandboxLevel,
|
||||
@@ -997,16 +980,41 @@ async fn exec(
|
||||
consume_output(child, expiration, capture_policy, stdout_stream).await
|
||||
}
|
||||
|
||||
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
|
||||
#[cfg_attr(not(test), 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,
|
||||
}
|
||||
codex_windows_sandbox::permission_profile_supports_windows_restricted_token_sandbox(
|
||||
permission_profile,
|
||||
)
|
||||
}
|
||||
|
||||
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> {
|
||||
codex_windows_sandbox::resolve_windows_restricted_token_filesystem_overrides(
|
||||
sandbox == SandboxType::WindowsRestrictedToken,
|
||||
permission_profile,
|
||||
sandbox_policy_cwd,
|
||||
windows_sandbox_level,
|
||||
)
|
||||
}
|
||||
|
||||
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> {
|
||||
codex_windows_sandbox::resolve_windows_elevated_filesystem_overrides(
|
||||
sandbox == SandboxType::WindowsRestrictedToken,
|
||||
permission_profile,
|
||||
sandbox_policy_cwd,
|
||||
use_windows_elevated_backend,
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg_attr(not(test), allow(dead_code))]
|
||||
@@ -1016,331 +1024,12 @@ pub(crate) fn unsupported_windows_restricted_token_sandbox_reason(
|
||||
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(
|
||||
codex_windows_sandbox::unsupported_windows_restricted_token_sandbox_reason(
|
||||
sandbox == SandboxType::WindowsRestrictedToken,
|
||||
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())
|
||||
})
|
||||
})
|
||||
})
|
||||
windows_sandbox_level,
|
||||
)
|
||||
}
|
||||
|
||||
/// Consumes the output of a child process according to the configured capture
|
||||
|
||||
@@ -31,6 +31,7 @@ chrono = { version = "0.4.42", default-features = false, features = [
|
||||
codex-utils-pty = { workspace = true }
|
||||
codex-utils-absolute-path = { workspace = true }
|
||||
codex-utils-string = { workspace = true }
|
||||
codex-sandboxing = { workspace = true }
|
||||
codex-otel = { workspace = true }
|
||||
dunce = "1.0"
|
||||
glob = { workspace = true }
|
||||
|
||||
@@ -0,0 +1,364 @@
|
||||
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_utils_absolute_path::AbsolutePathBuf;
|
||||
|
||||
/// 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, Default, 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 unsupported_windows_restricted_token_sandbox_reason(
|
||||
windows_sandbox_active: bool,
|
||||
permission_profile: &PermissionProfile,
|
||||
sandbox_policy_cwd: &AbsolutePathBuf,
|
||||
windows_sandbox_level: WindowsSandboxLevel,
|
||||
) -> Option<String> {
|
||||
if windows_sandbox_level == WindowsSandboxLevel::Elevated {
|
||||
resolve_windows_elevated_filesystem_overrides(
|
||||
windows_sandbox_active,
|
||||
permission_profile,
|
||||
sandbox_policy_cwd,
|
||||
windows_sandbox_level == WindowsSandboxLevel::Elevated,
|
||||
)
|
||||
.err()
|
||||
} else {
|
||||
resolve_windows_restricted_token_filesystem_overrides(
|
||||
windows_sandbox_active,
|
||||
permission_profile,
|
||||
sandbox_policy_cwd,
|
||||
windows_sandbox_level,
|
||||
)
|
||||
.err()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resolve_windows_restricted_token_filesystem_overrides(
|
||||
windows_sandbox_active: bool,
|
||||
permission_profile: &PermissionProfile,
|
||||
sandbox_policy_cwd: &AbsolutePathBuf,
|
||||
windows_sandbox_level: WindowsSandboxLevel,
|
||||
) -> std::result::Result<Option<WindowsSandboxFilesystemOverrides>, String> {
|
||||
if !windows_sandbox_active || 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 =
|
||||
crate::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 = codex_sandboxing::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(
|
||||
windows_sandbox_active: bool,
|
||||
permission_profile: &PermissionProfile,
|
||||
sandbox_policy_cwd: &AbsolutePathBuf,
|
||||
use_windows_elevated_backend: bool,
|
||||
) -> std::result::Result<Option<WindowsSandboxFilesystemOverrides>, String> {
|
||||
if !windows_sandbox_active || !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 =
|
||||
crate::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 = codex_sandboxing::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,
|
||||
}))
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
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: &[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())
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -53,6 +53,7 @@ mod desktop;
|
||||
mod dpapi;
|
||||
#[cfg(target_os = "windows")]
|
||||
mod env;
|
||||
mod filesystem_overrides;
|
||||
#[cfg(target_os = "windows")]
|
||||
mod helper_materialization;
|
||||
#[cfg(target_os = "windows")]
|
||||
@@ -169,6 +170,11 @@ pub use dpapi::unprotect as dpapi_unprotect;
|
||||
pub use elevated_impl::ElevatedSandboxProfileCaptureRequest;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use elevated_impl::run_windows_sandbox_capture_for_permission_profile as run_windows_sandbox_capture_for_permission_profile_elevated;
|
||||
pub use filesystem_overrides::WindowsSandboxFilesystemOverrides;
|
||||
pub use filesystem_overrides::permission_profile_supports_windows_restricted_token_sandbox;
|
||||
pub use filesystem_overrides::resolve_windows_elevated_filesystem_overrides;
|
||||
pub use filesystem_overrides::resolve_windows_restricted_token_filesystem_overrides;
|
||||
pub use filesystem_overrides::unsupported_windows_restricted_token_sandbox_reason;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use helper_materialization::resolve_current_exe_for_launch;
|
||||
#[cfg(target_os = "windows")]
|
||||
|
||||
Reference in New Issue
Block a user