Wire managed child MITM CA environments

This commit is contained in:
Winston Howes
2026-06-18 12:16:19 -07:00
parent c27c1f8faf
commit ac77ede42d
5 changed files with 724 additions and 70 deletions

View File

@@ -17,13 +17,15 @@ use codex_core::spawn::CODEX_SANDBOX_ENV_VAR;
use codex_core::spawn::CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR;
use codex_protocol::config_types::SandboxMode;
use codex_protocol::permissions::NetworkSandboxPolicy;
#[cfg(target_os = "linux")]
use codex_sandboxing::ensure_legacy_landlock_supports_managed_mitm;
use codex_sandboxing::landlock::allow_network_for_proxy;
use codex_sandboxing::landlock::create_linux_sandbox_command_args_for_permission_profile;
use codex_sandboxing::prepare_managed_network_child;
#[cfg(target_os = "macos")]
use codex_sandboxing::seatbelt::CreateSeatbeltCommandArgsParams;
#[cfg(target_os = "macos")]
use codex_sandboxing::seatbelt::create_seatbelt_command_args;
use codex_sandboxing::with_managed_mitm_ca_readable_root;
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_cli::CliConfigOverrides;
use tokio::process::Child;
@@ -216,7 +218,7 @@ async fn run_command_under_sandbox(
#[cfg(target_os = "windows")]
let workspace_roots = config.effective_workspace_roots();
let env = create_env(
let mut env = create_env(
&config.permissions.shell_environment_policy,
/*thread_id*/ None,
);
@@ -261,15 +263,36 @@ async fn run_command_under_sandbox(
// Proxy containment depends on whether a proxy is active, not whether its
// policy came from managed requirements.
let enforce_managed_network = network.is_some();
let managed_mitm_ca_trust_bundle_path = match network.as_ref() {
Some(network) => network.managed_mitm_ca_trust_bundle_path(),
None => None,
};
let runtime_permission_profile = with_managed_mitm_ca_readable_root(
config.permissions.effective_permission_profile(),
managed_mitm_ca_trust_bundle_path.as_ref(),
#[expect(
clippy::redundant_closure_for_method_calls,
reason = "the concrete proxy type is not a direct dependency of codex-cli"
)]
let network_child_env = network.as_ref().map(|network| network.child_env_snapshot());
let effective_permission_profile = config.permissions.effective_permission_profile();
#[cfg(target_os = "linux")]
if matches!(&sandbox_type, SandboxType::Landlock) {
let file_system_sandbox_policy = effective_permission_profile.file_system_sandbox_policy();
#[expect(
clippy::redundant_closure_for_method_calls,
reason = "the concrete snapshot type is not a direct dependency of codex-cli"
)]
let managed_mitm_ca_active = network_child_env
.as_ref()
.is_some_and(|snapshot| snapshot.has_managed_mitm_ca());
ensure_legacy_landlock_supports_managed_mitm(
&file_system_sandbox_policy,
config.features.use_legacy_landlock(),
managed_mitm_ca_active,
)?;
}
let runtime_permission_profile = prepare_managed_network_child(
network_child_env.as_ref(),
&mut env,
cwd.as_path(),
effective_permission_profile,
sandbox_policy_cwd.as_path(),
);
cfg!(target_os = "windows") && matches!(sandbox_type, SandboxType::Windows),
)?;
let mut child = match sandbox_type {
#[cfg(target_os = "macos")]
@@ -294,9 +317,6 @@ async fn run_command_under_sandbox(
env,
|env_map| {
env_map.insert(CODEX_SANDBOX_ENV_VAR.to_string(), "seatbelt".to_string());
if let Some(network) = network.as_ref() {
network.apply_to_env(env_map);
}
},
)
.await?
@@ -323,11 +343,7 @@ async fn run_command_under_sandbox(
cwd.to_path_buf(),
network_sandbox_policy,
env,
|env_map| {
if let Some(network) = network.as_ref() {
network.apply_to_env(env_map);
}
},
|_env_map| {},
)
.await?
}

View File

@@ -1,6 +1,7 @@
#[cfg(target_os = "linux")]
mod bwrap;
pub mod landlock;
mod managed_network;
mod manager;
pub mod policy_transforms;
#[cfg(target_os = "macos")]
@@ -11,6 +12,7 @@ mod windows;
pub use bwrap::find_system_bwrap_in_path;
#[cfg(target_os = "linux")]
pub use bwrap::system_bwrap_warning;
pub use managed_network::prepare_managed_network_child;
pub use manager::SandboxCommand;
pub use manager::SandboxDirectSpawnTransformRequest;
pub use manager::SandboxExecRequest;
@@ -20,8 +22,9 @@ pub use manager::SandboxTransformRequest;
pub use manager::SandboxType;
pub use manager::SandboxablePreference;
pub use manager::compatibility_sandbox_policy_for_permission_profile;
#[cfg(target_os = "linux")]
pub use manager::ensure_legacy_landlock_supports_managed_mitm;
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;
@@ -48,6 +51,25 @@ impl From<SandboxTransformError> for CodexErr {
SandboxTransformError::MissingLinuxSandboxExecutable => {
CodexErr::LandlockSandboxExecutableNotProvided
}
SandboxTransformError::ManagedMitmCaPathUnderWritableRoot => {
CodexErr::UnsupportedOperation(
"managed MITM CA isolation requires its proxy directory to be outside sandbox-writable roots"
.to_string(),
)
}
SandboxTransformError::ManagedMitmCustomCaUnsupportedOnWindows => {
CodexErr::UnsupportedOperation(
"CA directories and command-specific CA overrides with managed MITM are not supported in the Windows sandbox because its read grants persist across commands"
.to_string(),
)
}
#[cfg(target_os = "linux")]
SandboxTransformError::LegacyLandlockUnsupportedWithManagedMitm => {
CodexErr::UnsupportedOperation(
"managed MITM CA isolation requires bubblewrap and is incompatible with legacy Landlock"
.to_string(),
)
}
#[cfg(target_os = "linux")]
SandboxTransformError::Wsl1UnsupportedForBubblewrap => {
CodexErr::UnsupportedOperation(crate::bwrap::WSL1_BWRAP_WARNING.to_string())

View File

@@ -0,0 +1,205 @@
use crate::manager::SandboxTransformError;
use codex_network_proxy::NetworkProxyChildEnvSnapshot;
use codex_protocol::models::PermissionProfile;
use codex_protocol::permissions::FileSystemAccessMode;
use codex_protocol::permissions::FileSystemPath;
use codex_protocol::permissions::FileSystemSandboxEntry;
use codex_protocol::permissions::FileSystemSandboxKind;
use codex_protocol::permissions::FileSystemSandboxPolicy;
use codex_protocol::permissions::ReadDenyMatcher;
use codex_utils_absolute_path::AbsolutePathBuf;
use std::collections::HashMap;
use std::path::Path;
pub(crate) fn with_managed_mitm_ca_proxy_dirs_denied(
permission_profile: PermissionProfile,
managed_mitm_ca_trust_bundle_paths: &[AbsolutePathBuf],
sandbox_policy_cwd: &Path,
) -> Result<PermissionProfile, SandboxTransformError> {
if managed_mitm_ca_trust_bundle_paths.is_empty() {
return Ok(permission_profile);
}
let (mut file_system_sandbox_policy, network_sandbox_policy) =
permission_profile.to_runtime_permissions();
// Seatbelt and bubblewrap can apply the parent deny plus file carveback per
// invocation. Windows read grants persist; other profile kinds stay unchanged.
if !cfg!(any(target_os = "linux", target_os = "macos"))
|| file_system_sandbox_policy.kind != FileSystemSandboxKind::Restricted
{
return Ok(permission_profile);
}
// Hide the entire proxy directory before custom CA materialization. This
// prevents a later command from asking the unsandboxed host to copy an
// earlier command's generated bundle (or the MITM private key) into its
// own active bundle.
let mut managed_mitm_ca_dirs = managed_mitm_ca_trust_bundle_paths
.iter()
.filter_map(|path| path.as_path().parent())
.filter_map(|path| AbsolutePathBuf::from_absolute_path(path).ok())
.collect::<Vec<_>>();
managed_mitm_ca_dirs.sort();
managed_mitm_ca_dirs.dedup();
if managed_mitm_ca_dirs.iter().any(|path| {
managed_mitm_ca_dir_overlaps_writable_path(
&file_system_sandbox_policy,
path.as_path(),
sandbox_policy_cwd,
)
}) {
return Err(SandboxTransformError::ManagedMitmCaPathUnderWritableRoot);
}
for path in managed_mitm_ca_dirs {
let entry = FileSystemSandboxEntry {
path: FileSystemPath::Path { path },
access: FileSystemAccessMode::Deny,
};
if !file_system_sandbox_policy.entries.contains(&entry) {
file_system_sandbox_policy.entries.push(entry);
}
}
Ok(
PermissionProfile::from_runtime_permissions_with_enforcement(
permission_profile.enforcement(),
&file_system_sandbox_policy,
network_sandbox_policy,
),
)
}
fn managed_mitm_ca_dir_overlaps_writable_path(
file_system_sandbox_policy: &FileSystemSandboxPolicy,
managed_mitm_ca_dir: &Path,
sandbox_policy_cwd: &Path,
) -> bool {
if file_system_sandbox_policy.has_full_disk_write_access() {
return true;
}
let managed_mitm_ca_dir_canonical = managed_mitm_ca_dir.canonicalize().ok();
let has_explicit_writable_overlap = file_system_sandbox_policy
.get_writable_roots_with_cwd(sandbox_policy_cwd)
.into_iter()
.any(|root| {
let root = root.root;
managed_mitm_ca_dir.starts_with(root.as_path())
|| root.as_path().starts_with(managed_mitm_ca_dir)
|| managed_mitm_ca_dir_canonical
.as_ref()
.is_some_and(|managed_dir| {
root.as_path().canonicalize().is_ok_and(|root| {
managed_dir.starts_with(&root) || root.starts_with(managed_dir)
})
})
});
if has_explicit_writable_overlap {
return true;
}
#[cfg(target_os = "macos")]
if file_system_sandbox_policy.include_platform_defaults() {
return ["/tmp", "/private/tmp", "/var/tmp", "/private/var/tmp"]
.into_iter()
.map(Path::new)
.any(|root| {
managed_mitm_ca_dir.starts_with(root)
|| managed_mitm_ca_dir_canonical
.as_ref()
.is_some_and(|managed_dir| {
root.canonicalize()
.is_ok_and(|root| managed_dir.starts_with(root))
})
});
}
false
}
pub(crate) fn with_managed_mitm_ca_readable_roots(
permission_profile: PermissionProfile,
managed_mitm_ca_trust_bundle_paths: &[AbsolutePathBuf],
sandbox_policy_cwd: &Path,
) -> PermissionProfile {
let (file_system_sandbox_policy, network_sandbox_policy) =
permission_profile.to_runtime_permissions();
let file_system_sandbox_policy = file_system_sandbox_policy
.with_additional_readable_roots(sandbox_policy_cwd, managed_mitm_ca_trust_bundle_paths);
PermissionProfile::from_runtime_permissions_with_enforcement(
permission_profile.enforcement(),
&file_system_sandbox_policy,
network_sandbox_policy,
)
}
pub fn prepare_managed_network_child(
network: Option<&NetworkProxyChildEnvSnapshot>,
env: &mut HashMap<String, String>,
command_cwd: &Path,
permission_profile: PermissionProfile,
sandbox_policy_cwd: &Path,
persistent_windows_sandbox: bool,
) -> Result<PermissionProfile, SandboxTransformError> {
let managed_mitm_ca_trust_bundle_paths = network
.and_then(NetworkProxyChildEnvSnapshot::managed_mitm_ca_trust_bundle_path)
.into_iter()
.collect::<Vec<_>>();
let permission_profile = with_managed_mitm_ca_proxy_dirs_denied(
permission_profile,
&managed_mitm_ca_trust_bundle_paths,
sandbox_policy_cwd,
)?;
if persistent_windows_sandbox
&& network.is_some_and(|network| network.requires_child_specific_mitm_ca_bundle(env))
{
return Err(SandboxTransformError::ManagedMitmCustomCaUnsupportedOnWindows);
}
let active_mitm_ca_trust_bundle_paths = network.map_or_else(Vec::new, |network| {
if persistent_windows_sandbox {
return network.prepare_persistent_sandbox_child_env(env);
}
let file_system_sandbox_policy = permission_profile.file_system_sandbox_policy();
let read_deny_glob_matcher =
read_deny_glob_matcher(&file_system_sandbox_policy, sandbox_policy_cwd);
network.prepare_child_env(env, command_cwd, |path| {
can_read_path_with_policy(
&file_system_sandbox_policy,
read_deny_glob_matcher.as_ref(),
path,
sandbox_policy_cwd,
)
})
});
Ok(with_managed_mitm_ca_readable_roots(
permission_profile,
&active_mitm_ca_trust_bundle_paths,
sandbox_policy_cwd,
))
}
pub(crate) fn read_deny_glob_matcher(
file_system_sandbox_policy: &FileSystemSandboxPolicy,
cwd: &Path,
) -> Option<ReadDenyMatcher> {
// Exact deny roots participate in normal path-specificity resolution, so
// a narrower explicit read entry can validly reopen one CA file below a
// denied parent. Globs are enforced separately and must still fail closed.
let mut deny_glob_policy = file_system_sandbox_policy.clone();
deny_glob_policy.entries.retain(|entry| {
entry.access == FileSystemAccessMode::Deny
&& matches!(entry.path, FileSystemPath::GlobPattern { .. })
});
ReadDenyMatcher::new(&deny_glob_policy, cwd)
}
pub(crate) fn can_read_path_with_policy(
file_system_sandbox_policy: &FileSystemSandboxPolicy,
read_deny_glob_matcher: Option<&ReadDenyMatcher>,
path: &Path,
cwd: &Path,
) -> bool {
file_system_sandbox_policy.can_read_path_with_cwd(path, cwd)
&& path.canonicalize().is_ok_and(|canonical_path| {
file_system_sandbox_policy.can_read_path_with_cwd(&canonical_path, cwd)
})
&& !read_deny_glob_matcher.is_some_and(|matcher| matcher.is_read_denied(path))
}

View File

@@ -5,6 +5,15 @@ use crate::bwrap::is_wsl1;
use crate::landlock::CODEX_LINUX_SANDBOX_ARG0;
use crate::landlock::allow_network_for_proxy;
use crate::landlock::create_linux_sandbox_command_args_for_permission_profile;
#[cfg(test)]
use crate::managed_network::can_read_path_with_policy;
use crate::managed_network::prepare_managed_network_child;
#[cfg(test)]
use crate::managed_network::read_deny_glob_matcher;
#[cfg(test)]
use crate::managed_network::with_managed_mitm_ca_proxy_dirs_denied;
#[cfg(all(test, any(target_os = "linux", target_os = "macos")))]
use crate::managed_network::with_managed_mitm_ca_readable_roots;
use crate::policy_transforms::effective_permission_profile;
use crate::policy_transforms::should_require_platform_sandbox;
#[cfg(target_os = "windows")]
@@ -14,9 +23,12 @@ 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_network_proxy::NetworkProxyChildEnvSnapshot;
use codex_protocol::config_types::WindowsSandboxLevel;
use codex_protocol::models::AdditionalPermissionProfile;
use codex_protocol::models::PermissionProfile;
#[cfg(target_os = "linux")]
use codex_protocol::permissions::FileSystemSandboxKind;
use codex_protocol::permissions::FileSystemSandboxPolicy;
use codex_protocol::permissions::NetworkSandboxPolicy;
use codex_protocol::protocol::SandboxPolicy;
@@ -72,27 +84,6 @@ pub fn get_platform_sandbox(windows_sandbox_enabled: bool) -> Option<SandboxType
}
}
pub fn with_managed_mitm_ca_readable_root(
permission_profile: PermissionProfile,
managed_mitm_ca_trust_bundle_path: Option<&AbsolutePathBuf>,
sandbox_policy_cwd: &Path,
) -> PermissionProfile {
let Some(managed_mitm_ca_trust_bundle_path) = managed_mitm_ca_trust_bundle_path else {
return permission_profile;
};
let (file_system_sandbox_policy, network_sandbox_policy) =
permission_profile.to_runtime_permissions();
let file_system_sandbox_policy = file_system_sandbox_policy.with_additional_readable_roots(
sandbox_policy_cwd,
std::slice::from_ref(managed_mitm_ca_trust_bundle_path),
);
PermissionProfile::from_runtime_permissions_with_enforcement(
permission_profile.enforcement(),
&file_system_sandbox_policy,
network_sandbox_policy,
)
}
#[derive(Debug)]
pub struct SandboxCommand {
pub program: OsString,
@@ -165,8 +156,10 @@ impl PendingSandboxedExecRequest {
fn new(
command_cwd: &PathUri,
sandbox_policy_cwd: &PathUri,
env: &mut HashMap<String, String>,
effective_permission_profile: PermissionProfile,
managed_mitm_ca_trust_bundle_path: Option<&AbsolutePathBuf>,
network: Option<&NetworkProxyChildEnvSnapshot>,
persistent_windows_sandbox: bool,
) -> Result<Self, SandboxTransformError> {
// TODO(anp): Move PathUri conversion into the platform sandbox implementations.
let native_command_cwd = command_cwd.to_abs_path().map_err(|source| {
@@ -181,11 +174,14 @@ impl PendingSandboxedExecRequest {
source,
}
})?;
let effective_permission_profile = with_managed_mitm_ca_readable_root(
let effective_permission_profile = prepare_managed_network_child(
network,
env,
native_command_cwd.as_path(),
effective_permission_profile,
managed_mitm_ca_trust_bundle_path,
native_sandbox_policy_cwd.as_path(),
);
persistent_windows_sandbox,
)?;
let (effective_file_system_policy, effective_network_policy) =
effective_permission_profile.to_runtime_permissions();
Ok(Self {
@@ -209,6 +205,10 @@ pub enum SandboxTransformError {
source: io::Error,
},
MissingLinuxSandboxExecutable,
ManagedMitmCaPathUnderWritableRoot,
ManagedMitmCustomCaUnsupportedOnWindows,
#[cfg(target_os = "linux")]
LegacyLandlockUnsupportedWithManagedMitm,
#[cfg(target_os = "linux")]
Wsl1UnsupportedForBubblewrap,
#[cfg(not(target_os = "macos"))]
@@ -233,6 +233,19 @@ impl std::fmt::Display for SandboxTransformError {
Self::MissingLinuxSandboxExecutable => {
write!(f, "missing codex-linux-sandbox executable path")
}
Self::ManagedMitmCaPathUnderWritableRoot => write!(
f,
"managed MITM CA isolation requires its proxy directory to be outside sandbox-writable roots"
),
Self::ManagedMitmCustomCaUnsupportedOnWindows => write!(
f,
"CA directories and command-specific CA overrides with managed MITM are not supported in the Windows sandbox because its read grants persist across commands"
),
#[cfg(target_os = "linux")]
Self::LegacyLandlockUnsupportedWithManagedMitm => write!(
f,
"managed MITM CA isolation requires bubblewrap and is incompatible with legacy Landlock"
),
#[cfg(target_os = "linux")]
Self::Wsl1UnsupportedForBubblewrap => write!(f, "{WSL1_BWRAP_WARNING}"),
#[cfg(not(target_os = "macos"))]
@@ -250,9 +263,13 @@ impl std::error::Error for SandboxTransformError {
match self {
Self::InvalidCommandCwd { source, .. }
| Self::InvalidSandboxPolicyCwd { source, .. } => Some(source),
Self::MissingLinuxSandboxExecutable => None,
Self::MissingLinuxSandboxExecutable
| Self::ManagedMitmCaPathUnderWritableRoot
| Self::ManagedMitmCustomCaUnsupportedOnWindows => None,
#[cfg(target_os = "linux")]
Self::Wsl1UnsupportedForBubblewrap => None,
Self::LegacyLandlockUnsupportedWithManagedMitm | Self::Wsl1UnsupportedForBubblewrap => {
None
}
#[cfg(not(target_os = "macos"))]
Self::SeatbeltUnavailable => None,
#[cfg(target_os = "windows")]
@@ -316,15 +333,16 @@ impl SandboxManager {
windows_sandbox_private_desktop,
} = request;
let additional_permissions = command.additional_permissions.take();
let managed_mitm_ca_trust_bundle_path =
network.and_then(NetworkProxy::managed_mitm_ca_trust_bundle_path);
let network_child_env = network.map(NetworkProxy::child_env_snapshot);
let base_effective_permission_profile =
effective_permission_profile(permissions, additional_permissions.as_ref());
let pending_sandboxed_request = PendingSandboxedExecRequest::new(
&command.cwd,
sandbox_policy_cwd,
&mut command.env,
base_effective_permission_profile.clone(),
managed_mitm_ca_trust_bundle_path.as_ref(),
network_child_env.as_ref(),
cfg!(target_os = "windows") && sandbox == SandboxType::WindowsRestrictedToken,
);
let (base_file_system_policy, base_network_policy) =
base_effective_permission_profile.to_runtime_permissions();
@@ -367,6 +385,9 @@ impl SandboxManager {
&pending.effective_file_system_policy,
use_legacy_landlock,
allow_proxy_network,
network_child_env
.as_ref()
.is_some_and(NetworkProxyChildEnvSnapshot::has_managed_mitm_ca),
is_wsl1(),
)?;
let mut args = create_linux_sandbox_command_args_for_permission_profile(
@@ -636,10 +657,16 @@ fn ensure_linux_bubblewrap_is_supported(
file_system_sandbox_policy: &FileSystemSandboxPolicy,
use_legacy_landlock: bool,
allow_network_for_proxy: bool,
managed_mitm_ca_active: bool,
is_wsl1: bool,
) -> Result<(), SandboxTransformError> {
let requires_bubblewrap = allow_network_for_proxy
|| (!use_legacy_landlock && !file_system_sandbox_policy.has_full_disk_write_access());
ensure_legacy_landlock_supports_managed_mitm(
file_system_sandbox_policy,
use_legacy_landlock,
managed_mitm_ca_active,
)?;
let requires_bubblewrap = !use_legacy_landlock
&& (!file_system_sandbox_policy.has_full_disk_write_access() || allow_network_for_proxy);
if is_wsl1 && requires_bubblewrap {
return Err(SandboxTransformError::Wsl1UnsupportedForBubblewrap);
}
@@ -647,6 +674,22 @@ fn ensure_linux_bubblewrap_is_supported(
Ok(())
}
#[cfg(target_os = "linux")]
pub fn ensure_legacy_landlock_supports_managed_mitm(
file_system_sandbox_policy: &FileSystemSandboxPolicy,
use_legacy_landlock: bool,
managed_mitm_ca_active: bool,
) -> Result<(), SandboxTransformError> {
if use_legacy_landlock
&& managed_mitm_ca_active
&& file_system_sandbox_policy.kind == FileSystemSandboxKind::Restricted
{
return Err(SandboxTransformError::LegacyLandlockUnsupportedWithManagedMitm);
}
Ok(())
}
fn os_argv_to_strings(argv: Vec<OsString>) -> Vec<String> {
argv.into_iter()
.map(os_string_to_command_component)

View File

@@ -5,8 +5,20 @@ use super::SandboxManager;
use super::SandboxTransformRequest;
use super::SandboxType;
use super::SandboxablePreference;
use super::can_read_path_with_policy;
use super::get_platform_sandbox;
use super::with_managed_mitm_ca_readable_root;
use super::read_deny_glob_matcher;
use super::with_managed_mitm_ca_proxy_dirs_denied;
#[cfg(any(target_os = "linux", target_os = "macos"))]
use super::with_managed_mitm_ca_readable_roots;
#[cfg(target_os = "windows")]
use codex_network_proxy::NetworkProxy;
#[cfg(target_os = "windows")]
use codex_network_proxy::NetworkProxyConfig;
#[cfg(target_os = "windows")]
use codex_network_proxy::NetworkProxyConstraints;
#[cfg(target_os = "windows")]
use codex_network_proxy::build_config_state;
use codex_protocol::config_types::WindowsSandboxLevel;
use codex_protocol::models::AdditionalPermissionProfile;
use codex_protocol::models::FileSystemPermissions;
@@ -18,11 +30,15 @@ use codex_protocol::permissions::FileSystemSandboxEntry;
use codex_protocol::permissions::FileSystemSandboxPolicy;
use codex_protocol::permissions::FileSystemSpecialPath;
use codex_protocol::permissions::NetworkSandboxPolicy;
#[cfg(any(target_os = "linux", target_os = "macos"))]
use codex_protocol::permissions::ReadDenyMatcher;
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_path_uri::PathUri;
use dunce::canonicalize;
use pretty_assertions::assert_eq;
use std::collections::HashMap;
#[cfg(target_os = "windows")]
use std::sync::Arc;
use tempfile::TempDir;
#[test]
@@ -258,45 +274,300 @@ fn transform_additional_permissions_preserves_denied_entries() {
}
#[test]
fn managed_mitm_ca_bundle_becomes_readable_for_restricted_sandbox() {
#[cfg(any(target_os = "linux", target_os = "macos"))]
fn managed_mitm_ca_bundle_is_only_readable_carveback_in_proxy_dir() {
let cwd = TempDir::new().expect("create cwd");
let cwd =
AbsolutePathBuf::from_absolute_path(canonicalize(cwd.path()).expect("canonicalize cwd"))
.expect("absolute cwd");
let managed_bundle_dir = TempDir::new().expect("create managed bundle dir");
let managed_bundle_path =
AbsolutePathBuf::from_absolute_path(managed_bundle_dir.path().join("ca-bundle.pem"))
.expect("absolute managed bundle path");
let managed_bundle_dir = AbsolutePathBuf::from_absolute_path(
canonicalize(managed_bundle_dir.path()).expect("canonicalize managed bundle dir"),
)
.expect("absolute managed bundle dir");
let managed_bundle_path = managed_bundle_dir.join("ca-bundle-active.pem");
let previous_bundle_path = managed_bundle_dir.join("ca-bundle-previous.pem");
let managed_ca_key_path = managed_bundle_dir.join("ca.key");
for path in [
&managed_bundle_path,
&previous_bundle_path,
&managed_ca_key_path,
] {
std::fs::write(path.as_path(), "fixture").expect("write managed CA fixture");
}
let permission_profile = PermissionProfile::from_runtime_permissions(
&FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry {
path: FileSystemPath::Path { path: cwd.clone() },
path: FileSystemPath::Special {
value: FileSystemSpecialPath::Root,
},
access: FileSystemAccessMode::Read,
}]),
NetworkSandboxPolicy::Restricted,
);
let permission_profile = with_managed_mitm_ca_readable_root(
let permission_profile = with_managed_mitm_ca_proxy_dirs_denied(
permission_profile,
Some(&managed_bundle_path),
std::slice::from_ref(&managed_bundle_path),
cwd.as_path(),
)
.expect("managed bundle directory should be outside writable roots");
let (file_system_sandbox_policy, _) = permission_profile.to_runtime_permissions();
let read_deny_matcher =
ReadDenyMatcher::new(&file_system_sandbox_policy, cwd.as_path()).expect("deny matcher");
for path in [
&managed_bundle_path,
&previous_bundle_path,
&managed_ca_key_path,
] {
assert!(!can_read_path_with_policy(
&file_system_sandbox_policy,
Some(&read_deny_matcher),
path.as_path(),
cwd.as_path(),
));
}
let permission_profile = with_managed_mitm_ca_readable_roots(
permission_profile,
std::slice::from_ref(&managed_bundle_path),
cwd.as_path(),
);
let (file_system_sandbox_policy, _) = permission_profile.to_runtime_permissions();
let read_deny_glob_matcher = read_deny_glob_matcher(&file_system_sandbox_policy, cwd.as_path());
assert_eq!(
file_system_sandbox_policy,
FileSystemSandboxPolicy::restricted(vec![
FileSystemSandboxEntry {
path: FileSystemPath::Path { path: cwd },
path: FileSystemPath::Special {
value: FileSystemSpecialPath::Root,
},
access: FileSystemAccessMode::Read,
},
FileSystemSandboxEntry {
path: FileSystemPath::Path {
path: managed_bundle_path,
path: managed_bundle_dir,
},
access: FileSystemAccessMode::Deny,
},
FileSystemSandboxEntry {
path: FileSystemPath::Path {
path: managed_bundle_path.clone(),
},
access: FileSystemAccessMode::Read,
},
])
);
assert!(can_read_path_with_policy(
&file_system_sandbox_policy,
read_deny_glob_matcher.as_ref(),
managed_bundle_path.as_path(),
cwd.as_path(),
));
assert!(
!file_system_sandbox_policy
.can_read_path_with_cwd(previous_bundle_path.as_path(), cwd.as_path(),)
);
assert!(
!file_system_sandbox_policy
.can_read_path_with_cwd(managed_ca_key_path.as_path(), cwd.as_path(),)
);
}
#[cfg(unix)]
#[test]
fn managed_mitm_ca_materialization_checks_canonical_target_policy() {
use std::os::unix::fs::symlink;
let root = TempDir::new().expect("create policy root");
let root = AbsolutePathBuf::from_absolute_path(
canonicalize(root.path()).expect("canonicalize policy root"),
)
.expect("absolute policy root");
let denied_dir = root.join("secrets");
std::fs::create_dir(denied_dir.as_path()).expect("create denied dir");
let denied_ca = denied_dir.join("ca.pem");
std::fs::write(denied_ca.as_path(), "secret CA").expect("write denied CA");
let readable_alias = root.join("readable-ca.pem");
symlink(denied_ca.as_path(), readable_alias.as_path()).expect("create readable alias");
let file_system_sandbox_policy = FileSystemSandboxPolicy::restricted(vec![
FileSystemSandboxEntry {
path: FileSystemPath::Path { path: root.clone() },
access: FileSystemAccessMode::Read,
},
FileSystemSandboxEntry {
path: FileSystemPath::Path { path: denied_dir },
access: FileSystemAccessMode::Deny,
},
FileSystemSandboxEntry {
path: FileSystemPath::Path {
path: readable_alias.clone(),
},
access: FileSystemAccessMode::Read,
},
]);
assert!(
file_system_sandbox_policy.can_read_path_with_cwd(readable_alias.as_path(), root.as_path()),
"the lexical alias is explicitly readable"
);
assert!(!can_read_path_with_policy(
&file_system_sandbox_policy,
/*read_deny_glob_matcher*/ None,
readable_alias.as_path(),
root.as_path(),
));
}
#[test]
fn managed_mitm_ca_proxy_dir_deny_preserves_profiles_without_restricted_filesystem() {
let managed_bundle_dir = TempDir::new().expect("create managed bundle dir");
let managed_bundle_path =
AbsolutePathBuf::from_absolute_path(managed_bundle_dir.path().join("ca-bundle.pem"))
.expect("absolute managed bundle path");
for permission_profile in [
PermissionProfile::Disabled,
PermissionProfile::from_runtime_permissions(
&FileSystemSandboxPolicy::unrestricted(),
NetworkSandboxPolicy::Restricted,
),
PermissionProfile::External {
network: NetworkSandboxPolicy::Restricted,
},
] {
assert_eq!(
with_managed_mitm_ca_proxy_dirs_denied(
permission_profile.clone(),
std::slice::from_ref(&managed_bundle_path),
managed_bundle_dir.path(),
)
.expect("profile should remain supported"),
permission_profile,
);
}
}
#[test]
#[cfg(any(target_os = "linux", target_os = "macos"))]
fn managed_mitm_ca_proxy_dir_rejects_writable_overlap() {
let writable_root = TempDir::new().expect("create writable root");
let proxy_dir = writable_root.path().join("proxy");
std::fs::create_dir(&proxy_dir).expect("create proxy dir");
let managed_bundle_path =
AbsolutePathBuf::from_absolute_path(proxy_dir.join("ca-bundle-active.pem"))
.expect("absolute managed bundle path");
let writable_key = AbsolutePathBuf::from_absolute_path(proxy_dir.join("ca.key"))
.expect("absolute writable key path");
std::fs::write(writable_key.as_path(), "secret key").expect("write managed key fixture");
let writable_root = AbsolutePathBuf::from_absolute_path(
canonicalize(writable_root.path()).expect("canonicalize writable root"),
)
.expect("absolute writable root");
let policies = [
FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry {
path: FileSystemPath::Path {
path: writable_root.clone(),
},
access: FileSystemAccessMode::Write,
}]),
FileSystemSandboxPolicy::restricted(vec![
FileSystemSandboxEntry {
path: FileSystemPath::Special {
value: FileSystemSpecialPath::Root,
},
access: FileSystemAccessMode::Read,
},
FileSystemSandboxEntry {
path: FileSystemPath::Path { path: writable_key },
access: FileSystemAccessMode::Write,
},
]),
FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry {
path: FileSystemPath::Special {
value: FileSystemSpecialPath::Root,
},
access: FileSystemAccessMode::Write,
}]),
];
for policy in policies {
let permission_profile =
PermissionProfile::from_runtime_permissions(&policy, NetworkSandboxPolicy::Restricted);
assert!(matches!(
with_managed_mitm_ca_proxy_dirs_denied(
permission_profile,
std::slice::from_ref(&managed_bundle_path),
writable_root.as_path(),
),
Err(super::SandboxTransformError::ManagedMitmCaPathUnderWritableRoot)
));
}
}
#[test]
#[cfg(target_os = "macos")]
fn managed_mitm_ca_proxy_dir_rejects_platform_default_writable_ancestor() {
let proxy_dir = tempfile::tempdir_in("/private/tmp").expect("create proxy dir");
let managed_bundle_path =
AbsolutePathBuf::from_absolute_path(proxy_dir.path().join("ca-bundle-active.pem"))
.expect("absolute managed bundle path");
let permission_profile = PermissionProfile::from_runtime_permissions(
&FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry {
path: FileSystemPath::Special {
value: FileSystemSpecialPath::Minimal,
},
access: FileSystemAccessMode::Read,
}]),
NetworkSandboxPolicy::Restricted,
);
assert!(matches!(
with_managed_mitm_ca_proxy_dirs_denied(
permission_profile,
std::slice::from_ref(&managed_bundle_path),
proxy_dir.path(),
),
Err(super::SandboxTransformError::ManagedMitmCaPathUnderWritableRoot)
));
}
#[test]
fn managed_mitm_ca_materialization_rejects_glob_denied_paths_from_command_subdir() {
let sandbox_policy_cwd = TempDir::new().expect("create cwd");
let sandbox_policy_cwd = AbsolutePathBuf::from_absolute_path(
canonicalize(sandbox_policy_cwd.path()).expect("canonicalize cwd"),
)
.expect("absolute cwd");
let command_cwd = sandbox_policy_cwd.join("subdir");
std::fs::create_dir(command_cwd.as_path()).expect("create command cwd");
let ca_bundle_path = command_cwd.join("../secrets/blocked.pem");
std::fs::create_dir(sandbox_policy_cwd.join("secrets").as_path()).expect("create secrets");
std::fs::write(ca_bundle_path.as_path(), "secret").expect("write blocked CA bundle");
let file_system_sandbox_policy = FileSystemSandboxPolicy::restricted(vec![
FileSystemSandboxEntry {
path: FileSystemPath::Path {
path: sandbox_policy_cwd.clone(),
},
access: FileSystemAccessMode::Read,
},
FileSystemSandboxEntry {
path: FileSystemPath::GlobPattern {
pattern: "secrets/**".to_string(),
},
access: FileSystemAccessMode::Deny,
},
]);
let read_deny_glob_matcher =
read_deny_glob_matcher(&file_system_sandbox_policy, sandbox_policy_cwd.as_path())
.expect("deny glob matcher");
assert!(!can_read_path_with_policy(
&file_system_sandbox_policy,
Some(&read_deny_glob_matcher),
ca_bundle_path.as_path(),
sandbox_policy_cwd.as_path(),
));
}
#[cfg(target_os = "linux")]
@@ -345,6 +616,7 @@ fn wsl1_rejects_linux_bubblewrap_path() {
&restricted_policy,
/*use_legacy_landlock*/ false,
/*allow_network_for_proxy*/ false,
/*managed_mitm_ca_active*/ false,
/*is_wsl1*/ true,
),
Err(super::SandboxTransformError::Wsl1UnsupportedForBubblewrap)
@@ -354,15 +626,7 @@ fn wsl1_rejects_linux_bubblewrap_path() {
&FileSystemSandboxPolicy::unrestricted(),
/*use_legacy_landlock*/ false,
/*allow_network_for_proxy*/ true,
/*is_wsl1*/ true,
),
Err(super::SandboxTransformError::Wsl1UnsupportedForBubblewrap)
));
assert!(matches!(
super::ensure_linux_bubblewrap_is_supported(
&FileSystemSandboxPolicy::unrestricted(),
/*use_legacy_landlock*/ true,
/*allow_network_for_proxy*/ true,
/*managed_mitm_ca_active*/ false,
/*is_wsl1*/ true,
),
Err(super::SandboxTransformError::Wsl1UnsupportedForBubblewrap)
@@ -377,6 +641,7 @@ fn wsl1_allows_non_bubblewrap_linux_paths() {
&FileSystemSandboxPolicy::unrestricted(),
/*use_legacy_landlock*/ false,
/*allow_network_for_proxy*/ false,
/*managed_mitm_ca_active*/ false,
/*is_wsl1*/ true,
)
.is_ok()
@@ -393,12 +658,65 @@ fn wsl1_allows_non_bubblewrap_linux_paths() {
&restricted_policy,
/*use_legacy_landlock*/ true,
/*allow_network_for_proxy*/ false,
/*managed_mitm_ca_active*/ false,
/*is_wsl1*/ true,
)
.is_ok()
);
}
#[cfg(target_os = "linux")]
#[test]
fn legacy_landlock_rejects_managed_mitm_ca_isolation_for_restricted_profiles() {
let restricted_policy = FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry {
path: FileSystemPath::Special {
value: FileSystemSpecialPath::Root,
},
access: FileSystemAccessMode::Read,
}]);
assert!(matches!(
super::ensure_linux_bubblewrap_is_supported(
&restricted_policy,
/*use_legacy_landlock*/ true,
/*allow_network_for_proxy*/ false,
/*managed_mitm_ca_active*/ true,
/*is_wsl1*/ false,
),
Err(super::SandboxTransformError::LegacyLandlockUnsupportedWithManagedMitm)
));
assert!(
super::ensure_linux_bubblewrap_is_supported(
&restricted_policy,
/*use_legacy_landlock*/ false,
/*allow_network_for_proxy*/ false,
/*managed_mitm_ca_active*/ true,
/*is_wsl1*/ false,
)
.is_ok()
);
assert!(
super::ensure_linux_bubblewrap_is_supported(
&restricted_policy,
/*use_legacy_landlock*/ true,
/*allow_network_for_proxy*/ false,
/*managed_mitm_ca_active*/ false,
/*is_wsl1*/ false,
)
.is_ok()
);
assert!(
super::ensure_linux_bubblewrap_is_supported(
&FileSystemSandboxPolicy::unrestricted(),
/*use_legacy_landlock*/ true,
/*allow_network_for_proxy*/ false,
/*managed_mitm_ca_active*/ true,
/*is_wsl1*/ false,
)
.is_ok()
);
}
#[cfg(target_os = "linux")]
#[test]
fn transform_linux_seccomp_preserves_helper_path_in_arg0_when_available() {
@@ -454,6 +772,56 @@ fn transform_for_direct_spawn_windows_preserves_only_wrapper_setup_identity() {
);
}
#[cfg(target_os = "windows")]
#[tokio::test]
async fn windows_restricted_transform_rejects_command_specific_ca() {
let cwd = TempDir::new().expect("create cwd");
let cwd = AbsolutePathBuf::from_absolute_path(cwd.path()).expect("absolute cwd");
let cwd_uri = PathUri::from_abs_path(&cwd);
let mut config = NetworkProxyConfig::default();
config.network.mitm = true;
let state = Arc::new(
build_config_state(config, NetworkProxyConstraints::default()).expect("build proxy state"),
);
let network = NetworkProxy::builder()
.state(state)
.managed_by_codex(/*managed_by_codex*/ false)
.build()
.await
.expect("build proxy");
let permissions = PermissionProfile::read_only();
let err = SandboxManager::new()
.transform(SandboxTransformRequest {
command: SandboxCommand {
program: "cmd.exe".into(),
args: vec!["/c".to_string(), "exit 0".to_string()],
cwd: cwd_uri.clone(),
env: HashMap::from([(
"REQUESTS_CA_BUNDLE".to_string(),
r"C:\command-ca.pem".to_string(),
)]),
additional_permissions: None,
},
permissions: &permissions,
sandbox: SandboxType::WindowsRestrictedToken,
enforce_managed_network: true,
environment_id: None,
network: Some(&network),
sandbox_policy_cwd: &cwd_uri,
codex_linux_sandbox_exe: None,
use_legacy_landlock: false,
windows_sandbox_level: WindowsSandboxLevel::Elevated,
windows_sandbox_private_desktop: false,
})
.expect_err("command-specific CA should be rejected");
assert!(matches!(
err,
super::SandboxTransformError::ManagedMitmCustomCaUnsupportedOnWindows
));
}
#[cfg(target_os = "windows")]
#[test]
fn transform_for_direct_spawn_windows_materializes_inner_helper() {