diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index 8c0a165564..ca6198bc92 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -919,7 +919,8 @@ pub struct Config { pub codex_self_exe: Option, /// Path to the `codex-linux-sandbox` executable. This must be set if - /// [`codex_sandboxing::SandboxType::LinuxSeccomp`] is used. Note that this + /// [`codex_sandboxing::SandboxType::LinuxBubblewrap`] or + /// [`codex_sandboxing::SandboxType::LinuxLegacyLandlock`] is used. Note that this /// cannot be set in the config file: it must be set in code via /// [`ConfigOverrides`]. /// diff --git a/codex-rs/core/src/exec.rs b/codex-rs/core/src/exec.rs index 1b26214269..2464501b64 100644 --- a/codex-rs/core/src/exec.rs +++ b/codex-rs/core/src/exec.rs @@ -349,12 +349,15 @@ pub fn build_exec_request( let enforce_managed_network = network.is_some(); let (file_system_sandbox_policy, network_sandbox_policy) = permission_profile.to_runtime_permissions(); - let sandbox_type = select_process_exec_tool_sandbox_type( + let mut sandbox_type = select_process_exec_tool_sandbox_type( &file_system_sandbox_policy, network_sandbox_policy, windows_sandbox_level, enforce_managed_network, ); + if use_legacy_landlock && sandbox_type == SandboxType::LinuxBubblewrap { + sandbox_type = SandboxType::LinuxLegacyLandlock; + } tracing::debug!("Sandbox type: {sandbox_type:?}"); if let Some(network) = network.as_ref() { @@ -396,7 +399,6 @@ pub fn build_exec_request( network: network.as_ref(), sandbox_policy_cwd: &sandbox_policy_cwd_uri, codex_linux_sandbox_exe: codex_linux_sandbox_exe.as_deref(), - use_legacy_landlock, windows_sandbox_level, windows_sandbox_private_desktop, }) @@ -825,68 +827,6 @@ fn finalize_exec_result( } } -/// We don't have a fully deterministic way to tell if our command failed -/// because of the sandbox - a command in the user's zshrc file might hit an -/// error, but the command itself might fail or succeed for other reasons. -/// For now, we conservatively check for well known command failure exit codes and -/// also look for common sandbox denial keywords in the command output. -pub(crate) fn is_likely_sandbox_denied( - sandbox_type: SandboxType, - exec_output: &ExecToolCallOutput, -) -> bool { - if sandbox_type == SandboxType::None || exec_output.exit_code == 0 { - return false; - } - - // Quick rejects: well-known non-sandbox shell exit codes - // 2: misuse of shell builtins - // 126: permission denied - // 127: command not found - const SANDBOX_DENIED_KEYWORDS: [&str; 7] = [ - "operation not permitted", - "permission denied", - "read-only file system", - "seccomp", - "sandbox", - "landlock", - "failed to write file", - ]; - - let has_sandbox_keyword = [ - &exec_output.stderr.text, - &exec_output.stdout.text, - &exec_output.aggregated_output.text, - ] - .into_iter() - .any(|section| { - let lower = section.to_lowercase(); - SANDBOX_DENIED_KEYWORDS - .iter() - .any(|needle| lower.contains(needle)) - }); - - if has_sandbox_keyword { - return true; - } - - const QUICK_REJECT_EXIT_CODES: [i32; 3] = [2, 126, 127]; - if QUICK_REJECT_EXIT_CODES.contains(&exec_output.exit_code) { - return false; - } - - #[cfg(unix)] - { - const SIGSYS_CODE: i32 = libc::SIGSYS; - if sandbox_type == SandboxType::LinuxSeccomp - && exec_output.exit_code == EXIT_CODE_SIGNAL_BASE + SIGSYS_CODE - { - return true; - } - } - - false -} - #[derive(Debug)] struct RawExecToolCallOutput { pub exit_status: ExitStatus, diff --git a/codex-rs/core/src/exec_tests.rs b/codex-rs/core/src/exec_tests.rs index 1a4477599d..16efb73fe8 100644 --- a/codex-rs/core/src/exec_tests.rs +++ b/codex-rs/core/src/exec_tests.rs @@ -30,7 +30,7 @@ fn make_exec_output( fn sandbox_detection_requires_keywords() { let output = make_exec_output(/*exit_code*/ 1, "", "", ""); assert!(!is_likely_sandbox_denied( - SandboxType::LinuxSeccomp, + SandboxType::LinuxBubblewrap, &output )); } @@ -38,14 +38,17 @@ fn sandbox_detection_requires_keywords() { #[test] fn sandbox_detection_identifies_keyword_in_stderr() { let output = make_exec_output(/*exit_code*/ 1, "", "Operation not permitted", ""); - assert!(is_likely_sandbox_denied(SandboxType::LinuxSeccomp, &output)); + assert!(is_likely_sandbox_denied( + SandboxType::LinuxBubblewrap, + &output + )); } #[test] fn sandbox_detection_respects_quick_reject_exit_codes() { let output = make_exec_output(/*exit_code*/ 127, "", "command not found", ""); assert!(!is_likely_sandbox_denied( - SandboxType::LinuxSeccomp, + SandboxType::LinuxBubblewrap, &output )); } @@ -91,7 +94,7 @@ fn sandbox_detection_ignores_network_policy_text_with_zero_exit_code() { ); assert!(!is_likely_sandbox_denied( - SandboxType::LinuxSeccomp, + SandboxType::LinuxBubblewrap, &output )); } @@ -1027,7 +1030,12 @@ fn build_exec_request_preserves_windows_workspace_roots() -> Result<()> { fn sandbox_detection_flags_sigsys_exit_code() { let exit_code = EXIT_CODE_SIGNAL_BASE + libc::SIGSYS; let output = make_exec_output(exit_code, "", "", ""); - assert!(is_likely_sandbox_denied(SandboxType::LinuxSeccomp, &output)); + for sandbox_type in [ + SandboxType::LinuxBubblewrap, + SandboxType::LinuxLegacyLandlock, + ] { + assert!(is_likely_sandbox_denied(sandbox_type, &output)); + } } #[cfg(unix)] diff --git a/codex-rs/core/src/tools/orchestrator.rs b/codex-rs/core/src/tools/orchestrator.rs index f82ad5bb70..644fa0d6e3 100644 --- a/codex-rs/core/src/tools/orchestrator.rs +++ b/codex-rs/core/src/tools/orchestrator.rs @@ -260,6 +260,13 @@ impl ToolOrchestrator { // Platform-specific flag gating is handled by SandboxManager::select_initial. let use_legacy_landlock = turn_ctx.config.features.use_legacy_landlock(); + let initial_sandbox = + if use_legacy_landlock && initial_sandbox == SandboxType::LinuxBubblewrap { + SandboxType::LinuxLegacyLandlock + } else { + initial_sandbox + }; + #[allow(deprecated)] let sandbox_policy_cwd = tool .sandbox_cwd(req) @@ -444,6 +451,12 @@ impl ToolOrchestrator { } else { SandboxType::None }; + let retry_sandbox = + if use_legacy_landlock && retry_sandbox == SandboxType::LinuxBubblewrap { + SandboxType::LinuxLegacyLandlock + } else { + retry_sandbox + }; let retry_codex_linux_sandbox_exe = if unsandboxed_allowed { None } else { diff --git a/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs b/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs index 657a249c70..8e910267fa 100644 --- a/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs +++ b/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs @@ -999,13 +999,16 @@ impl CoreShellCommandExecutor { .split_first() .ok_or_else(|| anyhow::anyhow!("prepared command must not be empty"))?; let sandbox_manager = SandboxManager::new(); - let sandbox = sandbox_manager.select_initial( + let mut sandbox = sandbox_manager.select_initial( &file_system_sandbox_policy, network_sandbox_policy, SandboxablePreference::Auto, self.windows_sandbox_level, self.network.is_some(), ); + if self.use_legacy_landlock && sandbox == SandboxType::LinuxBubblewrap { + sandbox = SandboxType::LinuxLegacyLandlock; + } let cwd = PathUri::from_abs_path(workdir); let sandbox_policy_cwd = PathUri::from_abs_path(&self.sandbox_policy_cwd); let command = SandboxCommand { @@ -1029,7 +1032,6 @@ impl CoreShellCommandExecutor { network: self.network.as_ref(), sandbox_policy_cwd: &sandbox_policy_cwd, codex_linux_sandbox_exe: self.codex_linux_sandbox_exe.as_deref(), - use_legacy_landlock: self.use_legacy_landlock, windows_sandbox_level: self.windows_sandbox_level, windows_sandbox_private_desktop: false, })?; diff --git a/codex-rs/core/src/tools/sandboxing.rs b/codex-rs/core/src/tools/sandboxing.rs index 571c5ce6df..a1f8843913 100644 --- a/codex-rs/core/src/tools/sandboxing.rs +++ b/codex-rs/core/src/tools/sandboxing.rs @@ -459,7 +459,6 @@ impl<'a> SandboxAttempt<'a> { codex_linux_sandbox_exe: self .codex_linux_sandbox_exe .map(std::path::PathBuf::as_path), - use_legacy_landlock: self.use_legacy_landlock, windows_sandbox_level: self.windows_sandbox_level, windows_sandbox_private_desktop: self.windows_sandbox_private_desktop, }) diff --git a/codex-rs/exec-server/src/fs_sandbox.rs b/codex-rs/exec-server/src/fs_sandbox.rs index 812d76751c..026454e4db 100644 --- a/codex-rs/exec-server/src/fs_sandbox.rs +++ b/codex-rs/exec-server/src/fs_sandbox.rs @@ -13,6 +13,7 @@ use codex_sandboxing::SandboxDirectSpawnTransformRequest; use codex_sandboxing::SandboxExecRequest; use codex_sandboxing::SandboxManager; use codex_sandboxing::SandboxTransformRequest; +use codex_sandboxing::SandboxType; use codex_sandboxing::SandboxablePreference; use codex_utils_absolute_path::AbsolutePathBuf; use codex_utils_absolute_path::canonicalize_preserving_symlinks; @@ -103,13 +104,16 @@ impl FileSystemSandboxRunner { let helper = &self.runtime_paths.codex_self_exe; let sandbox_manager = SandboxManager::new(); let (file_system_policy, network_policy) = permission_profile.to_runtime_permissions(); - let sandbox = sandbox_manager.select_initial( + let mut sandbox = sandbox_manager.select_initial( &file_system_policy, network_policy, SandboxablePreference::Auto, sandbox_context.windows_sandbox_level, /*has_managed_network_requirements*/ false, ); + if sandbox_context.use_legacy_landlock && sandbox == SandboxType::LinuxBubblewrap { + sandbox = SandboxType::LinuxLegacyLandlock; + } let command = SandboxCommand { program: helper.as_path().as_os_str().to_owned(), args: vec![CODEX_FS_HELPER_ARG1.to_string()], @@ -142,7 +146,6 @@ impl FileSystemSandboxRunner { network: None, sandbox_policy_cwd: &cwd.uri, codex_linux_sandbox_exe: self.runtime_paths.codex_linux_sandbox_exe.as_deref(), - use_legacy_landlock: sandbox_context.use_legacy_landlock, windows_sandbox_level: sandbox_context.windows_sandbox_level, windows_sandbox_private_desktop: sandbox_context .windows_sandbox_private_desktop, diff --git a/codex-rs/exec-server/src/process_sandbox.rs b/codex-rs/exec-server/src/process_sandbox.rs index b5dd54e0b2..f36078f252 100644 --- a/codex-rs/exec-server/src/process_sandbox.rs +++ b/codex-rs/exec-server/src/process_sandbox.rs @@ -78,13 +78,16 @@ pub(crate) fn prepare_exec_request( ); let (file_system_policy, network_policy) = permissions.to_runtime_permissions(); let sandbox_manager = SandboxManager::new(); - let sandbox = sandbox_manager.select_initial( + let mut sandbox = sandbox_manager.select_initial( &file_system_policy, network_policy, SandboxablePreference::Require, sandbox_context.windows_sandbox_level, params.enforce_managed_network, ); + if sandbox_context.use_legacy_landlock && sandbox == SandboxType::LinuxBubblewrap { + sandbox = SandboxType::LinuxLegacyLandlock; + } match sandbox { SandboxType::None => { return Err(invalid_params( @@ -98,7 +101,9 @@ pub(crate) fn prepare_exec_request( "sandboxed remote process launch is not supported on Windows".to_string(), )); } - SandboxType::MacosSeatbelt | SandboxType::LinuxSeccomp => {} + SandboxType::MacosSeatbelt + | SandboxType::LinuxBubblewrap + | SandboxType::LinuxLegacyLandlock => {} } let (program, args) = params .argv @@ -127,7 +132,6 @@ pub(crate) fn prepare_exec_request( network: None, sandbox_policy_cwd, codex_linux_sandbox_exe: runtime_paths.codex_linux_sandbox_exe.as_deref(), - use_legacy_landlock: sandbox_context.use_legacy_landlock, windows_sandbox_level: sandbox_context.windows_sandbox_level, windows_sandbox_private_desktop: sandbox_context.windows_sandbox_private_desktop, }, diff --git a/codex-rs/sandboxing/src/denial.rs b/codex-rs/sandboxing/src/denial.rs index f355fa1c02..799e556c2d 100644 --- a/codex-rs/sandboxing/src/denial.rs +++ b/codex-rs/sandboxing/src/denial.rs @@ -2,7 +2,11 @@ use codex_protocol::exec_output::ExecToolCallOutput; use crate::SandboxType; -/// Returns whether a failed command was likely denied by the selected sandbox. +/// We don't have a fully deterministic way to tell if our command failed +/// because of the sandbox - a command in the user's zshrc file might hit an +/// error, but the command itself might fail or succeed for other reasons. +/// For now, we conservatively check for well known command failure exit codes and +/// also look for common sandbox denial keywords in the command output. pub fn is_likely_sandbox_denied( sandbox_type: SandboxType, exec_output: &ExecToolCallOutput, @@ -11,6 +15,10 @@ pub fn is_likely_sandbox_denied( return false; } + // Quick rejects: well-known non-sandbox shell exit codes + // 2: misuse of shell builtins + // 126: permission denied + // 127: command not found const SANDBOX_DENIED_KEYWORDS: [&str; 7] = [ "operation not permitted", "permission denied", @@ -46,8 +54,11 @@ pub fn is_likely_sandbox_denied( #[cfg(unix)] { const EXIT_CODE_SIGNAL_BASE: i32 = 128; - if sandbox_type == SandboxType::LinuxSeccomp - && exec_output.exit_code == EXIT_CODE_SIGNAL_BASE + libc::SIGSYS + const SIGSYS_CODE: i32 = libc::SIGSYS; + if matches!( + sandbox_type, + SandboxType::LinuxBubblewrap | SandboxType::LinuxLegacyLandlock + ) && exec_output.exit_code == EXIT_CODE_SIGNAL_BASE + SIGSYS_CODE { return true; } diff --git a/codex-rs/sandboxing/src/lib.rs b/codex-rs/sandboxing/src/lib.rs index 59b96b2f3e..3a63c7f59f 100644 --- a/codex-rs/sandboxing/src/lib.rs +++ b/codex-rs/sandboxing/src/lib.rs @@ -29,6 +29,7 @@ pub use manager::with_managed_mitm_ca_readable_root; pub use violation::FileSystemSandboxViolation; pub use violation::FileSystemSandboxViolationReason; pub use violation::NetworkSandboxViolation; +pub use violation::SandboxViolationBackend; pub use violation::SandboxViolationEvent; pub use violation::record_filesystem_sandbox_violation; pub use violation::record_network_sandbox_violation; diff --git a/codex-rs/sandboxing/src/manager.rs b/codex-rs/sandboxing/src/manager.rs index 51d2d6c98e..bf5785ab51 100644 --- a/codex-rs/sandboxing/src/manager.rs +++ b/codex-rs/sandboxing/src/manager.rs @@ -35,17 +35,19 @@ const WINDOWS_SANDBOX_WRAPPER_SETUP_ENV_ALLOWLIST: &[&str] = &["USERNAME", "USER pub enum SandboxType { None, MacosSeatbelt, - LinuxSeccomp, + LinuxBubblewrap, + LinuxLegacyLandlock, WindowsRestrictedToken, } impl SandboxType { - pub fn as_metric_tag(self) -> &'static str { + pub const fn as_metric_tag(self) -> &'static str { match self { - SandboxType::None => "none", - SandboxType::MacosSeatbelt => "seatbelt", - SandboxType::LinuxSeccomp => "seccomp", - SandboxType::WindowsRestrictedToken => "windows_sandbox", + Self::None => "none", + Self::MacosSeatbelt => "seatbelt", + Self::LinuxBubblewrap => "bubblewrap", + Self::LinuxLegacyLandlock => "legacy_landlock", + Self::WindowsRestrictedToken => "windows_sandbox", } } } @@ -61,7 +63,7 @@ pub fn get_platform_sandbox(windows_sandbox_enabled: bool) -> Option { pub network: Option<&'a NetworkProxy>, pub sandbox_policy_cwd: &'a PathUri, pub codex_linux_sandbox_exe: Option<&'a Path>, - pub use_legacy_landlock: bool, pub windows_sandbox_level: WindowsSandboxLevel, pub windows_sandbox_private_desktop: bool, } @@ -331,10 +332,10 @@ impl SandboxManager { network, sandbox_policy_cwd, codex_linux_sandbox_exe, - use_legacy_landlock, windows_sandbox_level, windows_sandbox_private_desktop, } = request; + let use_legacy_landlock = sandbox == SandboxType::LinuxLegacyLandlock; #[cfg(target_os = "macos")] let managed_network = command.managed_network.as_ref(); let additional_permissions = command.additional_permissions.take(); @@ -382,7 +383,7 @@ impl SandboxManager { } #[cfg(not(target_os = "macos"))] SandboxType::MacosSeatbelt => return Err(SandboxTransformError::SeatbeltUnavailable), - SandboxType::LinuxSeccomp => { + SandboxType::LinuxBubblewrap | SandboxType::LinuxLegacyLandlock => { let pending = pending_sandboxed_request?; let exe = codex_linux_sandbox_exe .ok_or(SandboxTransformError::MissingLinuxSandboxExecutable)?; diff --git a/codex-rs/sandboxing/src/manager_tests.rs b/codex-rs/sandboxing/src/manager_tests.rs index c1a52182d7..3f29c5518f 100644 --- a/codex-rs/sandboxing/src/manager_tests.rs +++ b/codex-rs/sandboxing/src/manager_tests.rs @@ -102,7 +102,6 @@ fn unsandboxed_transform_preserves_foreign_cwd_and_unrestricted_file_system_poli network: None, sandbox_policy_cwd: &cwd_uri, codex_linux_sandbox_exe: None, - use_legacy_landlock: false, windows_sandbox_level: WindowsSandboxLevel::Disabled, windows_sandbox_private_desktop: false, }) @@ -158,7 +157,6 @@ fn transform_additional_permissions_enable_network_for_external_sandbox() { network: None, sandbox_policy_cwd: &cwd_uri, codex_linux_sandbox_exe: None, - use_legacy_landlock: false, windows_sandbox_level: WindowsSandboxLevel::Disabled, windows_sandbox_private_desktop: false, }) @@ -229,7 +227,6 @@ fn transform_additional_permissions_preserves_denied_entries() { network: None, sandbox_policy_cwd: &cwd_uri, codex_linux_sandbox_exe: None, - use_legacy_landlock: false, windows_sandbox_level: WindowsSandboxLevel::Disabled, windows_sandbox_private_desktop: false, }) @@ -303,8 +300,9 @@ fn managed_mitm_ca_bundle_becomes_readable_for_restricted_sandbox() { } #[cfg(target_os = "linux")] -fn transform_linux_seccomp_request( +fn transform_linux_request( codex_linux_sandbox_exe: &std::path::Path, + sandbox: SandboxType, ) -> super::SandboxExecRequest { let manager = SandboxManager::new(); let cwd = AbsolutePathBuf::current_dir().expect("current dir"); @@ -321,13 +319,12 @@ fn transform_linux_seccomp_request( additional_permissions: None, }, permissions: &permissions, - sandbox: SandboxType::LinuxSeccomp, + sandbox, enforce_managed_network: false, environment_id: None, network: None, sandbox_policy_cwd: &cwd_uri, codex_linux_sandbox_exe: Some(codex_linux_sandbox_exe), - use_legacy_landlock: false, windows_sandbox_level: WindowsSandboxLevel::Disabled, windows_sandbox_private_desktop: false, }) @@ -405,9 +402,10 @@ fn wsl1_allows_non_bubblewrap_linux_paths() { #[cfg(target_os = "linux")] #[test] -fn transform_linux_seccomp_preserves_helper_path_in_arg0_when_available() { +fn transform_linux_bubblewrap_preserves_helper_path_in_arg0_when_available() { let codex_linux_sandbox_exe = std::path::PathBuf::from("/tmp/codex-linux-sandbox"); - let exec_request = transform_linux_seccomp_request(&codex_linux_sandbox_exe); + let exec_request = + transform_linux_request(&codex_linux_sandbox_exe, SandboxType::LinuxBubblewrap); assert_eq!( exec_request.arg0, @@ -417,9 +415,10 @@ fn transform_linux_seccomp_preserves_helper_path_in_arg0_when_available() { #[cfg(target_os = "linux")] #[test] -fn transform_linux_seccomp_uses_helper_alias_when_launcher_is_not_helper_path() { +fn transform_linux_bubblewrap_uses_helper_alias_when_launcher_is_not_helper_path() { let codex_linux_sandbox_exe = std::path::PathBuf::from("/tmp/codex"); - let exec_request = transform_linux_seccomp_request(&codex_linux_sandbox_exe); + let exec_request = + transform_linux_request(&codex_linux_sandbox_exe, SandboxType::LinuxBubblewrap); assert_eq!(exec_request.arg0, Some("codex-linux-sandbox".to_string())); } @@ -520,7 +519,6 @@ fn transform_for_direct_spawn_windows_materializes_inner_helper() { network: None, sandbox_policy_cwd: &cwd_uri, codex_linux_sandbox_exe: None, - use_legacy_landlock: false, windows_sandbox_level: WindowsSandboxLevel::Elevated, windows_sandbox_private_desktop: false, }, @@ -583,3 +581,19 @@ fn transform_for_direct_spawn_windows_materializes_inner_helper() { ); assert!(materialized_helper.exists()); } + +#[cfg(target_os = "linux")] +#[test] +fn transform_linux_landlock_uses_legacy_backend() { + let exec_request = transform_linux_request( + std::path::Path::new("/tmp/codex-linux-sandbox"), + SandboxType::LinuxLegacyLandlock, + ); + + assert_eq!(exec_request.sandbox, SandboxType::LinuxLegacyLandlock); + assert!( + exec_request + .command + .contains(&"--use-legacy-landlock".to_string()) + ); +} diff --git a/codex-rs/sandboxing/src/violation.rs b/codex-rs/sandboxing/src/violation.rs index 57d8a3a006..2e3237fdc4 100644 --- a/codex-rs/sandboxing/src/violation.rs +++ b/codex-rs/sandboxing/src/violation.rs @@ -21,9 +21,9 @@ const SANDBOX_DENIED_KEYWORDS: [(FileSystemSandboxViolationReason, &str); 7] = [ FileSystemSandboxViolationReason::ReadOnlyFileSystem, "read-only file system", ), - (FileSystemSandboxViolationReason::Seccomp, "seccomp"), - (FileSystemSandboxViolationReason::Sandbox, "sandbox"), - (FileSystemSandboxViolationReason::Landlock, "landlock"), + (FileSystemSandboxViolationReason::PolicyDenied, "seccomp"), + (FileSystemSandboxViolationReason::PolicyDenied, "sandbox"), + (FileSystemSandboxViolationReason::PolicyDenied, "landlock"), ( FileSystemSandboxViolationReason::FailedToWriteFile, "failed to write file", @@ -43,10 +43,32 @@ pub enum SandboxViolationEvent { Network(NetworkSandboxViolation), } +/// Enforcement backend that observed a sandbox violation. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum SandboxViolationBackend { + Bubblewrap, + LegacyLandlock, + ManagedNetworkProxy, + Seatbelt, + WindowsSandbox, +} + +impl SandboxViolationBackend { + pub const fn as_str(self) -> &'static str { + match self { + Self::Bubblewrap => "bubblewrap", + Self::LegacyLandlock => "legacy_landlock", + Self::ManagedNetworkProxy => "managed_network_proxy", + Self::Seatbelt => "seatbelt", + Self::WindowsSandbox => "windows_sandbox", + } + } +} + /// A filesystem sandbox denial inferred from a sandboxed process result. #[derive(Clone, Debug, PartialEq, Eq)] pub struct FileSystemSandboxViolation { - pub sandbox_type: SandboxType, + pub backend: SandboxViolationBackend, pub reason: FileSystemSandboxViolationReason, pub path: Option, pub output_snippet: String, @@ -58,9 +80,7 @@ pub enum FileSystemSandboxViolationReason { OperationNotPermitted, PermissionDenied, ReadOnlyFileSystem, - Seccomp, - Sandbox, - Landlock, + PolicyDenied, FailedToWriteFile, SignalSyscall, } @@ -71,9 +91,7 @@ impl FileSystemSandboxViolationReason { Self::OperationNotPermitted => "operation_not_permitted", Self::PermissionDenied => "permission_denied", Self::ReadOnlyFileSystem => "read_only_file_system", - Self::Seccomp => "seccomp", - Self::Sandbox => "sandbox", - Self::Landlock => "landlock", + Self::PolicyDenied => "policy_denied", Self::FailedToWriteFile => "failed_to_write_file", Self::SignalSyscall => "sigsys", } @@ -83,6 +101,7 @@ impl FileSystemSandboxViolationReason { /// A network sandbox denial reported by the managed network proxy. #[derive(Clone, Debug, PartialEq, Eq)] pub struct NetworkSandboxViolation { + pub backend: SandboxViolationBackend, pub host: String, pub reason: String, pub client: Option, @@ -98,6 +117,7 @@ pub struct NetworkSandboxViolation { impl NetworkSandboxViolation { pub fn from_blocked_request(blocked: &BlockedRequest) -> Self { Self { + backend: SandboxViolationBackend::ManagedNetworkProxy, host: blocked.host.clone(), reason: blocked.reason.clone(), client: blocked.client.clone(), @@ -117,13 +137,20 @@ fn classify_filesystem_sandbox_violation( sandbox_type: SandboxType, exec_output: &ExecToolCallOutput, ) -> Option { - if sandbox_type == SandboxType::None || exec_output.exit_code == 0 { + if exec_output.exit_code == 0 { return None; } + let backend = match sandbox_type { + SandboxType::None => return None, + SandboxType::MacosSeatbelt => SandboxViolationBackend::Seatbelt, + SandboxType::LinuxBubblewrap => SandboxViolationBackend::Bubblewrap, + SandboxType::LinuxLegacyLandlock => SandboxViolationBackend::LegacyLandlock, + SandboxType::WindowsRestrictedToken => SandboxViolationBackend::WindowsSandbox, + }; if let Some(reason) = filesystem_reason_from_output(exec_output) { return Some(FileSystemSandboxViolation { - sandbox_type, + backend, reason, path: extract_denied_path(exec_output), output_snippet: output_snippet(exec_output), @@ -136,11 +163,13 @@ fn classify_filesystem_sandbox_violation( #[cfg(unix)] { - if sandbox_type == SandboxType::LinuxSeccomp - && exec_output.exit_code == EXIT_CODE_SIGNAL_BASE + libc::SIGSYS + if matches!( + sandbox_type, + SandboxType::LinuxBubblewrap | SandboxType::LinuxLegacyLandlock + ) && exec_output.exit_code == EXIT_CODE_SIGNAL_BASE + libc::SIGSYS { return Some(FileSystemSandboxViolation { - sandbox_type, + backend, reason: FileSystemSandboxViolationReason::SignalSyscall, path: None, output_snippet: output_snippet(exec_output), @@ -174,15 +203,16 @@ pub fn record_sandbox_violation(event: &SandboxViolationEvent) { SandboxViolationEvent::FileSystem(violation) => { let path = violation.path.as_deref().unwrap_or("unknown"); warn!( - "recorded sandbox violation: resource=filesystem sandbox={} reason={} path={}", - violation.sandbox_type.as_metric_tag(), + "recorded sandbox violation: resource=filesystem backend={} reason={} path={}", + violation.backend.as_str(), violation.reason.as_str(), path ); } SandboxViolationEvent::Network(violation) => { warn!( - "recorded sandbox violation: resource=network protocol={} host={} port={:?} reason={} method={:?} mode={:?} client={:?} decision={:?} source={:?}", + "recorded sandbox violation: resource=network backend={} protocol={} host={} port={:?} reason={} method={:?} mode={:?} client={:?} decision={:?} source={:?}", + violation.backend.as_str(), violation.protocol, violation.host, violation.port, diff --git a/codex-rs/sandboxing/src/violation_tests.rs b/codex-rs/sandboxing/src/violation_tests.rs index 9c03af16bd..4bd751907d 100644 --- a/codex-rs/sandboxing/src/violation_tests.rs +++ b/codex-rs/sandboxing/src/violation_tests.rs @@ -37,12 +37,29 @@ fn classifies_legacy_denial_keywords() { let output = make_exec_output(/*exit_code*/ 1, "", keyword, ""); assert!( - classify_filesystem_sandbox_violation(SandboxType::LinuxSeccomp, &output).is_some(), + classify_filesystem_sandbox_violation(SandboxType::LinuxBubblewrap, &output).is_some(), "{keyword}" ); } } +#[test] +fn normalizes_backend_keywords_as_policy_denied() { + for keyword in ["seccomp", "sandbox", "landlock"] { + let output = make_exec_output(/*exit_code*/ 1, "", keyword, ""); + + assert_eq!( + classify_filesystem_sandbox_violation(SandboxType::LinuxBubblewrap, &output), + Some(FileSystemSandboxViolation { + backend: SandboxViolationBackend::Bubblewrap, + reason: FileSystemSandboxViolationReason::PolicyDenied, + path: None, + output_snippet: keyword.to_string(), + }) + ); + } +} + #[test] fn preserves_legacy_denial_ordering() { let quick_reject_without_keyword = @@ -56,21 +73,24 @@ fn preserves_legacy_denial_ordering() { assert!( classify_filesystem_sandbox_violation( - SandboxType::LinuxSeccomp, + SandboxType::LinuxBubblewrap, &quick_reject_without_keyword ) .is_none() ); assert!( classify_filesystem_sandbox_violation( - SandboxType::LinuxSeccomp, + SandboxType::LinuxBubblewrap, &quick_reject_with_keyword ) .is_some() ); assert!( - classify_filesystem_sandbox_violation(SandboxType::LinuxSeccomp, &zero_exit_with_keyword) - .is_none() + classify_filesystem_sandbox_violation( + SandboxType::LinuxBubblewrap, + &zero_exit_with_keyword + ) + .is_none() ); assert!( classify_filesystem_sandbox_violation(SandboxType::None, &non_sandbox_with_keyword) @@ -90,7 +110,7 @@ fn classifies_filesystem_violation_with_path() { assert_eq!( classify_filesystem_sandbox_violation(SandboxType::MacosSeatbelt, &output), Some(FileSystemSandboxViolation { - sandbox_type: SandboxType::MacosSeatbelt, + backend: SandboxViolationBackend::Seatbelt, reason: FileSystemSandboxViolationReason::OperationNotPermitted, path: Some("/private/tmp/denied".to_string()), output_snippet: "bash: /private/tmp/denied: Operation not permitted".to_string(), @@ -110,7 +130,7 @@ fn classifies_filesystem_violation_with_unicode_before_marker() { assert_eq!( classify_filesystem_sandbox_violation(SandboxType::MacosSeatbelt, &output), Some(FileSystemSandboxViolation { - sandbox_type: SandboxType::MacosSeatbelt, + backend: SandboxViolationBackend::Seatbelt, reason: FileSystemSandboxViolationReason::OperationNotPermitted, path: Some("/private/tmp/\u{130}-denied".to_string()), output_snippet: "bash: /private/tmp/\u{130}-denied: Operation not permitted" @@ -131,7 +151,7 @@ fn classifies_filesystem_violation_from_aggregated_output() { assert_eq!( classify_filesystem_sandbox_violation(SandboxType::MacosSeatbelt, &output), Some(FileSystemSandboxViolation { - sandbox_type: SandboxType::MacosSeatbelt, + backend: SandboxViolationBackend::Seatbelt, reason: FileSystemSandboxViolationReason::ReadOnlyFileSystem, path: None, output_snippet: "cargo failed: Read-only file system when writing target".to_string(), @@ -149,15 +169,26 @@ fn classifies_linux_sigsys_exit() { "", ); - assert_eq!( - classify_filesystem_sandbox_violation(SandboxType::LinuxSeccomp, &output), - Some(FileSystemSandboxViolation { - sandbox_type: SandboxType::LinuxSeccomp, - reason: FileSystemSandboxViolationReason::SignalSyscall, - path: None, - output_snippet: String::new(), - }) - ); + for (sandbox_type, backend) in [ + ( + SandboxType::LinuxBubblewrap, + SandboxViolationBackend::Bubblewrap, + ), + ( + SandboxType::LinuxLegacyLandlock, + SandboxViolationBackend::LegacyLandlock, + ), + ] { + assert_eq!( + classify_filesystem_sandbox_violation(sandbox_type, &output), + Some(FileSystemSandboxViolation { + backend, + reason: FileSystemSandboxViolationReason::SignalSyscall, + path: None, + output_snippet: String::new(), + }) + ); + } } #[test] @@ -184,6 +215,7 @@ fn converts_blocked_request_to_network_violation() { assert_eq!( NetworkSandboxViolation::from_blocked_request(&blocked), NetworkSandboxViolation { + backend: SandboxViolationBackend::ManagedNetworkProxy, host: "example.com".to_string(), reason: "not_allowed".to_string(), client: Some("curl".to_string()),