From ac77ede42d4ee5a2e51af446ef89c0fc3afa3df3 Mon Sep 17 00:00:00 2001 From: Winston Howes Date: Thu, 18 Jun 2026 12:16:19 -0700 Subject: [PATCH] Wire managed child MITM CA environments --- codex-rs/cli/src/debug_sandbox.rs | 52 ++- codex-rs/sandboxing/src/lib.rs | 24 +- codex-rs/sandboxing/src/managed_network.rs | 205 +++++++++++ codex-rs/sandboxing/src/manager.rs | 107 ++++-- codex-rs/sandboxing/src/manager_tests.rs | 406 ++++++++++++++++++++- 5 files changed, 724 insertions(+), 70 deletions(-) create mode 100644 codex-rs/sandboxing/src/managed_network.rs diff --git a/codex-rs/cli/src/debug_sandbox.rs b/codex-rs/cli/src/debug_sandbox.rs index 55a22d0155..9ea22da66c 100644 --- a/codex-rs/cli/src/debug_sandbox.rs +++ b/codex-rs/cli/src/debug_sandbox.rs @@ -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? } diff --git a/codex-rs/sandboxing/src/lib.rs b/codex-rs/sandboxing/src/lib.rs index bebfefe067..dbdb72840f 100644 --- a/codex-rs/sandboxing/src/lib.rs +++ b/codex-rs/sandboxing/src/lib.rs @@ -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 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()) diff --git a/codex-rs/sandboxing/src/managed_network.rs b/codex-rs/sandboxing/src/managed_network.rs new file mode 100644 index 0000000000..f453ec9913 --- /dev/null +++ b/codex-rs/sandboxing/src/managed_network.rs @@ -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 { + 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::>(); + 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, + command_cwd: &Path, + permission_profile: PermissionProfile, + sandbox_policy_cwd: &Path, + persistent_windows_sandbox: bool, +) -> Result { + let managed_mitm_ca_trust_bundle_paths = network + .and_then(NetworkProxyChildEnvSnapshot::managed_mitm_ca_trust_bundle_path) + .into_iter() + .collect::>(); + 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 { + // 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)) +} diff --git a/codex-rs/sandboxing/src/manager.rs b/codex-rs/sandboxing/src/manager.rs index f00e056907..27ebc384a1 100644 --- a/codex-rs/sandboxing/src/manager.rs +++ b/codex-rs/sandboxing/src/manager.rs @@ -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, - 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, effective_permission_profile: PermissionProfile, - managed_mitm_ca_trust_bundle_path: Option<&AbsolutePathBuf>, + network: Option<&NetworkProxyChildEnvSnapshot>, + persistent_windows_sandbox: bool, ) -> Result { // 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) -> Vec { argv.into_iter() .map(os_string_to_command_component) diff --git a/codex-rs/sandboxing/src/manager_tests.rs b/codex-rs/sandboxing/src/manager_tests.rs index 64fc87b6e4..89c06d7e2f 100644 --- a/codex-rs/sandboxing/src/manager_tests.rs +++ b/codex-rs/sandboxing/src/manager_tests.rs @@ -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() {