From 04892b4ceb3ba6516eddd21c3f7ed2920a977442 Mon Sep 17 00:00:00 2001 From: viyatb-oai Date: Wed, 11 Mar 2026 23:31:18 -0700 Subject: [PATCH] refactor: make bubblewrap the default Linux sandbox (#13996) ## Summary - make bubblewrap the default Linux sandbox and keep `use_legacy_landlock` as the only override - remove `use_linux_sandbox_bwrap` from feature, config, schema, and docs surfaces - update Linux sandbox selection, CLI/config plumbing, and related tests/docs to match the new default - fold in the follow-up CI fixes for request-permissions responses and Linux read-only sandbox error text --- .../app-server/src/codex_message_processor.rs | 4 +- .../app-server/tests/suite/v2/command_exec.rs | 46 ++++++++++-------- codex-rs/cli/src/debug_sandbox.rs | 5 +- codex-rs/cli/src/lib.rs | 2 +- codex-rs/cli/src/main.rs | 2 +- codex-rs/core/config.schema.json | 4 +- codex-rs/core/src/codex.rs | 16 ++----- codex-rs/core/src/connectors.rs | 3 +- codex-rs/core/src/exec.rs | 8 ++-- codex-rs/core/src/features.rs | 42 +++++----------- codex-rs/core/src/landlock.rs | 34 ++++++------- codex-rs/core/src/mcp/mod.rs | 2 +- codex-rs/core/src/mcp_connection_manager.rs | 2 +- codex-rs/core/src/sandbox_tags.rs | 16 +++---- codex-rs/core/src/sandboxing/mod.rs | 12 ++--- codex-rs/core/src/tools/js_repl/mod.rs | 4 +- codex-rs/core/src/tools/orchestrator.rs | 7 ++- codex-rs/core/src/tools/registry.rs | 8 +--- .../tools/runtimes/shell/unix_escalation.rs | 8 ++-- .../runtimes/shell/unix_escalation_tests.rs | 6 +-- codex-rs/core/src/tools/sandboxing.rs | 4 +- codex-rs/core/src/turn_metadata.rs | 48 +++++++++---------- codex-rs/core/tests/suite/approvals.rs | 19 ++++---- codex-rs/linux-sandbox/README.md | 23 +++++---- codex-rs/linux-sandbox/src/linux_run_main.rs | 34 ++++++------- .../linux-sandbox/src/linux_run_main_tests.rs | 10 ++-- .../linux-sandbox/tests/suite/landlock.rs | 26 +++++----- .../tests/suite/managed_proxy.rs | 1 - codex-rs/utils/cli/src/config_override.rs | 10 ++-- 29 files changed, 184 insertions(+), 222 deletions(-) diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index 6a22d899b..5b684a0eb 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -1723,7 +1723,7 @@ impl CodexMessageProcessor { let outgoing = self.outgoing.clone(); let request_for_task = request.clone(); let started_network_proxy_for_task = started_network_proxy; - let use_linux_sandbox_bwrap = self.config.features.enabled(Feature::UseLinuxSandboxBwrap); + let use_legacy_landlock = self.config.features.use_legacy_landlock(); let size = match size.map(crate::command_exec::terminal_size_from_protocol) { Some(Ok(size)) => Some(size), Some(Err(error)) => { @@ -1740,7 +1740,7 @@ impl CodexMessageProcessor { effective_network_sandbox_policy, sandbox_cwd.as_path(), &codex_linux_sandbox_exe, - use_linux_sandbox_bwrap, + use_legacy_landlock, ) { Ok(exec_request) => { if let Err(error) = self diff --git a/codex-rs/app-server/tests/suite/v2/command_exec.rs b/codex-rs/app-server/tests/suite/v2/command_exec.rs index c0dc140c4..ecd897eb8 100644 --- a/codex-rs/app-server/tests/suite/v2/command_exec.rs +++ b/codex-rs/app-server/tests/suite/v2/command_exec.rs @@ -710,6 +710,12 @@ async fn command_exec_process_ids_are_connection_scoped_and_disconnect_terminate let server = create_mock_responses_server_sequence_unchecked(Vec::new()).await; let codex_home = TempDir::new()?; create_config_toml(codex_home.path(), &server.uri(), "never")?; + let marker = format!( + "codex-command-exec-marker-{}", + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH)? + .as_nanos() + ); let (mut process, bind_addr) = spawn_websocket_server(codex_home.path()).await?; @@ -726,7 +732,12 @@ async fn command_exec_process_ids_are_connection_scoped_and_disconnect_terminate "command/exec", 101, Some(serde_json::json!({ - "command": ["sh", "-lc", "printf 'ready\\n%s\\n' $$; sleep 30"], + "command": [ + "python3", + "-c", + "import time; print('ready', flush=True); time.sleep(30)", + marker, + ], "processId": "shared-process", "streamStdoutStderr": true, })), @@ -737,12 +748,8 @@ async fn command_exec_process_ids_are_connection_scoped_and_disconnect_terminate assert_eq!(delta.process_id, "shared-process"); assert_eq!(delta.stream, CommandExecOutputStream::Stdout); let delta_text = String::from_utf8(STANDARD.decode(&delta.delta_base64)?)?; - let pid = delta_text - .lines() - .last() - .context("delta should include shell pid")? - .parse::() - .context("parse shell pid")?; + assert!(delta_text.contains("ready")); + wait_for_process_marker(&marker, true).await?; send_request( &mut ws2, @@ -766,12 +773,12 @@ async fn command_exec_process_ids_are_connection_scoped_and_disconnect_terminate terminate_error.error.message, "no active command/exec for process id \"shared-process\"" ); - assert!(process_is_alive(pid)?); + wait_for_process_marker(&marker, true).await?; assert_no_message(&mut ws2, Duration::from_millis(250)).await?; ws1.close(None).await?; - wait_for_process_exit(pid).await?; + wait_for_process_marker(&marker, false).await?; process .kill() @@ -855,24 +862,25 @@ async fn read_initialize_response( } } -async fn wait_for_process_exit(pid: u32) -> Result<()> { +async fn wait_for_process_marker(marker: &str, should_exist: bool) -> Result<()> { let deadline = Instant::now() + Duration::from_secs(5); loop { - if !process_is_alive(pid)? { + if process_with_marker_exists(marker)? == should_exist { return Ok(()); } if Instant::now() >= deadline { - anyhow::bail!("process {pid} was still alive after websocket disconnect"); + let expectation = if should_exist { "appear" } else { "exit" }; + anyhow::bail!("process marker {marker:?} did not {expectation} before timeout"); } sleep(Duration::from_millis(50)).await; } } -fn process_is_alive(pid: u32) -> Result { - let status = std::process::Command::new("kill") - .arg("-0") - .arg(pid.to_string()) - .status() - .context("spawn kill -0")?; - Ok(status.success()) +fn process_with_marker_exists(marker: &str) -> Result { + let output = std::process::Command::new("ps") + .args(["-axo", "command"]) + .output() + .context("spawn ps -axo command")?; + let stdout = String::from_utf8(output.stdout).context("decode ps output")?; + Ok(stdout.lines().any(|line| line.contains(marker))) } diff --git a/codex-rs/cli/src/debug_sandbox.rs b/codex-rs/cli/src/debug_sandbox.rs index 3fd435082..999a92db6 100644 --- a/codex-rs/cli/src/debug_sandbox.rs +++ b/codex-rs/cli/src/debug_sandbox.rs @@ -250,19 +250,18 @@ async fn run_command_under_sandbox( .await? } SandboxType::Landlock => { - use codex_core::features::Feature; #[expect(clippy::expect_used)] let codex_linux_sandbox_exe = config .codex_linux_sandbox_exe .expect("codex-linux-sandbox executable not found"); - let use_bwrap_sandbox = config.features.enabled(Feature::UseLinuxSandboxBwrap); + let use_legacy_landlock = config.features.use_legacy_landlock(); spawn_command_under_linux_sandbox( codex_linux_sandbox_exe, command, cwd, config.permissions.sandbox_policy.get(), sandbox_policy_cwd.as_path(), - use_bwrap_sandbox, + use_legacy_landlock, stdio_policy, network.as_ref(), env, diff --git a/codex-rs/cli/src/lib.rs b/codex-rs/cli/src/lib.rs index f71d45983..b6174efa3 100644 --- a/codex-rs/cli/src/lib.rs +++ b/codex-rs/cli/src/lib.rs @@ -32,7 +32,7 @@ pub struct LandlockCommand { #[clap(skip)] pub config_overrides: CliConfigOverrides, - /// Full command args to run under landlock. + /// Full command args to run under the Linux sandbox. #[arg(trailing_var_arg = true)] pub command: Vec, } diff --git a/codex-rs/cli/src/main.rs b/codex-rs/cli/src/main.rs index d977979b8..e44c2c624 100644 --- a/codex-rs/cli/src/main.rs +++ b/codex-rs/cli/src/main.rs @@ -239,7 +239,7 @@ enum SandboxCommand { #[clap(visible_alias = "seatbelt")] Macos(SeatbeltCommand), - /// Run a command under Landlock+seccomp (Linux only). + /// Run a command under the Linux sandbox (bubblewrap by default). #[clap(visible_alias = "landlock")] Linux(LandlockCommand), diff --git a/codex-rs/core/config.schema.json b/codex-rs/core/config.schema.json index 3b7395f48..413bccb10 100644 --- a/codex-rs/core/config.schema.json +++ b/codex-rs/core/config.schema.json @@ -480,7 +480,7 @@ "unified_exec": { "type": "boolean" }, - "use_linux_sandbox_bwrap": { + "use_legacy_landlock": { "type": "boolean" }, "voice_transcription": { @@ -1985,7 +1985,7 @@ "unified_exec": { "type": "boolean" }, - "use_linux_sandbox_bwrap": { + "use_legacy_landlock": { "type": "boolean" }, "voice_transcription": { diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index b5695a0bf..fcc00fdf4 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -1290,9 +1290,7 @@ impl Session { cwd.clone(), session_configuration.sandbox_policy.get(), session_configuration.windows_sandbox_level, - per_turn_config - .features - .enabled(Feature::UseLinuxSandboxBwrap), + per_turn_config.features.use_legacy_landlock(), )); let (current_date, timezone) = local_time_context(); TurnContext { @@ -1802,7 +1800,7 @@ impl Session { sandbox_policy: session_configuration.sandbox_policy.get().clone(), codex_linux_sandbox_exe: config.codex_linux_sandbox_exe.clone(), sandbox_cwd: session_configuration.cwd.clone(), - use_linux_sandbox_bwrap: config.features.enabled(Feature::UseLinuxSandboxBwrap), + use_legacy_landlock: config.features.use_legacy_landlock(), }; let mut required_mcp_servers: Vec = mcp_servers .iter() @@ -2275,9 +2273,7 @@ impl Session { sandbox_policy: per_turn_config.permissions.sandbox_policy.get().clone(), codex_linux_sandbox_exe: per_turn_config.codex_linux_sandbox_exe.clone(), sandbox_cwd: per_turn_config.cwd.clone(), - use_linux_sandbox_bwrap: per_turn_config - .features - .enabled(Feature::UseLinuxSandboxBwrap), + use_legacy_landlock: per_turn_config.features.use_legacy_landlock(), }; if let Err(e) = self .services @@ -3938,7 +3934,7 @@ impl Session { sandbox_policy: turn_context.sandbox_policy.get().clone(), codex_linux_sandbox_exe: turn_context.codex_linux_sandbox_exe.clone(), sandbox_cwd: turn_context.cwd.clone(), - use_linux_sandbox_bwrap: turn_context.features.enabled(Feature::UseLinuxSandboxBwrap), + use_legacy_landlock: turn_context.features.use_legacy_landlock(), }; { let mut guard = self.services.mcp_startup_cancellation_token.lock().await; @@ -5215,9 +5211,7 @@ async fn spawn_review_thread( parent_turn_context.cwd.clone(), parent_turn_context.sandbox_policy.get(), parent_turn_context.windows_sandbox_level, - parent_turn_context - .features - .enabled(Feature::UseLinuxSandboxBwrap), + parent_turn_context.features.use_legacy_landlock(), )); let review_turn_context = TurnContext { diff --git a/codex-rs/core/src/connectors.rs b/codex-rs/core/src/connectors.rs index 47d61fe98..92cc910c4 100644 --- a/codex-rs/core/src/connectors.rs +++ b/codex-rs/core/src/connectors.rs @@ -31,7 +31,6 @@ use crate::config::types::AppsConfigToml; use crate::default_client::create_client; use crate::default_client::is_first_party_chat_originator; use crate::default_client::originator; -use crate::features::Feature; use crate::mcp::CODEX_APPS_MCP_SERVER_NAME; use crate::mcp::McpManager; use crate::mcp::ToolPluginProvenance; @@ -203,7 +202,7 @@ pub async fn list_accessible_connectors_from_mcp_tools_with_options_and_status( sandbox_policy: SandboxPolicy::new_read_only_policy(), codex_linux_sandbox_exe: config.codex_linux_sandbox_exe.clone(), sandbox_cwd: env::current_dir().unwrap_or_else(|_| PathBuf::from("/")), - use_linux_sandbox_bwrap: config.features.enabled(Feature::UseLinuxSandboxBwrap), + use_legacy_landlock: config.features.use_legacy_landlock(), }; let (mcp_connection_manager, cancel_token) = McpConnectionManager::new( diff --git a/codex-rs/core/src/exec.rs b/codex-rs/core/src/exec.rs index 19d56d71d..1b9456fb8 100644 --- a/codex-rs/core/src/exec.rs +++ b/codex-rs/core/src/exec.rs @@ -185,7 +185,7 @@ pub async fn process_exec_tool_call( network_sandbox_policy: NetworkSandboxPolicy, sandbox_cwd: &Path, codex_linux_sandbox_exe: &Option, - use_linux_sandbox_bwrap: bool, + use_legacy_landlock: bool, stdout_stream: Option, ) -> Result { let exec_req = build_exec_request( @@ -195,7 +195,7 @@ pub async fn process_exec_tool_call( network_sandbox_policy, sandbox_cwd, codex_linux_sandbox_exe, - use_linux_sandbox_bwrap, + use_legacy_landlock, )?; // Route through the sandboxing module for a single, unified execution path. @@ -211,7 +211,7 @@ pub fn build_exec_request( network_sandbox_policy: NetworkSandboxPolicy, sandbox_cwd: &Path, codex_linux_sandbox_exe: &Option, - use_linux_sandbox_bwrap: bool, + use_legacy_landlock: bool, ) -> Result { let windows_sandbox_level = params.windows_sandbox_level; let enforce_managed_network = params.network.is_some(); @@ -269,7 +269,7 @@ pub fn build_exec_request( #[cfg(target_os = "macos")] macos_seatbelt_profile_extensions: None, codex_linux_sandbox_exe: codex_linux_sandbox_exe.as_ref(), - use_linux_sandbox_bwrap, + use_legacy_landlock, windows_sandbox_level, }) .map_err(CodexErr::from)?; diff --git a/codex-rs/core/src/features.rs b/codex-rs/core/src/features.rs index 76da67c32..27f27f55e 100644 --- a/codex-rs/core/src/features.rs +++ b/codex-rs/core/src/features.rs @@ -108,8 +108,9 @@ pub enum Feature { WebSearchCached, /// Legacy search-tool feature flag kept for backward compatibility. SearchTool, - /// Use the bubblewrap-based Linux sandbox pipeline. - UseLinuxSandboxBwrap, + /// Use the legacy Landlock Linux sandbox fallback instead of the default + /// bubblewrap pipeline. + UseLegacyLandlock, /// Allow the model to request approval and propose exec rules. RequestRule, /// Enable Windows sandbox (restricted token) on Windows. @@ -284,6 +285,10 @@ impl Features { self.enabled(Feature::Apps) && auth.is_some_and(CodexAuth::is_chatgpt_auth) } + pub fn use_legacy_landlock(&self) -> bool { + self.enabled(Feature::UseLegacyLandlock) + } + pub fn enable(&mut self, f: Feature) -> &mut Self { self.enabled.insert(f); self @@ -636,16 +641,9 @@ pub const FEATURES: &[FeatureSpec] = &[ default_enabled: false, }, FeatureSpec { - id: Feature::UseLinuxSandboxBwrap, - key: "use_linux_sandbox_bwrap", - #[cfg(target_os = "linux")] - stage: Stage::Experimental { - name: "Bubblewrap sandbox", - menu_description: "Try the new linux sandbox based on bubblewrap.", - announcement: "NEW: Linux bubblewrap sandbox offers stronger filesystem and network controls than Landlock alone, including keeping .git and .codex read-only inside writable workspaces. Enable it in /experimental and restart Codex to try it.", - }, - #[cfg(not(target_os = "linux"))] - stage: Stage::UnderDevelopment, + id: Feature::UseLegacyLandlock, + key: "use_legacy_landlock", + stage: Stage::Stable, default_enabled: false, }, FeatureSpec { @@ -932,24 +930,10 @@ mod tests { } } - #[cfg(target_os = "linux")] #[test] - fn use_linux_sandbox_bwrap_is_experimental_on_linux() { - assert!(matches!( - Feature::UseLinuxSandboxBwrap.stage(), - Stage::Experimental { .. } - )); - assert_eq!(Feature::UseLinuxSandboxBwrap.default_enabled(), false); - } - - #[cfg(not(target_os = "linux"))] - #[test] - fn use_linux_sandbox_bwrap_is_under_development_off_linux() { - assert_eq!( - Feature::UseLinuxSandboxBwrap.stage(), - Stage::UnderDevelopment - ); - assert_eq!(Feature::UseLinuxSandboxBwrap.default_enabled(), false); + fn use_legacy_landlock_is_stable_and_disabled_by_default() { + assert_eq!(Feature::UseLegacyLandlock.stage(), Stage::Stable); + assert_eq!(Feature::UseLegacyLandlock.default_enabled(), false); } #[test] diff --git a/codex-rs/core/src/landlock.rs b/codex-rs/core/src/landlock.rs index e90daf37f..107285488 100644 --- a/codex-rs/core/src/landlock.rs +++ b/codex-rs/core/src/landlock.rs @@ -11,7 +11,7 @@ use std::path::PathBuf; use tokio::process::Child; /// Spawn a shell tool command under the Linux sandbox helper -/// (codex-linux-sandbox), which currently uses bubblewrap for filesystem +/// (codex-linux-sandbox), which defaults to bubblewrap for filesystem /// isolation plus seccomp for network restrictions. /// /// Unlike macOS Seatbelt where we directly embed the policy text, the Linux @@ -25,7 +25,7 @@ pub async fn spawn_command_under_linux_sandbox

( command_cwd: PathBuf, sandbox_policy: &SandboxPolicy, sandbox_policy_cwd: &Path, - use_bwrap_sandbox: bool, + use_legacy_landlock: bool, stdio_policy: StdioPolicy, network: Option<&NetworkProxy>, env: HashMap, @@ -42,7 +42,7 @@ where &file_system_sandbox_policy, network_sandbox_policy, sandbox_policy_cwd, - use_bwrap_sandbox, + use_legacy_landlock, allow_network_for_proxy(false), ); let arg0 = Some("codex-linux-sandbox"); @@ -69,7 +69,7 @@ pub(crate) fn allow_network_for_proxy(enforce_managed_network: bool) -> bool { /// Converts the sandbox policies into the CLI invocation for /// `codex-linux-sandbox`. /// -/// The helper performs the actual sandboxing (bubblewrap + seccomp) after +/// The helper performs the actual sandboxing (bubblewrap by default + seccomp) after /// parsing these arguments. Policy JSON flags are emitted before helper feature /// flags so the argv order matches the helper's CLI shape. See /// `docs/linux_sandbox.md` for the Linux semantics. @@ -80,7 +80,7 @@ pub(crate) fn create_linux_sandbox_command_args_for_policies( file_system_sandbox_policy: &FileSystemSandboxPolicy, network_sandbox_policy: NetworkSandboxPolicy, sandbox_policy_cwd: &Path, - use_bwrap_sandbox: bool, + use_legacy_landlock: bool, allow_network_for_proxy: bool, ) -> Vec { let sandbox_policy_json = serde_json::to_string(sandbox_policy) @@ -104,8 +104,8 @@ pub(crate) fn create_linux_sandbox_command_args_for_policies( "--network-sandbox-policy".to_string(), network_policy_json, ]; - if use_bwrap_sandbox { - linux_cmd.push("--use-bwrap-sandbox".to_string()); + if use_legacy_landlock { + linux_cmd.push("--use-legacy-landlock".to_string()); } if allow_network_for_proxy { linux_cmd.push("--allow-network-for-proxy".to_string()); @@ -121,7 +121,7 @@ pub(crate) fn create_linux_sandbox_command_args_for_policies( pub(crate) fn create_linux_sandbox_command_args( command: Vec, sandbox_policy_cwd: &Path, - use_bwrap_sandbox: bool, + use_legacy_landlock: bool, allow_network_for_proxy: bool, ) -> Vec { let sandbox_policy_cwd = sandbox_policy_cwd @@ -130,8 +130,8 @@ pub(crate) fn create_linux_sandbox_command_args( .to_string(); let mut linux_cmd: Vec = vec!["--sandbox-policy-cwd".to_string(), sandbox_policy_cwd]; - if use_bwrap_sandbox { - linux_cmd.push("--use-bwrap-sandbox".to_string()); + if use_legacy_landlock { + linux_cmd.push("--use-legacy-landlock".to_string()); } if allow_network_for_proxy { linux_cmd.push("--allow-network-for-proxy".to_string()); @@ -153,20 +153,20 @@ mod tests { use pretty_assertions::assert_eq; #[test] - fn bwrap_flags_are_feature_gated() { + fn legacy_landlock_flag_is_included_when_requested() { let command = vec!["/bin/true".to_string()]; let cwd = Path::new("/tmp"); - let with_bwrap = create_linux_sandbox_command_args(command.clone(), cwd, true, false); + let default_bwrap = create_linux_sandbox_command_args(command.clone(), cwd, false, false); assert_eq!( - with_bwrap.contains(&"--use-bwrap-sandbox".to_string()), - true + default_bwrap.contains(&"--use-legacy-landlock".to_string()), + false ); - let without_bwrap = create_linux_sandbox_command_args(command, cwd, false, false); + let legacy_landlock = create_linux_sandbox_command_args(command, cwd, true, false); assert_eq!( - without_bwrap.contains(&"--use-bwrap-sandbox".to_string()), - false + legacy_landlock.contains(&"--use-legacy-landlock".to_string()), + true ); } diff --git a/codex-rs/core/src/mcp/mod.rs b/codex-rs/core/src/mcp/mod.rs index 39eca4b29..ed93106fe 100644 --- a/codex-rs/core/src/mcp/mod.rs +++ b/codex-rs/core/src/mcp/mod.rs @@ -304,7 +304,7 @@ pub async fn collect_mcp_snapshot(config: &Config) -> McpListToolsResponseEvent sandbox_policy: SandboxPolicy::new_read_only_policy(), codex_linux_sandbox_exe: config.codex_linux_sandbox_exe.clone(), sandbox_cwd: env::current_dir().unwrap_or_else(|_| PathBuf::from("/")), - use_linux_sandbox_bwrap: config.features.enabled(Feature::UseLinuxSandboxBwrap), + use_legacy_landlock: config.features.use_legacy_landlock(), }; let (mcp_connection_manager, cancel_token) = McpConnectionManager::new( diff --git a/codex-rs/core/src/mcp_connection_manager.rs b/codex-rs/core/src/mcp_connection_manager.rs index de8ad5143..5eb5ebc79 100644 --- a/codex-rs/core/src/mcp_connection_manager.rs +++ b/codex-rs/core/src/mcp_connection_manager.rs @@ -591,7 +591,7 @@ pub struct SandboxState { pub codex_linux_sandbox_exe: Option, pub sandbox_cwd: PathBuf, #[serde(default)] - pub use_linux_sandbox_bwrap: bool, + pub use_legacy_landlock: bool, } /// A thin wrapper around a set of running [`RmcpClient`] instances. diff --git a/codex-rs/core/src/sandbox_tags.rs b/codex-rs/core/src/sandbox_tags.rs index 56c9e0f18..4910d85f1 100644 --- a/codex-rs/core/src/sandbox_tags.rs +++ b/codex-rs/core/src/sandbox_tags.rs @@ -6,7 +6,7 @@ use codex_protocol::config_types::WindowsSandboxLevel; pub(crate) fn sandbox_tag( policy: &SandboxPolicy, windows_sandbox_level: WindowsSandboxLevel, - use_linux_sandbox_bwrap: bool, + use_legacy_landlock: bool, ) -> &'static str { if matches!(policy, SandboxPolicy::DangerFullAccess) { return "none"; @@ -18,7 +18,7 @@ pub(crate) fn sandbox_tag( { return "windows_elevated"; } - if cfg!(target_os = "linux") && use_linux_sandbox_bwrap { + if cfg!(target_os = "linux") && !use_legacy_landlock { return "linux_bubblewrap"; } @@ -38,33 +38,33 @@ mod tests { use pretty_assertions::assert_eq; #[test] - fn danger_full_access_is_untagged_even_when_bubblewrap_is_enabled() { + fn danger_full_access_is_untagged_even_when_bubblewrap_is_default() { let actual = sandbox_tag( &SandboxPolicy::DangerFullAccess, WindowsSandboxLevel::Disabled, - true, + false, ); assert_eq!(actual, "none"); } #[test] - fn external_sandbox_keeps_external_tag_when_bubblewrap_is_enabled() { + fn external_sandbox_keeps_external_tag_when_bubblewrap_is_default() { let actual = sandbox_tag( &SandboxPolicy::ExternalSandbox { network_access: NetworkAccess::Enabled, }, WindowsSandboxLevel::Disabled, - true, + false, ); assert_eq!(actual, "external"); } #[test] - fn bubblewrap_feature_sets_distinct_linux_tag() { + fn bubblewrap_default_sets_distinct_linux_tag() { let actual = sandbox_tag( &SandboxPolicy::new_read_only_policy(), WindowsSandboxLevel::Disabled, - true, + false, ); let expected = if cfg!(target_os = "linux") { "linux_bubblewrap" diff --git a/codex-rs/core/src/sandboxing/mod.rs b/codex-rs/core/src/sandboxing/mod.rs index 377ecb3db..a3a5617af 100644 --- a/codex-rs/core/src/sandboxing/mod.rs +++ b/codex-rs/core/src/sandboxing/mod.rs @@ -94,7 +94,7 @@ pub(crate) struct SandboxTransformRequest<'a> { #[cfg(target_os = "macos")] pub macos_seatbelt_profile_extensions: Option<&'a MacOsSeatbeltProfileExtensions>, pub codex_linux_sandbox_exe: Option<&'a PathBuf>, - pub use_linux_sandbox_bwrap: bool, + pub use_legacy_landlock: bool, pub windows_sandbox_level: WindowsSandboxLevel, } @@ -571,7 +571,7 @@ impl SandboxManager { #[cfg(target_os = "macos")] macos_seatbelt_profile_extensions, codex_linux_sandbox_exe, - use_linux_sandbox_bwrap, + use_legacy_landlock, windows_sandbox_level, } = request; #[cfg(not(target_os = "macos"))] @@ -653,7 +653,7 @@ impl SandboxManager { &effective_file_system_policy, effective_network_policy, sandbox_policy_cwd, - use_linux_sandbox_bwrap, + use_legacy_landlock, allow_proxy_network, ); let mut full_command = Vec::with_capacity(1 + args.len()); @@ -886,7 +886,7 @@ mod tests { #[cfg(target_os = "macos")] macos_seatbelt_profile_extensions: None, codex_linux_sandbox_exe: None, - use_linux_sandbox_bwrap: false, + use_legacy_landlock: false, windows_sandbox_level: WindowsSandboxLevel::Disabled, }) .expect("transform"); @@ -1219,7 +1219,7 @@ mod tests { #[cfg(target_os = "macos")] macos_seatbelt_profile_extensions: None, codex_linux_sandbox_exe: None, - use_linux_sandbox_bwrap: false, + use_legacy_landlock: false, windows_sandbox_level: WindowsSandboxLevel::Disabled, }) .expect("transform"); @@ -1291,7 +1291,7 @@ mod tests { #[cfg(target_os = "macos")] macos_seatbelt_profile_extensions: None, codex_linux_sandbox_exe: None, - use_linux_sandbox_bwrap: false, + use_legacy_landlock: false, windows_sandbox_level: WindowsSandboxLevel::Disabled, }) .expect("transform"); diff --git a/codex-rs/core/src/tools/js_repl/mod.rs b/codex-rs/core/src/tools/js_repl/mod.rs index 1fb3d528f..a6ce016ff 100644 --- a/codex-rs/core/src/tools/js_repl/mod.rs +++ b/codex-rs/core/src/tools/js_repl/mod.rs @@ -884,9 +884,7 @@ impl JsReplManager { #[cfg(target_os = "macos")] macos_seatbelt_profile_extensions: None, codex_linux_sandbox_exe: turn.codex_linux_sandbox_exe.as_ref(), - use_linux_sandbox_bwrap: turn - .features - .enabled(crate::features::Feature::UseLinuxSandboxBwrap), + use_legacy_landlock: turn.features.use_legacy_landlock(), windows_sandbox_level: turn.windows_sandbox_level, }) .map_err(|err| format!("failed to configure sandbox for js_repl: {err}"))?; diff --git a/codex-rs/core/src/tools/orchestrator.rs b/codex-rs/core/src/tools/orchestrator.rs index b26ea0bee..d773bd913 100644 --- a/codex-rs/core/src/tools/orchestrator.rs +++ b/codex-rs/core/src/tools/orchestrator.rs @@ -9,7 +9,6 @@ caching). use crate::error::CodexErr; use crate::error::SandboxErr; use crate::exec::ExecToolCallOutput; -use crate::features::Feature; use crate::guardian::GUARDIAN_REJECTION_MESSAGE; use crate::guardian::routes_approval_to_guardian; use crate::network_policy_decision::network_approval_context_from_payload; @@ -186,7 +185,7 @@ impl ToolOrchestrator { // Platform-specific flag gating is handled by SandboxManager::select_initial // via crate::safety::get_platform_sandbox(..). - let use_linux_sandbox_bwrap = turn_ctx.features.enabled(Feature::UseLinuxSandboxBwrap); + let use_legacy_landlock = turn_ctx.features.use_legacy_landlock(); let initial_attempt = SandboxAttempt { sandbox: initial_sandbox, policy: &turn_ctx.sandbox_policy, @@ -196,7 +195,7 @@ impl ToolOrchestrator { manager: &self.sandbox, sandbox_cwd: &turn_ctx.cwd, codex_linux_sandbox_exe: turn_ctx.codex_linux_sandbox_exe.as_ref(), - use_linux_sandbox_bwrap, + use_legacy_landlock, windows_sandbox_level: turn_ctx.windows_sandbox_level, }; @@ -318,7 +317,7 @@ impl ToolOrchestrator { manager: &self.sandbox, sandbox_cwd: &turn_ctx.cwd, codex_linux_sandbox_exe: None, - use_linux_sandbox_bwrap, + use_legacy_landlock, windows_sandbox_level: turn_ctx.windows_sandbox_level, }; diff --git a/codex-rs/core/src/tools/registry.rs b/codex-rs/core/src/tools/registry.rs index 47c2e12b6..088e3c836 100644 --- a/codex-rs/core/src/tools/registry.rs +++ b/codex-rs/core/src/tools/registry.rs @@ -4,7 +4,6 @@ use std::time::Duration; use std::time::Instant; use crate::client_common::tools::ToolSpec; -use crate::features::Feature; use crate::function_tool::FunctionCallError; use crate::memories::usage::emit_metric_for_tool_read; use crate::protocol::SandboxPolicy; @@ -174,10 +173,7 @@ impl ToolRegistry { sandbox_tag( &invocation.turn.sandbox_policy, invocation.turn.windows_sandbox_level, - invocation - .turn - .features - .enabled(Feature::UseLinuxSandboxBwrap), + invocation.turn.features.use_legacy_landlock(), ), ), ( @@ -505,7 +501,7 @@ async fn dispatch_after_tool_use_hook( sandbox: sandbox_tag( &turn.sandbox_policy, turn.windows_sandbox_level, - turn.features.enabled(Feature::UseLinuxSandboxBwrap), + turn.features.use_legacy_landlock(), ) .to_string(), sandbox_policy: sandbox_policy_tag(&turn.sandbox_policy).to_string(), 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 f0be71210..36260d59a 100644 --- a/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs +++ b/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs @@ -146,7 +146,7 @@ pub(super) async fn try_run_zsh_fork( .macos_seatbelt_profile_extensions .clone(), codex_linux_sandbox_exe: ctx.turn.codex_linux_sandbox_exe.clone(), - use_linux_sandbox_bwrap: ctx.turn.features.enabled(Feature::UseLinuxSandboxBwrap), + use_legacy_landlock: ctx.turn.features.use_legacy_landlock(), }; let main_execve_wrapper_exe = ctx .session @@ -258,7 +258,7 @@ pub(crate) async fn prepare_unified_exec_zsh_fork( .macos_seatbelt_profile_extensions .clone(), codex_linux_sandbox_exe: ctx.turn.codex_linux_sandbox_exe.clone(), - use_linux_sandbox_bwrap: ctx.turn.features.enabled(Feature::UseLinuxSandboxBwrap), + use_legacy_landlock: ctx.turn.features.use_legacy_landlock(), }; let main_execve_wrapper_exe = ctx .session @@ -855,7 +855,7 @@ struct CoreShellCommandExecutor { #[cfg_attr(not(target_os = "macos"), allow(dead_code))] macos_seatbelt_profile_extensions: Option, codex_linux_sandbox_exe: Option, - use_linux_sandbox_bwrap: bool, + use_legacy_landlock: bool, } struct PrepareSandboxedExecParams<'a> { @@ -1052,7 +1052,7 @@ impl CoreShellCommandExecutor { #[cfg(target_os = "macos")] macos_seatbelt_profile_extensions, codex_linux_sandbox_exe: self.codex_linux_sandbox_exe.as_ref(), - use_linux_sandbox_bwrap: self.use_linux_sandbox_bwrap, + use_legacy_landlock: self.use_legacy_landlock, windows_sandbox_level: self.windows_sandbox_level, })?; if let Some(network) = exec_request.network.as_ref() { diff --git a/codex-rs/core/src/tools/runtimes/shell/unix_escalation_tests.rs b/codex-rs/core/src/tools/runtimes/shell/unix_escalation_tests.rs index 4f9f31a78..a1a34cff4 100644 --- a/codex-rs/core/src/tools/runtimes/shell/unix_escalation_tests.rs +++ b/codex-rs/core/src/tools/runtimes/shell/unix_escalation_tests.rs @@ -611,7 +611,7 @@ async fn prepare_escalated_exec_turn_default_preserves_macos_seatbelt_extensions ..Default::default() }), codex_linux_sandbox_exe: None, - use_linux_sandbox_bwrap: false, + use_legacy_landlock: false, }; let prepared = executor @@ -660,7 +660,7 @@ async fn prepare_escalated_exec_permissions_preserve_macos_seatbelt_extensions() sandbox_policy_cwd: cwd.to_path_buf(), macos_seatbelt_profile_extensions: None, codex_linux_sandbox_exe: None, - use_linux_sandbox_bwrap: false, + use_legacy_landlock: false, }; let permissions = Permissions { @@ -737,7 +737,7 @@ async fn prepare_escalated_exec_permission_profile_unions_turn_and_requested_mac ..Default::default() }), codex_linux_sandbox_exe: None, - use_linux_sandbox_bwrap: false, + use_legacy_landlock: false, }; let prepared = executor diff --git a/codex-rs/core/src/tools/sandboxing.rs b/codex-rs/core/src/tools/sandboxing.rs index 922544471..c133dea7d 100644 --- a/codex-rs/core/src/tools/sandboxing.rs +++ b/codex-rs/core/src/tools/sandboxing.rs @@ -330,7 +330,7 @@ pub(crate) struct SandboxAttempt<'a> { pub(crate) manager: &'a SandboxManager, pub(crate) sandbox_cwd: &'a Path, pub codex_linux_sandbox_exe: Option<&'a std::path::PathBuf>, - pub use_linux_sandbox_bwrap: bool, + pub use_legacy_landlock: bool, pub windows_sandbox_level: codex_protocol::config_types::WindowsSandboxLevel, } @@ -353,7 +353,7 @@ impl<'a> SandboxAttempt<'a> { #[cfg(target_os = "macos")] macos_seatbelt_profile_extensions: None, codex_linux_sandbox_exe: self.codex_linux_sandbox_exe, - use_linux_sandbox_bwrap: self.use_linux_sandbox_bwrap, + use_legacy_landlock: self.use_legacy_landlock, windows_sandbox_level: self.windows_sandbox_level, }) } diff --git a/codex-rs/core/src/turn_metadata.rs b/codex-rs/core/src/turn_metadata.rs index 0814897d9..0ddaad918 100644 --- a/codex-rs/core/src/turn_metadata.rs +++ b/codex-rs/core/src/turn_metadata.rs @@ -132,16 +132,11 @@ impl TurnMetadataState { cwd: PathBuf, sandbox_policy: &SandboxPolicy, windows_sandbox_level: WindowsSandboxLevel, - use_linux_sandbox_bwrap: bool, + use_legacy_landlock: bool, ) -> Self { let repo_root = get_git_repo_root(&cwd).map(|root| root.to_string_lossy().into_owned()); let sandbox = Some( - sandbox_tag( - sandbox_policy, - windows_sandbox_level, - use_linux_sandbox_bwrap, - ) - .to_string(), + sandbox_tag(sandbox_policy, windows_sandbox_level, use_legacy_landlock).to_string(), ); let base_metadata = build_turn_metadata_bag(Some(turn_id), sandbox, None, None); let base_header = base_metadata @@ -300,19 +295,19 @@ mod tests { } #[test] - fn turn_metadata_state_respects_linux_bubblewrap_toggle() { + fn turn_metadata_state_respects_legacy_landlock_flag() { let temp_dir = TempDir::new().expect("temp dir"); let cwd = temp_dir.path().to_path_buf(); let sandbox_policy = SandboxPolicy::new_read_only_policy(); - let without_bubblewrap = TurnMetadataState::new( + let default_bubblewrap = TurnMetadataState::new( "turn-a".to_string(), cwd.clone(), &sandbox_policy, WindowsSandboxLevel::Disabled, false, ); - let with_bubblewrap = TurnMetadataState::new( + let legacy_landlock = TurnMetadataState::new( "turn-b".to_string(), cwd, &sandbox_policy, @@ -320,30 +315,33 @@ mod tests { true, ); - let without_bubblewrap_header = without_bubblewrap + let default_bubblewrap_header = default_bubblewrap .current_header_value() - .expect("without_bubblewrap_header"); - let with_bubblewrap_header = with_bubblewrap + .expect("default_bubblewrap_header"); + let legacy_landlock_header = legacy_landlock .current_header_value() - .expect("with_bubblewrap_header"); + .expect("legacy_landlock_header"); - let without_bubblewrap_json: Value = - serde_json::from_str(&without_bubblewrap_header).expect("without_bubblewrap_json"); - let with_bubblewrap_json: Value = - serde_json::from_str(&with_bubblewrap_header).expect("with_bubblewrap_json"); + let default_bubblewrap_json: Value = + serde_json::from_str(&default_bubblewrap_header).expect("default_bubblewrap_json"); + let legacy_landlock_json: Value = + serde_json::from_str(&legacy_landlock_header).expect("legacy_landlock_json"); - let without_bubblewrap_sandbox = without_bubblewrap_json + let default_bubblewrap_sandbox = default_bubblewrap_json .get("sandbox") .and_then(Value::as_str); - let with_bubblewrap_sandbox = with_bubblewrap_json.get("sandbox").and_then(Value::as_str); + let legacy_landlock_sandbox = legacy_landlock_json.get("sandbox").and_then(Value::as_str); - let expected_with_bubblewrap = - sandbox_tag(&sandbox_policy, WindowsSandboxLevel::Disabled, true); - assert_eq!(with_bubblewrap_sandbox, Some(expected_with_bubblewrap)); + let expected_default_bubblewrap = + sandbox_tag(&sandbox_policy, WindowsSandboxLevel::Disabled, false); + assert_eq!( + default_bubblewrap_sandbox, + Some(expected_default_bubblewrap) + ); if cfg!(target_os = "linux") { - assert_eq!(with_bubblewrap_sandbox, Some("linux_bubblewrap")); - assert_ne!(with_bubblewrap_sandbox, without_bubblewrap_sandbox); + assert_eq!(default_bubblewrap_sandbox, Some("linux_bubblewrap")); + assert_ne!(default_bubblewrap_sandbox, legacy_landlock_sandbox); } } } diff --git a/codex-rs/core/tests/suite/approvals.rs b/codex-rs/core/tests/suite/approvals.rs index abeb792af..66c30f614 100644 --- a/codex-rs/core/tests/suite/approvals.rs +++ b/codex-rs/core/tests/suite/approvals.rs @@ -1321,7 +1321,7 @@ fn scenarios() -> Vec { expectation: Expectation::FileNotCreated { target: TargetPath::Workspace("ro_never.txt"), message_contains: if cfg!(target_os = "linux") { - &["Permission denied"] + &["Permission denied|Read-only file system"] } else { &[ "Permission denied|Operation not permitted|operation not permitted|\ @@ -1468,7 +1468,7 @@ fn scenarios() -> Vec { expectation: Expectation::FileNotCreated { target: TargetPath::OutsideWorkspace("ww_never.txt"), message_contains: if cfg!(target_os = "linux") { - &["Permission denied"] + &["Permission denied|Read-only file system"] } else { &[ "Permission denied|Operation not permitted|operation not permitted|\ @@ -2290,20 +2290,17 @@ allow_local_binding = true test.config.permissions.network.is_some(), "expected managed network proxy config to be present" ); - let runtime_proxy = test - .session_configured + test.session_configured .network_proxy .as_ref() .expect("expected runtime managed network proxy addresses"); - let proxy_addr = runtime_proxy.http_addr.as_str(); let call_id_first = "allow-network-first"; - // Use the same urllib-based pattern as the other network integration tests, - // but point it at the runtime proxy directly so the blocked host reliably - // produces a network approval request without relying on curl. - let fetch_command = format!( - "python3 -c \"import urllib.request; proxy = urllib.request.ProxyHandler({{'http': 'http://{proxy_addr}'}}); opener = urllib.request.build_opener(proxy); print('OK:' + opener.open('http://codex-network-test.invalid', timeout=30).read().decode(errors='replace'))\"" - ); + // Use urllib without overriding proxy settings so managed-network sessions + // continue to exercise the env-based proxy routing path under bubblewrap. + let fetch_command = + "python3 -c \"import urllib.request; opener = urllib.request.build_opener(urllib.request.ProxyHandler()); print('OK:' + opener.open('http://codex-network-test.invalid', timeout=30).read().decode(errors='replace'))\"" + .to_string(); let first_event = shell_event( call_id_first, &fetch_command, diff --git a/codex-rs/linux-sandbox/README.md b/codex-rs/linux-sandbox/README.md index 32d8d99f0..5b02be8bd 100644 --- a/codex-rs/linux-sandbox/README.md +++ b/codex-rs/linux-sandbox/README.md @@ -12,29 +12,28 @@ into this binary. **Current Behavior** - Legacy Landlock + mount protections remain available as the legacy pipeline. -- The bubblewrap pipeline is standardized on the vendored path. -- During rollout, the bubblewrap pipeline is gated by the temporary feature - flag `use_linux_sandbox_bwrap` (CLI `-c` alias for - `features.use_linux_sandbox_bwrap`; legacy remains default when off). -- When enabled, the bubblewrap pipeline applies `PR_SET_NO_NEW_PRIVS` and a +- The default Linux sandbox pipeline is bubblewrap on the vendored path. +- Set `features.use_legacy_landlock = true` (or CLI `-c use_legacy_landlock=true`) + to force the legacy Landlock fallback. +- When the default bubblewrap pipeline is active, it applies `PR_SET_NO_NEW_PRIVS` and a seccomp network filter in-process. -- When enabled, the filesystem is read-only by default via `--ro-bind / /`. -- When enabled, writable roots are layered with `--bind `. -- When enabled, protected subpaths under writable roots (for example `.git`, +- When the default bubblewrap pipeline is active, the filesystem is read-only by default via `--ro-bind / /`. +- When the default bubblewrap pipeline is active, writable roots are layered with `--bind `. +- When the default bubblewrap pipeline is active, protected subpaths under writable roots (for example `.git`, resolved `gitdir:`, and `.codex`) are re-applied as read-only via `--ro-bind`. -- When enabled, symlink-in-path and non-existent protected paths inside +- When the default bubblewrap pipeline is active, symlink-in-path and non-existent protected paths inside writable roots are blocked by mounting `/dev/null` on the symlink or first missing component. -- When enabled, the helper explicitly isolates the user namespace via +- When the default bubblewrap pipeline is active, the helper explicitly isolates the user namespace via `--unshare-user` and the PID namespace via `--unshare-pid`. -- When enabled and network is restricted without proxy routing, the helper also +- When the default bubblewrap pipeline is active and network is restricted without proxy routing, the helper also isolates the network namespace via `--unshare-net`. - In managed proxy mode, the helper uses `--unshare-net` plus an internal TCP->UDS->TCP routing bridge so tool traffic reaches only configured proxy endpoints. - In managed proxy mode, after the bridge is live, seccomp blocks new AF_UNIX/socketpair creation for the user command. -- When enabled, it mounts a fresh `/proc` via `--proc /proc` by default, but +- When the default bubblewrap pipeline is active, it mounts a fresh `/proc` via `--proc /proc` by default, but you can skip this in restrictive container environments with `--no-proc`. **Notes** diff --git a/codex-rs/linux-sandbox/src/linux_run_main.rs b/codex-rs/linux-sandbox/src/linux_run_main.rs index e6304d2d6..9a5a4c738 100644 --- a/codex-rs/linux-sandbox/src/linux_run_main.rs +++ b/codex-rs/linux-sandbox/src/linux_run_main.rs @@ -22,7 +22,8 @@ use codex_protocol::protocol::SandboxPolicy; /// CLI surface for the Linux sandbox helper. /// /// The type name remains `LandlockCommand` for compatibility with existing -/// wiring, but the filesystem sandbox now uses bubblewrap. +/// wiring, but bubblewrap is now the default filesystem sandbox and Landlock +/// is the legacy fallback. pub struct LandlockCommand { /// It is possible that the cwd used in the context of the sandbox policy /// is different from the cwd of the process to spawn. @@ -42,11 +43,11 @@ pub struct LandlockCommand { #[arg(long = "network-sandbox-policy", hide = true)] pub network_sandbox_policy: Option, - /// Opt-in: use the bubblewrap-based Linux sandbox pipeline. + /// Opt-in: use the legacy Landlock Linux sandbox fallback. /// - /// When not set, we fall back to the legacy Landlock + mount pipeline. - #[arg(long = "use-bwrap-sandbox", hide = true, default_value_t = false)] - pub use_bwrap_sandbox: bool, + /// When not set, the helper uses the default bubblewrap pipeline. + #[arg(long = "use-legacy-landlock", hide = true, default_value_t = false)] + pub use_legacy_landlock: bool, /// Internal: apply seccomp and `no_new_privs` in the already-sandboxed /// process, then exec the user command. @@ -92,7 +93,7 @@ pub fn run_main() -> ! { sandbox_policy, file_system_sandbox_policy, network_sandbox_policy, - use_bwrap_sandbox, + use_legacy_landlock, apply_seccomp_then_exec, allow_network_for_proxy, proxy_route_spec, @@ -103,7 +104,7 @@ pub fn run_main() -> ! { if command.is_empty() { panic!("No command specified to execute."); } - ensure_inner_stage_mode_is_valid(apply_seccomp_then_exec, use_bwrap_sandbox); + ensure_inner_stage_mode_is_valid(apply_seccomp_then_exec, use_legacy_landlock); let EffectiveSandboxPolicies { sandbox_policy, file_system_sandbox_policy, @@ -154,7 +155,7 @@ pub fn run_main() -> ! { exec_or_panic(command); } - if use_bwrap_sandbox { + if !use_legacy_landlock { // Outer stage: bubblewrap first, then re-enter this binary in the // sandboxed environment to apply seccomp. This path never falls back // to legacy Landlock on failure. @@ -171,7 +172,6 @@ pub fn run_main() -> ! { sandbox_policy: &sandbox_policy, file_system_sandbox_policy: &file_system_sandbox_policy, network_sandbox_policy, - use_bwrap_sandbox, allow_network_for_proxy, proxy_route_spec, command, @@ -256,9 +256,9 @@ fn resolve_sandbox_policies( } } -fn ensure_inner_stage_mode_is_valid(apply_seccomp_then_exec: bool, use_bwrap_sandbox: bool) { - if apply_seccomp_then_exec && !use_bwrap_sandbox { - panic!("--apply-seccomp-then-exec requires --use-bwrap-sandbox"); +fn ensure_inner_stage_mode_is_valid(apply_seccomp_then_exec: bool, use_legacy_landlock: bool) { + if apply_seccomp_then_exec && use_legacy_landlock { + panic!("--apply-seccomp-then-exec is incompatible with --use-legacy-landlock"); } } @@ -280,7 +280,8 @@ fn run_bwrap_with_proc_fallback( network_mode, ) { - eprintln!("codex-linux-sandbox: bwrap could not mount /proc; retrying with --no-proc"); + // Keep the retry silent so sandbox-internal diagnostics do not leak into the + // child process stderr stream. mount_proc = false; } @@ -470,7 +471,6 @@ struct InnerSeccompCommandArgs<'a> { sandbox_policy: &'a SandboxPolicy, file_system_sandbox_policy: &'a FileSystemSandboxPolicy, network_sandbox_policy: NetworkSandboxPolicy, - use_bwrap_sandbox: bool, allow_network_for_proxy: bool, proxy_route_spec: Option, command: Vec, @@ -483,7 +483,6 @@ fn build_inner_seccomp_command(args: InnerSeccompCommandArgs<'_>) -> Vec sandbox_policy, file_system_sandbox_policy, network_sandbox_policy, - use_bwrap_sandbox, allow_network_for_proxy, proxy_route_spec, command, @@ -516,10 +515,7 @@ fn build_inner_seccomp_command(args: InnerSeccompCommandArgs<'_>) -> Vec "--network-sandbox-policy".to_string(), network_policy_json, ]; - if use_bwrap_sandbox { - inner.push("--use-bwrap-sandbox".to_string()); - inner.push("--apply-seccomp-then-exec".to_string()); - } + inner.push("--apply-seccomp-then-exec".to_string()); if allow_network_for_proxy { inner.push("--allow-network-for-proxy".to_string()); let proxy_route_spec = proxy_route_spec diff --git a/codex-rs/linux-sandbox/src/linux_run_main_tests.rs b/codex-rs/linux-sandbox/src/linux_run_main_tests.rs index a1adf65b1..dec1c1214 100644 --- a/codex-rs/linux-sandbox/src/linux_run_main_tests.rs +++ b/codex-rs/linux-sandbox/src/linux_run_main_tests.rs @@ -127,7 +127,6 @@ fn managed_proxy_inner_command_includes_route_spec() { sandbox_policy: &sandbox_policy, file_system_sandbox_policy: &FileSystemSandboxPolicy::from(&sandbox_policy), network_sandbox_policy: NetworkSandboxPolicy::Restricted, - use_bwrap_sandbox: true, allow_network_for_proxy: true, proxy_route_spec: Some("{\"routes\":[]}".to_string()), command: vec!["/bin/true".to_string()], @@ -145,7 +144,6 @@ fn inner_command_includes_split_policy_flags() { sandbox_policy: &sandbox_policy, file_system_sandbox_policy: &FileSystemSandboxPolicy::from(&sandbox_policy), network_sandbox_policy: NetworkSandboxPolicy::Restricted, - use_bwrap_sandbox: true, allow_network_for_proxy: false, proxy_route_spec: None, command: vec!["/bin/true".to_string()], @@ -163,7 +161,6 @@ fn non_managed_inner_command_omits_route_spec() { sandbox_policy: &sandbox_policy, file_system_sandbox_policy: &FileSystemSandboxPolicy::from(&sandbox_policy), network_sandbox_policy: NetworkSandboxPolicy::Restricted, - use_bwrap_sandbox: true, allow_network_for_proxy: false, proxy_route_spec: None, command: vec!["/bin/true".to_string()], @@ -181,7 +178,6 @@ fn managed_proxy_inner_command_requires_route_spec() { sandbox_policy: &sandbox_policy, file_system_sandbox_policy: &FileSystemSandboxPolicy::from(&sandbox_policy), network_sandbox_policy: NetworkSandboxPolicy::Restricted, - use_bwrap_sandbox: true, allow_network_for_proxy: true, proxy_route_spec: None, command: vec!["/bin/true".to_string()], @@ -244,8 +240,8 @@ fn resolve_sandbox_policies_rejects_partial_split_policies() { } #[test] -fn apply_seccomp_then_exec_without_bwrap_panics() { - let result = std::panic::catch_unwind(|| ensure_inner_stage_mode_is_valid(true, false)); +fn apply_seccomp_then_exec_with_legacy_landlock_panics() { + let result = std::panic::catch_unwind(|| ensure_inner_stage_mode_is_valid(true, true)); assert!(result.is_err()); } @@ -253,5 +249,5 @@ fn apply_seccomp_then_exec_without_bwrap_panics() { fn valid_inner_stage_modes_do_not_panic() { ensure_inner_stage_mode_is_valid(false, false); ensure_inner_stage_mode_is_valid(false, true); - ensure_inner_stage_mode_is_valid(true, true); + ensure_inner_stage_mode_is_valid(true, false); } diff --git a/codex-rs/linux-sandbox/tests/suite/landlock.rs b/codex-rs/linux-sandbox/tests/suite/landlock.rs index 8e2f5ef41..774fb4f17 100644 --- a/codex-rs/linux-sandbox/tests/suite/landlock.rs +++ b/codex-rs/linux-sandbox/tests/suite/landlock.rs @@ -72,7 +72,7 @@ async fn run_cmd_result_with_writable_roots( cmd: &[&str], writable_roots: &[PathBuf], timeout_ms: u64, - use_bwrap_sandbox: bool, + use_legacy_landlock: bool, network_access: bool, ) -> Result { let sandbox_policy = SandboxPolicy::WorkspaceWrite { @@ -96,7 +96,7 @@ async fn run_cmd_result_with_writable_roots( file_system_sandbox_policy, network_sandbox_policy, timeout_ms, - use_bwrap_sandbox, + use_legacy_landlock, ) .await } @@ -108,7 +108,7 @@ async fn run_cmd_result_with_policies( file_system_sandbox_policy: FileSystemSandboxPolicy, network_sandbox_policy: NetworkSandboxPolicy, timeout_ms: u64, - use_bwrap_sandbox: bool, + use_legacy_landlock: bool, ) -> Result { let cwd = std::env::current_dir().expect("cwd should exist"); let sandbox_cwd = cwd.clone(); @@ -133,7 +133,7 @@ async fn run_cmd_result_with_policies( network_sandbox_policy, sandbox_cwd.as_path(), &codex_linux_sandbox_exe, - use_bwrap_sandbox, + use_legacy_landlock, None, ) .await @@ -155,7 +155,7 @@ async fn should_skip_bwrap_tests() -> bool { &["bash", "-lc", "true"], &[], NETWORK_TIMEOUT_MS, - true, + false, true, ) .await @@ -216,7 +216,7 @@ async fn test_dev_null_write() { // We have seen timeouts when running this test in CI on GitHub, // so we are using a generous timeout until we can diagnose further. LONG_TIMEOUT_MS, - true, + false, true, ) .await @@ -240,7 +240,7 @@ async fn bwrap_populates_minimal_dev_nodes() { ], &[], LONG_TIMEOUT_MS, - true, + false, true, ) .await @@ -278,7 +278,7 @@ async fn bwrap_preserves_writable_dev_shm_bind_mount() { ], &[PathBuf::from("/dev/shm")], LONG_TIMEOUT_MS, - true, + false, true, ) .await @@ -442,7 +442,7 @@ async fn sandbox_blocks_git_and_codex_writes_inside_writable_root() { ], &[tmpdir.path().to_path_buf()], LONG_TIMEOUT_MS, - true, + false, true, ) .await, @@ -458,7 +458,7 @@ async fn sandbox_blocks_git_and_codex_writes_inside_writable_root() { ], &[tmpdir.path().to_path_buf()], LONG_TIMEOUT_MS, - true, + false, true, ) .await, @@ -495,7 +495,7 @@ async fn sandbox_blocks_codex_symlink_replacement_attack() { ], &[tmpdir.path().to_path_buf()], LONG_TIMEOUT_MS, - true, + false, true, ) .await, @@ -548,7 +548,7 @@ async fn sandbox_blocks_explicit_split_policy_carveouts_under_bwrap() { file_system_sandbox_policy, NetworkSandboxPolicy::Enabled, LONG_TIMEOUT_MS, - true, + false, ) .await, "explicit split-policy carveout should be denied under bubblewrap", @@ -599,7 +599,7 @@ async fn sandbox_blocks_root_read_carveouts_under_bwrap() { file_system_sandbox_policy, NetworkSandboxPolicy::Enabled, LONG_TIMEOUT_MS, - true, + false, ) .await, "root-read carveout should be denied under bubblewrap", diff --git a/codex-rs/linux-sandbox/tests/suite/managed_proxy.rs b/codex-rs/linux-sandbox/tests/suite/managed_proxy.rs index e27930cdb..4d7aa2ac7 100644 --- a/codex-rs/linux-sandbox/tests/suite/managed_proxy.rs +++ b/codex-rs/linux-sandbox/tests/suite/managed_proxy.rs @@ -133,7 +133,6 @@ async fn run_linux_sandbox_direct( cwd.to_string_lossy().to_string(), "--sandbox-policy".to_string(), policy_json, - "--use-bwrap-sandbox".to_string(), ]; if allow_network_for_proxy { args.push("--allow-network-for-proxy".to_string()); diff --git a/codex-rs/utils/cli/src/config_override.rs b/codex-rs/utils/cli/src/config_override.rs index 1592563a0..41be3ca6b 100644 --- a/codex-rs/utils/cli/src/config_override.rs +++ b/codex-rs/utils/cli/src/config_override.rs @@ -89,8 +89,8 @@ impl CliConfigOverrides { } fn canonicalize_override_key(key: &str) -> String { - if key == "use_linux_sandbox_bwrap" { - "features.use_linux_sandbox_bwrap".to_string() + if key == "use_legacy_landlock" { + "features.use_legacy_landlock".to_string() } else { key.to_string() } @@ -181,12 +181,12 @@ mod tests { } #[test] - fn canonicalizes_use_linux_sandbox_bwrap_alias() { + fn canonicalizes_use_legacy_landlock_alias() { let overrides = CliConfigOverrides { - raw_overrides: vec!["use_linux_sandbox_bwrap=true".to_string()], + raw_overrides: vec!["use_legacy_landlock=true".to_string()], }; let parsed = overrides.parse_overrides().expect("parse_overrides"); - assert_eq!(parsed[0].0.as_str(), "features.use_linux_sandbox_bwrap"); + assert_eq!(parsed[0].0.as_str(), "features.use_legacy_landlock"); assert_eq!(parsed[0].1.as_bool(), Some(true)); }