diff --git a/codex-rs/cli/BUILD.bazel b/codex-rs/cli/BUILD.bazel index a8a97cef0..faa862747 100644 --- a/codex-rs/cli/BUILD.bazel +++ b/codex-rs/cli/BUILD.bazel @@ -3,6 +3,9 @@ load("//:defs.bzl", "MACOS_WEBRTC_RUSTC_LINK_FLAGS", "codex_rust_crate", "multip codex_rust_crate( name = "cli", crate_name = "codex_cli", + extra_binaries = [ + "//codex-rs/bwrap:bwrap", + ], rustc_flags_extra = MACOS_WEBRTC_RUSTC_LINK_FLAGS, ) diff --git a/codex-rs/cli/src/debug_sandbox.rs b/codex-rs/cli/src/debug_sandbox.rs index 6d59bf7d2..a1931aced 100644 --- a/codex-rs/cli/src/debug_sandbox.rs +++ b/codex-rs/cli/src/debug_sandbox.rs @@ -258,6 +258,9 @@ async fn run_command_under_sandbox( let network = network_proxy .as_ref() .map(codex_core::config::StartedNetworkProxy::proxy); + // 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, @@ -278,7 +281,7 @@ async fn run_command_under_sandbox( file_system_sandbox_policy: &file_system_sandbox_policy, network_sandbox_policy, sandbox_policy_cwd: sandbox_policy_cwd.as_path(), - enforce_managed_network: false, + enforce_managed_network, network: network.as_ref(), extra_allow_unix_sockets: allow_unix_sockets, }); @@ -311,7 +314,7 @@ async fn run_command_under_sandbox( &runtime_permission_profile, sandbox_policy_cwd.as_path(), use_legacy_landlock, - allow_network_for_proxy(managed_network_requirements_enabled), + allow_network_for_proxy(enforce_managed_network), ); spawn_debug_sandbox_child( codex_linux_sandbox_exe, diff --git a/codex-rs/cli/tests/sandbox_network_proxy.rs b/codex-rs/cli/tests/sandbox_network_proxy.rs new file mode 100644 index 000000000..22bdcce49 --- /dev/null +++ b/codex-rs/cli/tests/sandbox_network_proxy.rs @@ -0,0 +1,70 @@ +#![cfg(target_os = "linux")] + +use std::net::TcpListener; + +use anyhow::Result; +use tempfile::TempDir; + +const BWRAP_UNAVAILABLE_ERR: &str = "bubblewrap is unavailable"; + +#[test] +fn sandbox_with_network_proxy_blocks_direct_loopback_access() -> Result<()> { + let codex_home = TempDir::new()?; + let listener = TcpListener::bind("127.0.0.2:0")?; + let port = listener.local_addr()?.port(); + std::fs::write( + codex_home.path().join("config.toml"), + r#" +default_permissions = "network-test" + +[features] +network_proxy = true +use_legacy_landlock = true + +[permissions.network-test] +extends = ":workspace" + +[permissions.network-test.network] +enabled = true +mode = "full" +"#, + )?; + + let url = format!("http://127.0.0.2:{port}/"); + let output = std::process::Command::new(codex_utils_cargo_bin::cargo_bin("codex")?) + .env("CODEX_HOME", codex_home.path()) + .args([ + "sandbox", + "--permissions-profile", + "network-test", + "--", + "curl", + "--noproxy", + "*", + "--silent", + "--show-error", + "--connect-timeout", + "1", + "--max-time", + "2", + url.as_str(), + ]) + .output()?; + + let stderr = String::from_utf8_lossy(&output.stderr); + if stderr.contains(BWRAP_UNAVAILABLE_ERR) { + eprintln!("skipping network proxy sandbox test: bubblewrap is unavailable"); + return Ok(()); + } + + assert_eq!( + output.status.code(), + Some(7), + "expected direct loopback access to be blocked; status={:?}; stdout={}; stderr={}", + output.status.code(), + String::from_utf8_lossy(&output.stdout), + stderr, + ); + + Ok(()) +} diff --git a/codex-rs/sandboxing/src/landlock.rs b/codex-rs/sandboxing/src/landlock.rs index 0ff3f6977..819fc9365 100644 --- a/codex-rs/sandboxing/src/landlock.rs +++ b/codex-rs/sandboxing/src/landlock.rs @@ -47,7 +47,8 @@ pub fn create_linux_sandbox_command_args_for_permission_profile( "--permission-profile".to_string(), permission_profile_json, ]; - if use_legacy_landlock { + // Proxy-only networking requires bubblewrap's isolated network namespace. + if use_legacy_landlock && !allow_network_for_proxy { linux_cmd.push("--use-legacy-landlock".to_string()); } if allow_network_for_proxy { @@ -83,7 +84,8 @@ fn create_linux_sandbox_command_args( "--command-cwd".to_string(), command_cwd, ]; - if use_legacy_landlock { + // Proxy-only networking requires bubblewrap's isolated network namespace. + if use_legacy_landlock && !allow_network_for_proxy { linux_cmd.push("--use-legacy-landlock".to_string()); } if allow_network_for_proxy { diff --git a/codex-rs/sandboxing/src/landlock_tests.rs b/codex-rs/sandboxing/src/landlock_tests.rs index 14b1c047e..d0bbab840 100644 --- a/codex-rs/sandboxing/src/landlock_tests.rs +++ b/codex-rs/sandboxing/src/landlock_tests.rs @@ -33,14 +33,16 @@ fn legacy_landlock_flag_is_included_when_requested() { } #[test] -fn proxy_flag_is_included_when_requested() { +fn proxy_flag_takes_precedence_over_legacy_landlock() { let command = vec!["/bin/true".to_string()]; let command_cwd = Path::new("/tmp/link"); let cwd = Path::new("/tmp"); + let permission_profile = PermissionProfile::read_only(); - let args = create_linux_sandbox_command_args( + let args = create_linux_sandbox_command_args_for_permission_profile( command, command_cwd, + &permission_profile, cwd, /*use_legacy_landlock*/ true, /*allow_network_for_proxy*/ true, @@ -49,6 +51,7 @@ fn proxy_flag_is_included_when_requested() { args.contains(&"--allow-network-for-proxy".to_string()), true ); + assert_eq!(args.contains(&"--use-legacy-landlock".to_string()), false); } #[test] diff --git a/codex-rs/sandboxing/src/manager.rs b/codex-rs/sandboxing/src/manager.rs index 39ef358f8..682e8e3dc 100644 --- a/codex-rs/sandboxing/src/manager.rs +++ b/codex-rs/sandboxing/src/manager.rs @@ -338,8 +338,8 @@ fn ensure_linux_bubblewrap_is_supported( allow_network_for_proxy: bool, is_wsl1: bool, ) -> Result<(), SandboxTransformError> { - let requires_bubblewrap = !use_legacy_landlock - && (!file_system_sandbox_policy.has_full_disk_write_access() || allow_network_for_proxy); + let requires_bubblewrap = allow_network_for_proxy + || (!use_legacy_landlock && !file_system_sandbox_policy.has_full_disk_write_access()); if is_wsl1 && requires_bubblewrap { return Err(SandboxTransformError::Wsl1UnsupportedForBubblewrap); } diff --git a/codex-rs/sandboxing/src/manager_tests.rs b/codex-rs/sandboxing/src/manager_tests.rs index fede00fe1..58138ad77 100644 --- a/codex-rs/sandboxing/src/manager_tests.rs +++ b/codex-rs/sandboxing/src/manager_tests.rs @@ -342,6 +342,15 @@ fn wsl1_rejects_linux_bubblewrap_path() { ), Err(super::SandboxTransformError::Wsl1UnsupportedForBubblewrap) )); + assert!(matches!( + super::ensure_linux_bubblewrap_is_supported( + &FileSystemSandboxPolicy::unrestricted(), + /*use_legacy_landlock*/ true, + /*allow_network_for_proxy*/ true, + /*is_wsl1*/ true, + ), + Err(super::SandboxTransformError::Wsl1UnsupportedForBubblewrap) + )); } #[cfg(target_os = "linux")]