From 6c1f39a7ca8453ba630aeb624657357b8de4f24b Mon Sep 17 00:00:00 2001 From: viyatb-oai Date: Mon, 16 Mar 2026 22:30:41 -0700 Subject: [PATCH 01/11] fix(linux-sandbox): preserve detached children --- codex-rs/linux-sandbox/src/bwrap.rs | 9 ++-- .../linux-sandbox/src/linux_run_main_tests.rs | 1 - .../linux-sandbox/tests/suite/landlock.rs | 52 +++++++++++++++++++ 3 files changed, 57 insertions(+), 5 deletions(-) diff --git a/codex-rs/linux-sandbox/src/bwrap.rs b/codex-rs/linux-sandbox/src/bwrap.rs index 23e503e6baef..a4416d0fe659 100644 --- a/codex-rs/linux-sandbox/src/bwrap.rs +++ b/codex-rs/linux-sandbox/src/bwrap.rs @@ -123,13 +123,14 @@ pub(crate) fn create_bwrap_command_args( fn create_bwrap_flags_full_filesystem(command: Vec, options: BwrapOptions) -> BwrapArgs { let mut args = vec![ "--new-session".to_string(), - "--die-with-parent".to_string(), "--bind".to_string(), "/".to_string(), "/".to_string(), // Always enter a fresh user namespace so root inside a container does // not need ambient CAP_SYS_ADMIN to create the remaining namespaces. "--unshare-user".to_string(), + // Preserve the PID namespace even when the initial tool process exits + // so detached descendants keep their sandboxed `/proc` view. "--unshare-pid".to_string(), ]; if options.network_mode.should_unshare_network() { @@ -162,12 +163,13 @@ fn create_bwrap_flags( let normalized_command_cwd = normalize_command_cwd_for_bwrap(command_cwd); let mut args = Vec::new(); args.push("--new-session".to_string()); - args.push("--die-with-parent".to_string()); args.extend(filesystem_args); // Request a user namespace explicitly rather than relying on bubblewrap's // auto-enable behavior, which is skipped when the caller runs as uid 0. args.push("--unshare-user".to_string()); - // Isolate the PID namespace. + // Keep the PID namespace, but do not tie the sandbox lifecycle to + // bubblewrap's parent process. Detached children should be able to + // survive after the initial one-shot tool process exits. args.push("--unshare-pid".to_string()); if options.network_mode.should_unshare_network() { args.push("--unshare-net".to_string()); @@ -660,7 +662,6 @@ mod tests { args.args, vec![ "--new-session".to_string(), - "--die-with-parent".to_string(), "--bind".to_string(), "/".to_string(), "/".to_string(), 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 b42d7e552a55..c87ff8beca4b 100644 --- a/codex-rs/linux-sandbox/src/linux_run_main_tests.rs +++ b/codex-rs/linux-sandbox/src/linux_run_main_tests.rs @@ -56,7 +56,6 @@ fn inserts_bwrap_argv0_before_command_separator() { vec![ "bwrap".to_string(), "--new-session".to_string(), - "--die-with-parent".to_string(), "--ro-bind".to_string(), "/".to_string(), "/".to_string(), diff --git a/codex-rs/linux-sandbox/tests/suite/landlock.rs b/codex-rs/linux-sandbox/tests/suite/landlock.rs index 05e06c2449fa..7c1dd545323e 100644 --- a/codex-rs/linux-sandbox/tests/suite/landlock.rs +++ b/codex-rs/linux-sandbox/tests/suite/landlock.rs @@ -310,6 +310,58 @@ async fn test_writable_root() { .await; } +#[tokio::test] +async fn detached_children_survive_parent_exit_under_bwrap() { + if should_skip_bwrap_tests().await { + eprintln!("skipping bwrap test: bwrap sandbox prerequisites are unavailable"); + return; + } + + let tempdir = tempfile::tempdir().expect("tempdir"); + let log_path = tempdir.path().join("detached.log"); + let pid_path = tempdir.path().join("detached.pid"); + let command = format!( + "nohup sh -c 'echo \"$$\" > \"{pid}\"; while true; do printf x >> \"{log}\"; sleep 0.2; done' >/dev/null 2>&1 0, "detached child should write to the log"); + + tokio::time::sleep(std::time::Duration::from_millis(700)).await; + let second_len = std::fs::metadata(&log_path) + .expect("detached child log file should still exist") + .len(); + assert!( + second_len > first_len, + "detached child should keep writing after the parent exits", + ); + + if let Ok(pid) = std::fs::read_to_string(&pid_path) + && let Ok(pid) = pid.trim().parse::() + { + unsafe { + libc::kill(pid, libc::SIGKILL); + } + } +} + #[tokio::test] async fn test_no_new_privs_is_enabled() { let output = run_cmd_output( From e2b7bde812f07bc14ef887acd41eddc0a3a562b6 Mon Sep 17 00:00:00 2001 From: viyatb-oai Date: Tue, 17 Mar 2026 00:08:28 -0700 Subject: [PATCH 02/11] fix(linux-sandbox): scope detached child lifetime --- .../app-server/src/codex_message_processor.rs | 1 + codex-rs/core/src/exec.rs | 16 ++++++ codex-rs/core/src/landlock.rs | 10 ++++ codex-rs/core/src/landlock_tests.rs | 49 +++++++++++++++++-- codex-rs/core/src/sandboxing/mod.rs | 4 ++ codex-rs/core/src/sandboxing/mod_tests.rs | 6 +++ codex-rs/core/src/tools/js_repl/mod.rs | 2 + .../tools/runtimes/shell/unix_escalation.rs | 2 + codex-rs/core/src/tools/sandboxing.rs | 2 + codex-rs/linux-sandbox/src/bwrap.rs | 43 ++++++++++++++-- codex-rs/linux-sandbox/src/linux_run_main.rs | 17 +++++++ .../linux-sandbox/src/linux_run_main_tests.rs | 25 ++++++++++ 12 files changed, 168 insertions(+), 9 deletions(-) diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index eb4e432c5bb6..1d3bb184be9b 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -1769,6 +1769,7 @@ impl CodexMessageProcessor { sandbox_cwd.as_path(), &codex_linux_sandbox_exe, use_legacy_landlock, + codex_core::exec::LinuxSandboxProcessLifetime::TerminateWithParent, ) { Ok(exec_request) => { if let Err(error) = self diff --git a/codex-rs/core/src/exec.rs b/codex-rs/core/src/exec.rs index 4f462821e5cb..1058e3aab949 100644 --- a/codex-rs/core/src/exec.rs +++ b/codex-rs/core/src/exec.rs @@ -160,6 +160,18 @@ pub enum SandboxType { WindowsRestrictedToken, } +/// Controls how Linux sandboxed processes relate to the helper lifecycle. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub enum LinuxSandboxProcessLifetime { + /// Keep bubblewrap tied to the helper so tracked command trees terminate + /// with the original process. + #[default] + TerminateWithParent, + /// Omit bubblewrap's parent-death coupling so intentionally detached + /// descendants can outlive the original helper process. + AllowDetachedChildren, +} + impl SandboxType { pub(crate) fn as_metric_tag(self) -> &'static str { match self { @@ -197,6 +209,7 @@ pub async fn process_exec_tool_call( sandbox_cwd, codex_linux_sandbox_exe, use_legacy_landlock, + LinuxSandboxProcessLifetime::AllowDetachedChildren, )?; // Route through the sandboxing module for a single, unified execution path. @@ -205,6 +218,7 @@ pub async fn process_exec_tool_call( /// Transform a portable exec request into the concrete argv/env that should be /// spawned under the requested sandbox policy. +#[allow(clippy::too_many_arguments)] pub fn build_exec_request( params: ExecParams, sandbox_policy: &SandboxPolicy, @@ -213,6 +227,7 @@ pub fn build_exec_request( sandbox_cwd: &Path, codex_linux_sandbox_exe: &Option, use_legacy_landlock: bool, + linux_sandbox_process_lifetime: LinuxSandboxProcessLifetime, ) -> Result { let windows_sandbox_level = params.windows_sandbox_level; let enforce_managed_network = params.network.is_some(); @@ -268,6 +283,7 @@ pub fn build_exec_request( enforce_managed_network, network: network.as_ref(), sandbox_policy_cwd: sandbox_cwd, + linux_sandbox_process_lifetime, #[cfg(target_os = "macos")] macos_seatbelt_profile_extensions: None, codex_linux_sandbox_exe: codex_linux_sandbox_exe.as_ref(), diff --git a/codex-rs/core/src/landlock.rs b/codex-rs/core/src/landlock.rs index 6dcac030acd0..0e48fbc9e8a6 100644 --- a/codex-rs/core/src/landlock.rs +++ b/codex-rs/core/src/landlock.rs @@ -1,3 +1,4 @@ +use crate::exec::LinuxSandboxProcessLifetime; use crate::protocol::SandboxPolicy; use crate::spawn::SpawnChildRequest; use crate::spawn::StdioPolicy; @@ -45,6 +46,7 @@ where sandbox_policy_cwd, use_legacy_landlock, allow_network_for_proxy(/*enforce_managed_network*/ false), + LinuxSandboxProcessLifetime::AllowDetachedChildren, ); let arg0 = Some("codex-linux-sandbox"); spawn_child_async(SpawnChildRequest { @@ -84,6 +86,7 @@ pub(crate) fn create_linux_sandbox_command_args_for_policies( sandbox_policy_cwd: &Path, use_legacy_landlock: bool, allow_network_for_proxy: bool, + process_lifetime: LinuxSandboxProcessLifetime, ) -> Vec { let sandbox_policy_json = serde_json::to_string(sandbox_policy) .unwrap_or_else(|err| panic!("failed to serialize sandbox policy: {err}")); @@ -118,6 +121,9 @@ pub(crate) fn create_linux_sandbox_command_args_for_policies( if allow_network_for_proxy { linux_cmd.push("--allow-network-for-proxy".to_string()); } + if process_lifetime == LinuxSandboxProcessLifetime::AllowDetachedChildren { + linux_cmd.push("--allow-detached-children".to_string()); + } linux_cmd.push("--".to_string()); linux_cmd.extend(command); linux_cmd @@ -132,6 +138,7 @@ pub(crate) fn create_linux_sandbox_command_args( sandbox_policy_cwd: &Path, use_legacy_landlock: bool, allow_network_for_proxy: bool, + process_lifetime: LinuxSandboxProcessLifetime, ) -> Vec { let command_cwd = command_cwd .to_str() @@ -154,6 +161,9 @@ pub(crate) fn create_linux_sandbox_command_args( if allow_network_for_proxy { linux_cmd.push("--allow-network-for-proxy".to_string()); } + if process_lifetime == LinuxSandboxProcessLifetime::AllowDetachedChildren { + linux_cmd.push("--allow-detached-children".to_string()); + } // Separator so that command arguments starting with `-` are not parsed as // options of the helper itself. diff --git a/codex-rs/core/src/landlock_tests.rs b/codex-rs/core/src/landlock_tests.rs index 454878cd511d..84aa14e20cde 100644 --- a/codex-rs/core/src/landlock_tests.rs +++ b/codex-rs/core/src/landlock_tests.rs @@ -7,14 +7,27 @@ fn legacy_landlock_flag_is_included_when_requested() { let command_cwd = Path::new("/tmp/link"); let cwd = Path::new("/tmp"); - let default_bwrap = - create_linux_sandbox_command_args(command.clone(), command_cwd, cwd, false, false); + let default_bwrap = create_linux_sandbox_command_args( + command.clone(), + command_cwd, + cwd, + false, + false, + LinuxSandboxProcessLifetime::TerminateWithParent, + ); assert_eq!( default_bwrap.contains(&"--use-legacy-landlock".to_string()), false ); - let legacy_landlock = create_linux_sandbox_command_args(command, command_cwd, cwd, true, false); + let legacy_landlock = create_linux_sandbox_command_args( + command, + command_cwd, + cwd, + true, + false, + LinuxSandboxProcessLifetime::TerminateWithParent, + ); assert_eq!( legacy_landlock.contains(&"--use-legacy-landlock".to_string()), true @@ -27,13 +40,40 @@ fn proxy_flag_is_included_when_requested() { let command_cwd = Path::new("/tmp/link"); let cwd = Path::new("/tmp"); - let args = create_linux_sandbox_command_args(command, command_cwd, cwd, true, true); + let args = create_linux_sandbox_command_args( + command, + command_cwd, + cwd, + true, + true, + LinuxSandboxProcessLifetime::TerminateWithParent, + ); assert_eq!( args.contains(&"--allow-network-for-proxy".to_string()), true ); } +#[test] +fn detached_children_flag_is_included_when_requested() { + let command = vec!["/bin/true".to_string()]; + let command_cwd = Path::new("/tmp/link"); + let cwd = Path::new("/tmp"); + + let args = create_linux_sandbox_command_args( + command, + command_cwd, + cwd, + false, + false, + LinuxSandboxProcessLifetime::AllowDetachedChildren, + ); + assert_eq!( + args.contains(&"--allow-detached-children".to_string()), + true + ); +} + #[test] fn split_policy_flags_are_included() { let command = vec!["/bin/true".to_string()]; @@ -52,6 +92,7 @@ fn split_policy_flags_are_included() { cwd, true, false, + LinuxSandboxProcessLifetime::TerminateWithParent, ); assert_eq!( diff --git a/codex-rs/core/src/sandboxing/mod.rs b/codex-rs/core/src/sandboxing/mod.rs index db88788814e1..aa53ece43a3d 100644 --- a/codex-rs/core/src/sandboxing/mod.rs +++ b/codex-rs/core/src/sandboxing/mod.rs @@ -10,6 +10,7 @@ pub(crate) mod macos_permissions; use crate::exec::ExecExpiration; use crate::exec::ExecToolCallOutput; +use crate::exec::LinuxSandboxProcessLifetime; use crate::exec::SandboxType; use crate::exec::StdoutStream; use crate::exec::execute_exec_request; @@ -92,6 +93,7 @@ pub(crate) struct SandboxTransformRequest<'a> { // to make shared ownership explicit across runtime/sandbox plumbing. pub network: Option<&'a NetworkProxy>, pub sandbox_policy_cwd: &'a Path, + pub linux_sandbox_process_lifetime: LinuxSandboxProcessLifetime, #[cfg(target_os = "macos")] pub macos_seatbelt_profile_extensions: Option<&'a MacOsSeatbeltProfileExtensions>, pub codex_linux_sandbox_exe: Option<&'a PathBuf>, @@ -590,6 +592,7 @@ impl SandboxManager { enforce_managed_network, network, sandbox_policy_cwd, + linux_sandbox_process_lifetime, #[cfg(target_os = "macos")] macos_seatbelt_profile_extensions, codex_linux_sandbox_exe, @@ -679,6 +682,7 @@ impl SandboxManager { sandbox_policy_cwd, use_legacy_landlock, allow_proxy_network, + linux_sandbox_process_lifetime, ); let mut full_command = Vec::with_capacity(1 + args.len()); full_command.push(exe.to_string_lossy().to_string()); diff --git a/codex-rs/core/src/sandboxing/mod_tests.rs b/codex-rs/core/src/sandboxing/mod_tests.rs index 4d45dfb0080b..6428892ff7a0 100644 --- a/codex-rs/core/src/sandboxing/mod_tests.rs +++ b/codex-rs/core/src/sandboxing/mod_tests.rs @@ -171,6 +171,8 @@ fn transform_preserves_unrestricted_file_system_policy_for_restricted_network() enforce_managed_network: false, network: None, sandbox_policy_cwd: cwd.as_path(), + linux_sandbox_process_lifetime: + crate::exec::LinuxSandboxProcessLifetime::TerminateWithParent, #[cfg(target_os = "macos")] macos_seatbelt_profile_extensions: None, codex_linux_sandbox_exe: None, @@ -540,6 +542,8 @@ fn transform_additional_permissions_enable_network_for_external_sandbox() { enforce_managed_network: false, network: None, sandbox_policy_cwd: cwd.as_path(), + linux_sandbox_process_lifetime: + crate::exec::LinuxSandboxProcessLifetime::TerminateWithParent, #[cfg(target_os = "macos")] macos_seatbelt_profile_extensions: None, codex_linux_sandbox_exe: None, @@ -613,6 +617,8 @@ fn transform_additional_permissions_preserves_denied_entries() { enforce_managed_network: false, network: None, sandbox_policy_cwd: cwd.as_path(), + linux_sandbox_process_lifetime: + crate::exec::LinuxSandboxProcessLifetime::TerminateWithParent, #[cfg(target_os = "macos")] macos_seatbelt_profile_extensions: None, codex_linux_sandbox_exe: None, diff --git a/codex-rs/core/src/tools/js_repl/mod.rs b/codex-rs/core/src/tools/js_repl/mod.rs index 392f311ce21f..367818a5f00d 100644 --- a/codex-rs/core/src/tools/js_repl/mod.rs +++ b/codex-rs/core/src/tools/js_repl/mod.rs @@ -1066,6 +1066,8 @@ impl JsReplManager { enforce_managed_network: has_managed_network_requirements, network: None, sandbox_policy_cwd: &turn.cwd, + linux_sandbox_process_lifetime: + crate::exec::LinuxSandboxProcessLifetime::TerminateWithParent, #[cfg(target_os = "macos")] macos_seatbelt_profile_extensions: None, codex_linux_sandbox_exe: turn.codex_linux_sandbox_exe.as_ref(), 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 89bf4d424c3a..60174ed3b694 100644 --- a/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs +++ b/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs @@ -1057,6 +1057,8 @@ impl CoreShellCommandExecutor { enforce_managed_network: self.network.is_some(), network: self.network.as_ref(), sandbox_policy_cwd: &self.sandbox_policy_cwd, + linux_sandbox_process_lifetime: + crate::exec::LinuxSandboxProcessLifetime::TerminateWithParent, #[cfg(target_os = "macos")] macos_seatbelt_profile_extensions, codex_linux_sandbox_exe: self.codex_linux_sandbox_exe.as_ref(), diff --git a/codex-rs/core/src/tools/sandboxing.rs b/codex-rs/core/src/tools/sandboxing.rs index bf386a12d0b6..c3f2f7161ebf 100644 --- a/codex-rs/core/src/tools/sandboxing.rs +++ b/codex-rs/core/src/tools/sandboxing.rs @@ -352,6 +352,8 @@ impl<'a> SandboxAttempt<'a> { enforce_managed_network: self.enforce_managed_network, network, sandbox_policy_cwd: self.sandbox_cwd, + linux_sandbox_process_lifetime: + crate::exec::LinuxSandboxProcessLifetime::TerminateWithParent, #[cfg(target_os = "macos")] macos_seatbelt_profile_extensions: None, codex_linux_sandbox_exe: self.codex_linux_sandbox_exe, diff --git a/codex-rs/linux-sandbox/src/bwrap.rs b/codex-rs/linux-sandbox/src/bwrap.rs index a4416d0fe659..6a835f616fb3 100644 --- a/codex-rs/linux-sandbox/src/bwrap.rs +++ b/codex-rs/linux-sandbox/src/bwrap.rs @@ -48,6 +48,9 @@ pub(crate) struct BwrapOptions { pub mount_proc: bool, /// How networking should be configured inside the bubblewrap sandbox. pub network_mode: BwrapNetworkMode, + /// Whether the sandbox should terminate with the helper process or allow + /// intentionally detached descendants to outlive it. + pub process_lifetime: BwrapProcessLifetime, } impl Default for BwrapOptions { @@ -55,10 +58,18 @@ impl Default for BwrapOptions { Self { mount_proc: true, network_mode: BwrapNetworkMode::FullAccess, + process_lifetime: BwrapProcessLifetime::TerminateWithParent, } } } +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub(crate) enum BwrapProcessLifetime { + #[default] + TerminateWithParent, + AllowDetachedChildren, +} + /// Network policy modes for bubblewrap. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub(crate) enum BwrapNetworkMode { @@ -129,10 +140,11 @@ fn create_bwrap_flags_full_filesystem(command: Vec, options: BwrapOption // Always enter a fresh user namespace so root inside a container does // not need ambient CAP_SYS_ADMIN to create the remaining namespaces. "--unshare-user".to_string(), - // Preserve the PID namespace even when the initial tool process exits - // so detached descendants keep their sandboxed `/proc` view. "--unshare-pid".to_string(), ]; + if options.process_lifetime == BwrapProcessLifetime::TerminateWithParent { + args.push("--die-with-parent".to_string()); + } if options.network_mode.should_unshare_network() { args.push("--unshare-net".to_string()); } @@ -167,10 +179,10 @@ fn create_bwrap_flags( // Request a user namespace explicitly rather than relying on bubblewrap's // auto-enable behavior, which is skipped when the caller runs as uid 0. args.push("--unshare-user".to_string()); - // Keep the PID namespace, but do not tie the sandbox lifecycle to - // bubblewrap's parent process. Detached children should be able to - // survive after the initial one-shot tool process exits. args.push("--unshare-pid".to_string()); + if options.process_lifetime == BwrapProcessLifetime::TerminateWithParent { + args.push("--die-with-parent".to_string()); + } if options.network_mode.should_unshare_network() { args.push("--unshare-net".to_string()); } @@ -636,6 +648,7 @@ mod tests { BwrapOptions { mount_proc: true, network_mode: BwrapNetworkMode::FullAccess, + process_lifetime: BwrapProcessLifetime::TerminateWithParent, }, ) .expect("create bwrap args"); @@ -654,6 +667,7 @@ mod tests { BwrapOptions { mount_proc: true, network_mode: BwrapNetworkMode::ProxyOnly, + process_lifetime: BwrapProcessLifetime::TerminateWithParent, }, ) .expect("create bwrap args"); @@ -667,6 +681,7 @@ mod tests { "/".to_string(), "--unshare-user".to_string(), "--unshare-pid".to_string(), + "--die-with-parent".to_string(), "--unshare-net".to_string(), "--proc".to_string(), "/proc".to_string(), @@ -676,6 +691,24 @@ mod tests { ); } + #[test] + fn allow_detached_children_omits_die_with_parent() { + let args = create_bwrap_command_args( + vec!["/bin/true".to_string()], + &FileSystemSandboxPolicy::from(&SandboxPolicy::DangerFullAccess), + Path::new("/"), + Path::new("/"), + BwrapOptions { + mount_proc: true, + network_mode: BwrapNetworkMode::FullAccess, + process_lifetime: BwrapProcessLifetime::AllowDetachedChildren, + }, + ) + .expect("create bwrap args"); + + assert!(!args.args.contains(&"--die-with-parent".to_string())); + } + #[cfg(unix)] #[test] fn restricted_policy_chdirs_to_canonical_command_cwd() { diff --git a/codex-rs/linux-sandbox/src/linux_run_main.rs b/codex-rs/linux-sandbox/src/linux_run_main.rs index a6a47117e75d..8105c7a088b9 100644 --- a/codex-rs/linux-sandbox/src/linux_run_main.rs +++ b/codex-rs/linux-sandbox/src/linux_run_main.rs @@ -9,6 +9,7 @@ use std::path::PathBuf; use crate::bwrap::BwrapNetworkMode; use crate::bwrap::BwrapOptions; +use crate::bwrap::BwrapProcessLifetime; use crate::bwrap::create_bwrap_command_args; use crate::landlock::apply_sandbox_policy_to_current_thread; use crate::proxy_routing::activate_proxy_routes_in_netns; @@ -79,6 +80,13 @@ pub struct LandlockCommand { #[arg(long = "proxy-route-spec", hide = true)] pub proxy_route_spec: Option, + /// Internal compatibility flag. + /// + /// If set, omit bubblewrap's parent-death coupling so intentionally + /// detached descendants can outlive the original helper process. + #[arg(long = "allow-detached-children", hide = true, default_value_t = false)] + pub allow_detached_children: bool, + /// When set, skip mounting a fresh `/proc` even though PID isolation is /// still enabled. This is primarily intended for restrictive container /// environments that deny `--proc /proc`. @@ -108,6 +116,7 @@ pub fn run_main() -> ! { apply_seccomp_then_exec, allow_network_for_proxy, proxy_route_spec, + allow_detached_children, no_proc, command, } = LandlockCommand::parse(); @@ -203,6 +212,7 @@ pub fn run_main() -> ! { inner, !no_proc, allow_network_for_proxy, + allow_detached_children, ); } @@ -405,6 +415,7 @@ fn run_bwrap_with_proc_fallback( inner: Vec, mount_proc: bool, allow_network_for_proxy: bool, + allow_detached_children: bool, ) -> ! { let network_mode = bwrap_network_mode(network_sandbox_policy, allow_network_for_proxy); let mut mount_proc = mount_proc; @@ -426,6 +437,11 @@ fn run_bwrap_with_proc_fallback( let options = BwrapOptions { mount_proc, network_mode, + process_lifetime: if allow_detached_children { + BwrapProcessLifetime::AllowDetachedChildren + } else { + BwrapProcessLifetime::TerminateWithParent + }, }; let bwrap_args = build_bwrap_argv( inner, @@ -515,6 +531,7 @@ fn build_preflight_bwrap_argv( BwrapOptions { mount_proc: true, network_mode, + process_lifetime: BwrapProcessLifetime::TerminateWithParent, }, ) } 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 c87ff8beca4b..6924a5088faf 100644 --- a/codex-rs/linux-sandbox/src/linux_run_main_tests.rs +++ b/codex-rs/linux-sandbox/src/linux_run_main_tests.rs @@ -1,6 +1,8 @@ #[cfg(test)] use super::*; #[cfg(test)] +use crate::bwrap::BwrapProcessLifetime; +#[cfg(test)] use codex_protocol::protocol::FileSystemSandboxPolicy; #[cfg(test)] use codex_protocol::protocol::NetworkSandboxPolicy; @@ -48,6 +50,7 @@ fn inserts_bwrap_argv0_before_command_separator() { BwrapOptions { mount_proc: true, network_mode: BwrapNetworkMode::FullAccess, + process_lifetime: BwrapProcessLifetime::TerminateWithParent, }, ) .args; @@ -63,6 +66,7 @@ fn inserts_bwrap_argv0_before_command_separator() { "/dev".to_string(), "--unshare-user".to_string(), "--unshare-pid".to_string(), + "--die-with-parent".to_string(), "--proc".to_string(), "/proc".to_string(), "--argv0".to_string(), @@ -84,6 +88,7 @@ fn inserts_unshare_net_when_network_isolation_requested() { BwrapOptions { mount_proc: true, network_mode: BwrapNetworkMode::Isolated, + process_lifetime: BwrapProcessLifetime::TerminateWithParent, }, ) .args; @@ -101,12 +106,32 @@ fn inserts_unshare_net_when_proxy_only_network_mode_requested() { BwrapOptions { mount_proc: true, network_mode: BwrapNetworkMode::ProxyOnly, + process_lifetime: BwrapProcessLifetime::TerminateWithParent, }, ) .args; assert!(argv.contains(&"--unshare-net".to_string())); } +#[test] +fn omits_die_with_parent_when_detached_children_are_allowed() { + let sandbox_policy = SandboxPolicy::new_read_only_policy(); + let argv = build_bwrap_argv( + vec!["/bin/true".to_string()], + &FileSystemSandboxPolicy::from(&sandbox_policy), + Path::new("/"), + Path::new("/"), + BwrapOptions { + mount_proc: true, + network_mode: BwrapNetworkMode::FullAccess, + process_lifetime: BwrapProcessLifetime::AllowDetachedChildren, + }, + ) + .args; + + assert!(!argv.contains(&"--die-with-parent".to_string())); +} + #[test] fn proxy_only_mode_takes_precedence_over_full_network_policy() { let mode = bwrap_network_mode(NetworkSandboxPolicy::Enabled, true); From 38df46334f9a8737e2e1acf37513b8246ffee5e4 Mon Sep 17 00:00:00 2001 From: viyatb-oai Date: Tue, 17 Mar 2026 00:36:37 -0700 Subject: [PATCH 03/11] refactor(linux-sandbox): narrow detached child plumbing --- .../app-server/src/codex_message_processor.rs | 1 - codex-rs/core/src/exec.rs | 28 +++++------ codex-rs/core/src/landlock.rs | 10 ---- codex-rs/core/src/landlock_tests.rs | 49 ++----------------- codex-rs/core/src/sandboxing/mod.rs | 4 -- codex-rs/core/src/sandboxing/mod_tests.rs | 6 --- codex-rs/core/src/tools/js_repl/mod.rs | 2 - .../tools/runtimes/shell/unix_escalation.rs | 2 - codex-rs/core/src/tools/sandboxing.rs | 2 - 9 files changed, 15 insertions(+), 89 deletions(-) diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index 1d3bb184be9b..eb4e432c5bb6 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -1769,7 +1769,6 @@ impl CodexMessageProcessor { sandbox_cwd.as_path(), &codex_linux_sandbox_exe, use_legacy_landlock, - codex_core::exec::LinuxSandboxProcessLifetime::TerminateWithParent, ) { Ok(exec_request) => { if let Err(error) = self diff --git a/codex-rs/core/src/exec.rs b/codex-rs/core/src/exec.rs index 1058e3aab949..2345d2a1cd28 100644 --- a/codex-rs/core/src/exec.rs +++ b/codex-rs/core/src/exec.rs @@ -160,18 +160,6 @@ pub enum SandboxType { WindowsRestrictedToken, } -/// Controls how Linux sandboxed processes relate to the helper lifecycle. -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] -pub enum LinuxSandboxProcessLifetime { - /// Keep bubblewrap tied to the helper so tracked command trees terminate - /// with the original process. - #[default] - TerminateWithParent, - /// Omit bubblewrap's parent-death coupling so intentionally detached - /// descendants can outlive the original helper process. - AllowDetachedChildren, -} - impl SandboxType { pub(crate) fn as_metric_tag(self) -> &'static str { match self { @@ -201,7 +189,7 @@ pub async fn process_exec_tool_call( use_legacy_landlock: bool, stdout_stream: Option, ) -> Result { - let exec_req = build_exec_request( + let mut exec_req = build_exec_request( params, sandbox_policy, file_system_sandbox_policy, @@ -209,8 +197,17 @@ pub async fn process_exec_tool_call( sandbox_cwd, codex_linux_sandbox_exe, use_legacy_landlock, - LinuxSandboxProcessLifetime::AllowDetachedChildren, )?; + if exec_req.sandbox == SandboxType::LinuxSeccomp + && let Some(separator) = exec_req.command.iter().position(|arg| arg == "--") + && !exec_req.command[..separator] + .iter() + .any(|arg| arg == "--allow-detached-children") + { + exec_req + .command + .insert(separator, "--allow-detached-children".to_string()); + } // Route through the sandboxing module for a single, unified execution path. crate::sandboxing::execute_env(exec_req, stdout_stream).await @@ -218,7 +215,6 @@ pub async fn process_exec_tool_call( /// Transform a portable exec request into the concrete argv/env that should be /// spawned under the requested sandbox policy. -#[allow(clippy::too_many_arguments)] pub fn build_exec_request( params: ExecParams, sandbox_policy: &SandboxPolicy, @@ -227,7 +223,6 @@ pub fn build_exec_request( sandbox_cwd: &Path, codex_linux_sandbox_exe: &Option, use_legacy_landlock: bool, - linux_sandbox_process_lifetime: LinuxSandboxProcessLifetime, ) -> Result { let windows_sandbox_level = params.windows_sandbox_level; let enforce_managed_network = params.network.is_some(); @@ -283,7 +278,6 @@ pub fn build_exec_request( enforce_managed_network, network: network.as_ref(), sandbox_policy_cwd: sandbox_cwd, - linux_sandbox_process_lifetime, #[cfg(target_os = "macos")] macos_seatbelt_profile_extensions: None, codex_linux_sandbox_exe: codex_linux_sandbox_exe.as_ref(), diff --git a/codex-rs/core/src/landlock.rs b/codex-rs/core/src/landlock.rs index 0e48fbc9e8a6..6dcac030acd0 100644 --- a/codex-rs/core/src/landlock.rs +++ b/codex-rs/core/src/landlock.rs @@ -1,4 +1,3 @@ -use crate::exec::LinuxSandboxProcessLifetime; use crate::protocol::SandboxPolicy; use crate::spawn::SpawnChildRequest; use crate::spawn::StdioPolicy; @@ -46,7 +45,6 @@ where sandbox_policy_cwd, use_legacy_landlock, allow_network_for_proxy(/*enforce_managed_network*/ false), - LinuxSandboxProcessLifetime::AllowDetachedChildren, ); let arg0 = Some("codex-linux-sandbox"); spawn_child_async(SpawnChildRequest { @@ -86,7 +84,6 @@ pub(crate) fn create_linux_sandbox_command_args_for_policies( sandbox_policy_cwd: &Path, use_legacy_landlock: bool, allow_network_for_proxy: bool, - process_lifetime: LinuxSandboxProcessLifetime, ) -> Vec { let sandbox_policy_json = serde_json::to_string(sandbox_policy) .unwrap_or_else(|err| panic!("failed to serialize sandbox policy: {err}")); @@ -121,9 +118,6 @@ pub(crate) fn create_linux_sandbox_command_args_for_policies( if allow_network_for_proxy { linux_cmd.push("--allow-network-for-proxy".to_string()); } - if process_lifetime == LinuxSandboxProcessLifetime::AllowDetachedChildren { - linux_cmd.push("--allow-detached-children".to_string()); - } linux_cmd.push("--".to_string()); linux_cmd.extend(command); linux_cmd @@ -138,7 +132,6 @@ pub(crate) fn create_linux_sandbox_command_args( sandbox_policy_cwd: &Path, use_legacy_landlock: bool, allow_network_for_proxy: bool, - process_lifetime: LinuxSandboxProcessLifetime, ) -> Vec { let command_cwd = command_cwd .to_str() @@ -161,9 +154,6 @@ pub(crate) fn create_linux_sandbox_command_args( if allow_network_for_proxy { linux_cmd.push("--allow-network-for-proxy".to_string()); } - if process_lifetime == LinuxSandboxProcessLifetime::AllowDetachedChildren { - linux_cmd.push("--allow-detached-children".to_string()); - } // Separator so that command arguments starting with `-` are not parsed as // options of the helper itself. diff --git a/codex-rs/core/src/landlock_tests.rs b/codex-rs/core/src/landlock_tests.rs index 84aa14e20cde..454878cd511d 100644 --- a/codex-rs/core/src/landlock_tests.rs +++ b/codex-rs/core/src/landlock_tests.rs @@ -7,27 +7,14 @@ fn legacy_landlock_flag_is_included_when_requested() { let command_cwd = Path::new("/tmp/link"); let cwd = Path::new("/tmp"); - let default_bwrap = create_linux_sandbox_command_args( - command.clone(), - command_cwd, - cwd, - false, - false, - LinuxSandboxProcessLifetime::TerminateWithParent, - ); + let default_bwrap = + create_linux_sandbox_command_args(command.clone(), command_cwd, cwd, false, false); assert_eq!( default_bwrap.contains(&"--use-legacy-landlock".to_string()), false ); - let legacy_landlock = create_linux_sandbox_command_args( - command, - command_cwd, - cwd, - true, - false, - LinuxSandboxProcessLifetime::TerminateWithParent, - ); + let legacy_landlock = create_linux_sandbox_command_args(command, command_cwd, cwd, true, false); assert_eq!( legacy_landlock.contains(&"--use-legacy-landlock".to_string()), true @@ -40,40 +27,13 @@ fn proxy_flag_is_included_when_requested() { let command_cwd = Path::new("/tmp/link"); let cwd = Path::new("/tmp"); - let args = create_linux_sandbox_command_args( - command, - command_cwd, - cwd, - true, - true, - LinuxSandboxProcessLifetime::TerminateWithParent, - ); + let args = create_linux_sandbox_command_args(command, command_cwd, cwd, true, true); assert_eq!( args.contains(&"--allow-network-for-proxy".to_string()), true ); } -#[test] -fn detached_children_flag_is_included_when_requested() { - let command = vec!["/bin/true".to_string()]; - let command_cwd = Path::new("/tmp/link"); - let cwd = Path::new("/tmp"); - - let args = create_linux_sandbox_command_args( - command, - command_cwd, - cwd, - false, - false, - LinuxSandboxProcessLifetime::AllowDetachedChildren, - ); - assert_eq!( - args.contains(&"--allow-detached-children".to_string()), - true - ); -} - #[test] fn split_policy_flags_are_included() { let command = vec!["/bin/true".to_string()]; @@ -92,7 +52,6 @@ fn split_policy_flags_are_included() { cwd, true, false, - LinuxSandboxProcessLifetime::TerminateWithParent, ); assert_eq!( diff --git a/codex-rs/core/src/sandboxing/mod.rs b/codex-rs/core/src/sandboxing/mod.rs index aa53ece43a3d..db88788814e1 100644 --- a/codex-rs/core/src/sandboxing/mod.rs +++ b/codex-rs/core/src/sandboxing/mod.rs @@ -10,7 +10,6 @@ pub(crate) mod macos_permissions; use crate::exec::ExecExpiration; use crate::exec::ExecToolCallOutput; -use crate::exec::LinuxSandboxProcessLifetime; use crate::exec::SandboxType; use crate::exec::StdoutStream; use crate::exec::execute_exec_request; @@ -93,7 +92,6 @@ pub(crate) struct SandboxTransformRequest<'a> { // to make shared ownership explicit across runtime/sandbox plumbing. pub network: Option<&'a NetworkProxy>, pub sandbox_policy_cwd: &'a Path, - pub linux_sandbox_process_lifetime: LinuxSandboxProcessLifetime, #[cfg(target_os = "macos")] pub macos_seatbelt_profile_extensions: Option<&'a MacOsSeatbeltProfileExtensions>, pub codex_linux_sandbox_exe: Option<&'a PathBuf>, @@ -592,7 +590,6 @@ impl SandboxManager { enforce_managed_network, network, sandbox_policy_cwd, - linux_sandbox_process_lifetime, #[cfg(target_os = "macos")] macos_seatbelt_profile_extensions, codex_linux_sandbox_exe, @@ -682,7 +679,6 @@ impl SandboxManager { sandbox_policy_cwd, use_legacy_landlock, allow_proxy_network, - linux_sandbox_process_lifetime, ); let mut full_command = Vec::with_capacity(1 + args.len()); full_command.push(exe.to_string_lossy().to_string()); diff --git a/codex-rs/core/src/sandboxing/mod_tests.rs b/codex-rs/core/src/sandboxing/mod_tests.rs index 6428892ff7a0..4d45dfb0080b 100644 --- a/codex-rs/core/src/sandboxing/mod_tests.rs +++ b/codex-rs/core/src/sandboxing/mod_tests.rs @@ -171,8 +171,6 @@ fn transform_preserves_unrestricted_file_system_policy_for_restricted_network() enforce_managed_network: false, network: None, sandbox_policy_cwd: cwd.as_path(), - linux_sandbox_process_lifetime: - crate::exec::LinuxSandboxProcessLifetime::TerminateWithParent, #[cfg(target_os = "macos")] macos_seatbelt_profile_extensions: None, codex_linux_sandbox_exe: None, @@ -542,8 +540,6 @@ fn transform_additional_permissions_enable_network_for_external_sandbox() { enforce_managed_network: false, network: None, sandbox_policy_cwd: cwd.as_path(), - linux_sandbox_process_lifetime: - crate::exec::LinuxSandboxProcessLifetime::TerminateWithParent, #[cfg(target_os = "macos")] macos_seatbelt_profile_extensions: None, codex_linux_sandbox_exe: None, @@ -617,8 +613,6 @@ fn transform_additional_permissions_preserves_denied_entries() { enforce_managed_network: false, network: None, sandbox_policy_cwd: cwd.as_path(), - linux_sandbox_process_lifetime: - crate::exec::LinuxSandboxProcessLifetime::TerminateWithParent, #[cfg(target_os = "macos")] macos_seatbelt_profile_extensions: None, codex_linux_sandbox_exe: None, diff --git a/codex-rs/core/src/tools/js_repl/mod.rs b/codex-rs/core/src/tools/js_repl/mod.rs index 367818a5f00d..392f311ce21f 100644 --- a/codex-rs/core/src/tools/js_repl/mod.rs +++ b/codex-rs/core/src/tools/js_repl/mod.rs @@ -1066,8 +1066,6 @@ impl JsReplManager { enforce_managed_network: has_managed_network_requirements, network: None, sandbox_policy_cwd: &turn.cwd, - linux_sandbox_process_lifetime: - crate::exec::LinuxSandboxProcessLifetime::TerminateWithParent, #[cfg(target_os = "macos")] macos_seatbelt_profile_extensions: None, codex_linux_sandbox_exe: turn.codex_linux_sandbox_exe.as_ref(), 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 60174ed3b694..89bf4d424c3a 100644 --- a/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs +++ b/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs @@ -1057,8 +1057,6 @@ impl CoreShellCommandExecutor { enforce_managed_network: self.network.is_some(), network: self.network.as_ref(), sandbox_policy_cwd: &self.sandbox_policy_cwd, - linux_sandbox_process_lifetime: - crate::exec::LinuxSandboxProcessLifetime::TerminateWithParent, #[cfg(target_os = "macos")] macos_seatbelt_profile_extensions, codex_linux_sandbox_exe: self.codex_linux_sandbox_exe.as_ref(), diff --git a/codex-rs/core/src/tools/sandboxing.rs b/codex-rs/core/src/tools/sandboxing.rs index c3f2f7161ebf..bf386a12d0b6 100644 --- a/codex-rs/core/src/tools/sandboxing.rs +++ b/codex-rs/core/src/tools/sandboxing.rs @@ -352,8 +352,6 @@ impl<'a> SandboxAttempt<'a> { enforce_managed_network: self.enforce_managed_network, network, sandbox_policy_cwd: self.sandbox_cwd, - linux_sandbox_process_lifetime: - crate::exec::LinuxSandboxProcessLifetime::TerminateWithParent, #[cfg(target_os = "macos")] macos_seatbelt_profile_extensions: None, codex_linux_sandbox_exe: self.codex_linux_sandbox_exe, From ebcd1057d5987824db77cd7c6628916480a42726 Mon Sep 17 00:00:00 2001 From: viyatb-oai Date: Tue, 17 Mar 2026 01:11:24 -0700 Subject: [PATCH 04/11] refactor(sandboxing): own detached child helper args --- codex-rs/core/src/exec.rs | 11 +---------- codex-rs/core/src/sandboxing/mod.rs | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/codex-rs/core/src/exec.rs b/codex-rs/core/src/exec.rs index 2345d2a1cd28..147667f22877 100644 --- a/codex-rs/core/src/exec.rs +++ b/codex-rs/core/src/exec.rs @@ -198,16 +198,7 @@ pub async fn process_exec_tool_call( codex_linux_sandbox_exe, use_legacy_landlock, )?; - if exec_req.sandbox == SandboxType::LinuxSeccomp - && let Some(separator) = exec_req.command.iter().position(|arg| arg == "--") - && !exec_req.command[..separator] - .iter() - .any(|arg| arg == "--allow-detached-children") - { - exec_req - .command - .insert(separator, "--allow-detached-children".to_string()); - } + exec_req.allow_detached_children_in_linux_sandbox(); // Route through the sandboxing module for a single, unified execution path. crate::sandboxing::execute_env(exec_req, stdout_stream).await diff --git a/codex-rs/core/src/sandboxing/mod.rs b/codex-rs/core/src/sandboxing/mod.rs index db88788814e1..1a9c2d177173 100644 --- a/codex-rs/core/src/sandboxing/mod.rs +++ b/codex-rs/core/src/sandboxing/mod.rs @@ -78,6 +78,25 @@ pub struct ExecRequest { pub arg0: Option, } +impl ExecRequest { + pub(crate) fn allow_detached_children_in_linux_sandbox(&mut self) { + const ALLOW_DETACHED_CHILDREN: &str = "--allow-detached-children"; + + if self.sandbox != SandboxType::LinuxSeccomp { + return; + } + + if let Some(separator) = self.command.iter().position(|arg| arg == "--") + && !self.command[..separator] + .iter() + .any(|arg| arg == ALLOW_DETACHED_CHILDREN) + { + self.command + .insert(separator, ALLOW_DETACHED_CHILDREN.to_string()); + } + } +} + /// Bundled arguments for sandbox transformation. /// /// This keeps call sites self-documenting when several fields are optional. From 47778675e1bf383b425e2efe38bd11a7a8d759ea Mon Sep 17 00:00:00 2001 From: viyatb-oai Date: Tue, 17 Mar 2026 12:46:30 -0700 Subject: [PATCH 05/11] fix(linux-sandbox): reuse bwrap options in fallback --- codex-rs/linux-sandbox/src/linux_run_main.rs | 38 ++++++++------------ 1 file changed, 15 insertions(+), 23 deletions(-) diff --git a/codex-rs/linux-sandbox/src/linux_run_main.rs b/codex-rs/linux-sandbox/src/linux_run_main.rs index 8105c7a088b9..4baef2797ad5 100644 --- a/codex-rs/linux-sandbox/src/linux_run_main.rs +++ b/codex-rs/linux-sandbox/src/linux_run_main.rs @@ -204,15 +204,21 @@ pub fn run_main() -> ! { proxy_route_spec, command, }); + let options = BwrapOptions { + mount_proc: !no_proc, + network_mode: bwrap_network_mode(network_sandbox_policy, allow_network_for_proxy), + process_lifetime: if allow_detached_children { + BwrapProcessLifetime::AllowDetachedChildren + } else { + BwrapProcessLifetime::TerminateWithParent + }, + }; run_bwrap_with_proc_fallback( &sandbox_policy_cwd, command_cwd.as_deref(), &file_system_sandbox_policy, - network_sandbox_policy, inner, - !no_proc, - allow_network_for_proxy, - allow_detached_children, + options, ); } @@ -411,38 +417,24 @@ fn run_bwrap_with_proc_fallback( sandbox_policy_cwd: &Path, command_cwd: Option<&Path>, file_system_sandbox_policy: &FileSystemSandboxPolicy, - network_sandbox_policy: NetworkSandboxPolicy, inner: Vec, - mount_proc: bool, - allow_network_for_proxy: bool, - allow_detached_children: bool, + options: BwrapOptions, ) -> ! { - let network_mode = bwrap_network_mode(network_sandbox_policy, allow_network_for_proxy); - let mut mount_proc = mount_proc; + let mut options = options; let command_cwd = command_cwd.unwrap_or(sandbox_policy_cwd); - if mount_proc + if options.mount_proc && !preflight_proc_mount_support( sandbox_policy_cwd, command_cwd, file_system_sandbox_policy, - network_mode, + options.network_mode, ) { // Keep the retry silent so sandbox-internal diagnostics do not leak into the // child process stderr stream. - mount_proc = false; + options.mount_proc = false; } - - let options = BwrapOptions { - mount_proc, - network_mode, - process_lifetime: if allow_detached_children { - BwrapProcessLifetime::AllowDetachedChildren - } else { - BwrapProcessLifetime::TerminateWithParent - }, - }; let bwrap_args = build_bwrap_argv( inner, file_system_sandbox_policy, From 8ad7cb46cf6622da0f313b996da79d4bec75692f Mon Sep 17 00:00:00 2001 From: viyatb-oai Date: Tue, 17 Mar 2026 18:11:34 -0700 Subject: [PATCH 06/11] refactor(linux-sandbox): address detached child review feedback --- codex-rs/core/src/exec.rs | 25 +++++++- .../linux-sandbox/tests/suite/landlock.rs | 61 +++++++++++-------- 2 files changed, 57 insertions(+), 29 deletions(-) diff --git a/codex-rs/core/src/exec.rs b/codex-rs/core/src/exec.rs index 147667f22877..e04d2909ff29 100644 --- a/codex-rs/core/src/exec.rs +++ b/codex-rs/core/src/exec.rs @@ -189,7 +189,7 @@ pub async fn process_exec_tool_call( use_legacy_landlock: bool, stdout_stream: Option, ) -> Result { - let mut exec_req = build_exec_request( + let exec_req = build_process_exec_tool_request( params, sandbox_policy, file_system_sandbox_policy, @@ -198,12 +198,33 @@ pub async fn process_exec_tool_call( codex_linux_sandbox_exe, use_legacy_landlock, )?; - exec_req.allow_detached_children_in_linux_sandbox(); // Route through the sandboxing module for a single, unified execution path. crate::sandboxing::execute_env(exec_req, stdout_stream).await } +fn build_process_exec_tool_request( + params: ExecParams, + sandbox_policy: &SandboxPolicy, + file_system_sandbox_policy: &FileSystemSandboxPolicy, + network_sandbox_policy: NetworkSandboxPolicy, + sandbox_cwd: &Path, + codex_linux_sandbox_exe: &Option, + use_legacy_landlock: bool, +) -> Result { + let mut exec_req = build_exec_request( + params, + sandbox_policy, + file_system_sandbox_policy, + network_sandbox_policy, + sandbox_cwd, + codex_linux_sandbox_exe, + use_legacy_landlock, + )?; + exec_req.allow_detached_children_in_linux_sandbox(); + Ok(exec_req) +} + /// Transform a portable exec request into the concrete argv/env that should be /// spawned under the requested sandbox policy. pub fn build_exec_request( diff --git a/codex-rs/linux-sandbox/tests/suite/landlock.rs b/codex-rs/linux-sandbox/tests/suite/landlock.rs index 0dfbf940042a..739935e49c8e 100644 --- a/codex-rs/linux-sandbox/tests/suite/landlock.rs +++ b/codex-rs/linux-sandbox/tests/suite/landlock.rs @@ -21,6 +21,7 @@ use codex_utils_absolute_path::AbsolutePathBuf; use pretty_assertions::assert_eq; use std::collections::HashMap; use std::path::PathBuf; +use std::time::Duration; use tempfile::NamedTempFile; // At least on GitHub CI, the arm64 tests appear to need longer timeouts. @@ -63,9 +64,15 @@ async fn run_cmd_output( writable_roots: &[PathBuf], timeout_ms: u64, ) -> codex_core::exec::ExecToolCallOutput { - run_cmd_result_with_writable_roots(cmd, writable_roots, timeout_ms, false, false) - .await - .expect("sandboxed command should execute") + run_cmd_result_with_writable_roots( + cmd, + writable_roots, + timeout_ms, + /*use_legacy_landlock*/ false, + /*network_access*/ false, + ) + .await + .expect("sandboxed command should execute") } async fn run_cmd_result_with_writable_roots( @@ -156,8 +163,8 @@ async fn should_skip_bwrap_tests() -> bool { &["bash", "-lc", "true"], &[], NETWORK_TIMEOUT_MS, - false, - true, + /*use_legacy_landlock*/ false, + /*network_access*/ true, ) .await { @@ -217,8 +224,8 @@ 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, - false, - true, + /*use_legacy_landlock*/ false, + /*network_access*/ true, ) .await .expect("sandboxed command should execute"); @@ -241,8 +248,8 @@ async fn bwrap_populates_minimal_dev_nodes() { ], &[], LONG_TIMEOUT_MS, - false, - true, + /*use_legacy_landlock*/ false, + /*network_access*/ true, ) .await .expect("sandboxed command should execute"); @@ -279,8 +286,8 @@ async fn bwrap_preserves_writable_dev_shm_bind_mount() { ], &[PathBuf::from("/dev/shm")], LONG_TIMEOUT_MS, - false, - true, + /*use_legacy_landlock*/ false, + /*network_access*/ true, ) .await .expect("sandboxed command should execute"); @@ -330,21 +337,21 @@ async fn detached_children_survive_parent_exit_under_bwrap() { &["bash", "-lc", &command], &[tempdir.path().to_path_buf()], LONG_TIMEOUT_MS, - false, - true, + /*use_legacy_landlock*/ false, + /*network_access*/ true, ) .await .expect("sandboxed command should execute"); assert_eq!(output.exit_code, 0); - tokio::time::sleep(std::time::Duration::from_millis(700)).await; + tokio::time::sleep(Duration::from_millis(700)).await; let first_len = std::fs::metadata(&log_path) .expect("detached child should create log file") .len(); assert!(first_len > 0, "detached child should write to the log"); - tokio::time::sleep(std::time::Duration::from_millis(700)).await; + tokio::time::sleep(Duration::from_millis(700)).await; let second_len = std::fs::metadata(&log_path) .expect("detached child log file should still exist") .len(); @@ -378,8 +385,8 @@ async fn sandbox_ignores_missing_writable_roots_under_bwrap() { &["bash", "-lc", "printf sandbox-ok"], &[existing_root, missing_root], LONG_TIMEOUT_MS, - false, - true, + /*use_legacy_landlock*/ false, + /*network_access*/ true, ) .await .expect("sandboxed command should execute"); @@ -446,7 +453,7 @@ async fn assert_network_blocked(cmd: &[&str]) { NetworkSandboxPolicy::from(&sandbox_policy), sandbox_cwd.as_path(), &codex_linux_sandbox_exe, - false, + /*use_legacy_landlock*/ false, None, ) .await; @@ -522,8 +529,8 @@ async fn sandbox_blocks_git_and_codex_writes_inside_writable_root() { ], &[tmpdir.path().to_path_buf()], LONG_TIMEOUT_MS, - false, - true, + /*use_legacy_landlock*/ false, + /*network_access*/ true, ) .await, ".git write should be denied under bubblewrap", @@ -538,8 +545,8 @@ async fn sandbox_blocks_git_and_codex_writes_inside_writable_root() { ], &[tmpdir.path().to_path_buf()], LONG_TIMEOUT_MS, - false, - true, + /*use_legacy_landlock*/ false, + /*network_access*/ true, ) .await, ".codex write should be denied under bubblewrap", @@ -575,8 +582,8 @@ async fn sandbox_blocks_codex_symlink_replacement_attack() { ], &[tmpdir.path().to_path_buf()], LONG_TIMEOUT_MS, - false, - true, + /*use_legacy_landlock*/ false, + /*network_access*/ true, ) .await, ".codex symlink replacement should be denied", @@ -647,7 +654,7 @@ async fn sandbox_blocks_explicit_split_policy_carveouts_under_bwrap() { file_system_sandbox_policy, NetworkSandboxPolicy::Enabled, LONG_TIMEOUT_MS, - false, + /*use_legacy_landlock*/ false, ) .await, "explicit split-policy carveout should be denied under bubblewrap", @@ -729,7 +736,7 @@ async fn sandbox_reenables_writable_subpaths_under_unreadable_parents() { file_system_sandbox_policy, NetworkSandboxPolicy::Enabled, LONG_TIMEOUT_MS, - false, + /*use_legacy_landlock*/ false, ) .await .expect("nested writable carveout should execute under bubblewrap"); @@ -780,7 +787,7 @@ async fn sandbox_blocks_root_read_carveouts_under_bwrap() { file_system_sandbox_policy, NetworkSandboxPolicy::Enabled, LONG_TIMEOUT_MS, - false, + /*use_legacy_landlock*/ false, ) .await, "root-read carveout should be denied under bubblewrap", From 44b7bcfe9a4f6f82843f98852dda07b4952f4ffa Mon Sep 17 00:00:00 2001 From: viyatb-oai Date: Tue, 17 Mar 2026 19:04:19 -0700 Subject: [PATCH 07/11] refactor(sandboxing): use value helper for detached children --- codex-rs/core/src/exec.rs | 7 +++---- codex-rs/core/src/sandboxing/mod.rs | 32 ++++++++++++++--------------- 2 files changed, 19 insertions(+), 20 deletions(-) diff --git a/codex-rs/core/src/exec.rs b/codex-rs/core/src/exec.rs index e04d2909ff29..41966b22cd16 100644 --- a/codex-rs/core/src/exec.rs +++ b/codex-rs/core/src/exec.rs @@ -212,7 +212,7 @@ fn build_process_exec_tool_request( codex_linux_sandbox_exe: &Option, use_legacy_landlock: bool, ) -> Result { - let mut exec_req = build_exec_request( + build_exec_request( params, sandbox_policy, file_system_sandbox_policy, @@ -220,9 +220,8 @@ fn build_process_exec_tool_request( sandbox_cwd, codex_linux_sandbox_exe, use_legacy_landlock, - )?; - exec_req.allow_detached_children_in_linux_sandbox(); - Ok(exec_req) + ) + .map(crate::sandboxing::allow_detached_children_in_linux_sandbox) } /// Transform a portable exec request into the concrete argv/env that should be diff --git a/codex-rs/core/src/sandboxing/mod.rs b/codex-rs/core/src/sandboxing/mod.rs index 1a9c2d177173..123ef21f9688 100644 --- a/codex-rs/core/src/sandboxing/mod.rs +++ b/codex-rs/core/src/sandboxing/mod.rs @@ -78,23 +78,23 @@ pub struct ExecRequest { pub arg0: Option, } -impl ExecRequest { - pub(crate) fn allow_detached_children_in_linux_sandbox(&mut self) { - const ALLOW_DETACHED_CHILDREN: &str = "--allow-detached-children"; - - if self.sandbox != SandboxType::LinuxSeccomp { - return; - } - - if let Some(separator) = self.command.iter().position(|arg| arg == "--") - && !self.command[..separator] - .iter() - .any(|arg| arg == ALLOW_DETACHED_CHILDREN) - { - self.command - .insert(separator, ALLOW_DETACHED_CHILDREN.to_string()); - } +pub(crate) fn allow_detached_children_in_linux_sandbox( + mut exec_request: ExecRequest, +) -> ExecRequest { + const ALLOW_DETACHED_CHILDREN: &str = "--allow-detached-children"; + + if exec_request.sandbox == SandboxType::LinuxSeccomp + && let Some(separator) = exec_request.command.iter().position(|arg| arg == "--") + && !exec_request.command[..separator] + .iter() + .any(|arg| arg == ALLOW_DETACHED_CHILDREN) + { + exec_request + .command + .insert(separator, ALLOW_DETACHED_CHILDREN.to_string()); } + + exec_request } /// Bundled arguments for sandbox transformation. From 31f9a78640ea113a687a00c15a7e66afd2356fb9 Mon Sep 17 00:00:00 2001 From: viyatb-oai Date: Tue, 17 Mar 2026 20:31:03 -0700 Subject: [PATCH 08/11] refactor(linux-sandbox): simplify detached child builder --- .../app-server/src/codex_message_processor.rs | 1 + codex-rs/core/src/exec.rs | 33 +++++-------- codex-rs/core/src/landlock.rs | 10 ++++ codex-rs/core/src/landlock_tests.rs | 49 +++++++++++++++++-- codex-rs/core/src/sandboxing/mod.rs | 23 ++------- codex-rs/core/src/sandboxing/mod_tests.rs | 3 ++ codex-rs/core/src/tools/js_repl/mod.rs | 2 + .../tools/runtimes/shell/unix_escalation.rs | 2 + codex-rs/core/src/tools/sandboxing.rs | 2 + 9 files changed, 80 insertions(+), 45 deletions(-) diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index eb4e432c5bb6..6c4b11f66d34 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -1768,6 +1768,7 @@ impl CodexMessageProcessor { effective_network_sandbox_policy, sandbox_cwd.as_path(), &codex_linux_sandbox_exe, + codex_core::exec::LinuxSandboxDetachedChildren::Disallow, use_legacy_landlock, ) { Ok(exec_request) => { diff --git a/codex-rs/core/src/exec.rs b/codex-rs/core/src/exec.rs index 41966b22cd16..fabc266d9815 100644 --- a/codex-rs/core/src/exec.rs +++ b/codex-rs/core/src/exec.rs @@ -86,6 +86,12 @@ pub struct ExecParams { pub arg0: Option, } +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum LinuxSandboxDetachedChildren { + Allow, + Disallow, +} + fn select_process_exec_tool_sandbox_type( file_system_sandbox_policy: &FileSystemSandboxPolicy, network_sandbox_policy: NetworkSandboxPolicy, @@ -189,13 +195,14 @@ pub async fn process_exec_tool_call( use_legacy_landlock: bool, stdout_stream: Option, ) -> Result { - let exec_req = build_process_exec_tool_request( + let exec_req = build_exec_request( params, sandbox_policy, file_system_sandbox_policy, network_sandbox_policy, sandbox_cwd, codex_linux_sandbox_exe, + LinuxSandboxDetachedChildren::Allow, use_legacy_landlock, )?; @@ -203,29 +210,9 @@ pub async fn process_exec_tool_call( crate::sandboxing::execute_env(exec_req, stdout_stream).await } -fn build_process_exec_tool_request( - params: ExecParams, - sandbox_policy: &SandboxPolicy, - file_system_sandbox_policy: &FileSystemSandboxPolicy, - network_sandbox_policy: NetworkSandboxPolicy, - sandbox_cwd: &Path, - codex_linux_sandbox_exe: &Option, - use_legacy_landlock: bool, -) -> Result { - build_exec_request( - params, - sandbox_policy, - file_system_sandbox_policy, - network_sandbox_policy, - sandbox_cwd, - codex_linux_sandbox_exe, - use_legacy_landlock, - ) - .map(crate::sandboxing::allow_detached_children_in_linux_sandbox) -} - /// Transform a portable exec request into the concrete argv/env that should be /// spawned under the requested sandbox policy. +#[allow(clippy::too_many_arguments)] pub fn build_exec_request( params: ExecParams, sandbox_policy: &SandboxPolicy, @@ -233,6 +220,7 @@ pub fn build_exec_request( network_sandbox_policy: NetworkSandboxPolicy, sandbox_cwd: &Path, codex_linux_sandbox_exe: &Option, + linux_sandbox_detached_children: LinuxSandboxDetachedChildren, use_legacy_landlock: bool, ) -> Result { let windows_sandbox_level = params.windows_sandbox_level; @@ -292,6 +280,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(), + linux_sandbox_detached_children, use_legacy_landlock, windows_sandbox_level, windows_sandbox_private_desktop, diff --git a/codex-rs/core/src/landlock.rs b/codex-rs/core/src/landlock.rs index 6dcac030acd0..90154af6204f 100644 --- a/codex-rs/core/src/landlock.rs +++ b/codex-rs/core/src/landlock.rs @@ -1,3 +1,4 @@ +use crate::exec::LinuxSandboxDetachedChildren; use crate::protocol::SandboxPolicy; use crate::spawn::SpawnChildRequest; use crate::spawn::StdioPolicy; @@ -45,6 +46,7 @@ where sandbox_policy_cwd, use_legacy_landlock, allow_network_for_proxy(/*enforce_managed_network*/ false), + LinuxSandboxDetachedChildren::Disallow, ); let arg0 = Some("codex-linux-sandbox"); spawn_child_async(SpawnChildRequest { @@ -84,6 +86,7 @@ pub(crate) fn create_linux_sandbox_command_args_for_policies( sandbox_policy_cwd: &Path, use_legacy_landlock: bool, allow_network_for_proxy: bool, + detached_children: LinuxSandboxDetachedChildren, ) -> Vec { let sandbox_policy_json = serde_json::to_string(sandbox_policy) .unwrap_or_else(|err| panic!("failed to serialize sandbox policy: {err}")); @@ -118,6 +121,9 @@ pub(crate) fn create_linux_sandbox_command_args_for_policies( if allow_network_for_proxy { linux_cmd.push("--allow-network-for-proxy".to_string()); } + if detached_children == LinuxSandboxDetachedChildren::Allow { + linux_cmd.push("--allow-detached-children".to_string()); + } linux_cmd.push("--".to_string()); linux_cmd.extend(command); linux_cmd @@ -132,6 +138,7 @@ pub(crate) fn create_linux_sandbox_command_args( sandbox_policy_cwd: &Path, use_legacy_landlock: bool, allow_network_for_proxy: bool, + detached_children: LinuxSandboxDetachedChildren, ) -> Vec { let command_cwd = command_cwd .to_str() @@ -154,6 +161,9 @@ pub(crate) fn create_linux_sandbox_command_args( if allow_network_for_proxy { linux_cmd.push("--allow-network-for-proxy".to_string()); } + if detached_children == LinuxSandboxDetachedChildren::Allow { + linux_cmd.push("--allow-detached-children".to_string()); + } // Separator so that command arguments starting with `-` are not parsed as // options of the helper itself. diff --git a/codex-rs/core/src/landlock_tests.rs b/codex-rs/core/src/landlock_tests.rs index 454878cd511d..b53b59897ff7 100644 --- a/codex-rs/core/src/landlock_tests.rs +++ b/codex-rs/core/src/landlock_tests.rs @@ -7,14 +7,27 @@ fn legacy_landlock_flag_is_included_when_requested() { let command_cwd = Path::new("/tmp/link"); let cwd = Path::new("/tmp"); - let default_bwrap = - create_linux_sandbox_command_args(command.clone(), command_cwd, cwd, false, false); + let default_bwrap = create_linux_sandbox_command_args( + command.clone(), + command_cwd, + cwd, + false, + false, + LinuxSandboxDetachedChildren::Disallow, + ); assert_eq!( default_bwrap.contains(&"--use-legacy-landlock".to_string()), false ); - let legacy_landlock = create_linux_sandbox_command_args(command, command_cwd, cwd, true, false); + let legacy_landlock = create_linux_sandbox_command_args( + command, + command_cwd, + cwd, + true, + false, + LinuxSandboxDetachedChildren::Disallow, + ); assert_eq!( legacy_landlock.contains(&"--use-legacy-landlock".to_string()), true @@ -27,13 +40,40 @@ fn proxy_flag_is_included_when_requested() { let command_cwd = Path::new("/tmp/link"); let cwd = Path::new("/tmp"); - let args = create_linux_sandbox_command_args(command, command_cwd, cwd, true, true); + let args = create_linux_sandbox_command_args( + command, + command_cwd, + cwd, + true, + true, + LinuxSandboxDetachedChildren::Disallow, + ); assert_eq!( args.contains(&"--allow-network-for-proxy".to_string()), true ); } +#[test] +fn detached_children_flag_is_included_when_requested() { + let command = vec!["/bin/true".to_string()]; + let command_cwd = Path::new("/tmp/link"); + let cwd = Path::new("/tmp"); + + let args = create_linux_sandbox_command_args( + command, + command_cwd, + cwd, + false, + false, + LinuxSandboxDetachedChildren::Allow, + ); + assert_eq!( + args.contains(&"--allow-detached-children".to_string()), + true + ); +} + #[test] fn split_policy_flags_are_included() { let command = vec!["/bin/true".to_string()]; @@ -52,6 +92,7 @@ fn split_policy_flags_are_included() { cwd, true, false, + LinuxSandboxDetachedChildren::Disallow, ); assert_eq!( diff --git a/codex-rs/core/src/sandboxing/mod.rs b/codex-rs/core/src/sandboxing/mod.rs index 123ef21f9688..9bc1265b991d 100644 --- a/codex-rs/core/src/sandboxing/mod.rs +++ b/codex-rs/core/src/sandboxing/mod.rs @@ -10,6 +10,7 @@ pub(crate) mod macos_permissions; use crate::exec::ExecExpiration; use crate::exec::ExecToolCallOutput; +use crate::exec::LinuxSandboxDetachedChildren; use crate::exec::SandboxType; use crate::exec::StdoutStream; use crate::exec::execute_exec_request; @@ -78,25 +79,6 @@ pub struct ExecRequest { pub arg0: Option, } -pub(crate) fn allow_detached_children_in_linux_sandbox( - mut exec_request: ExecRequest, -) -> ExecRequest { - const ALLOW_DETACHED_CHILDREN: &str = "--allow-detached-children"; - - if exec_request.sandbox == SandboxType::LinuxSeccomp - && let Some(separator) = exec_request.command.iter().position(|arg| arg == "--") - && !exec_request.command[..separator] - .iter() - .any(|arg| arg == ALLOW_DETACHED_CHILDREN) - { - exec_request - .command - .insert(separator, ALLOW_DETACHED_CHILDREN.to_string()); - } - - exec_request -} - /// Bundled arguments for sandbox transformation. /// /// This keeps call sites self-documenting when several fields are optional. @@ -114,6 +96,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 linux_sandbox_detached_children: LinuxSandboxDetachedChildren, pub use_legacy_landlock: bool, pub windows_sandbox_level: WindowsSandboxLevel, pub windows_sandbox_private_desktop: bool, @@ -612,6 +595,7 @@ impl SandboxManager { #[cfg(target_os = "macos")] macos_seatbelt_profile_extensions, codex_linux_sandbox_exe, + linux_sandbox_detached_children, use_legacy_landlock, windows_sandbox_level, windows_sandbox_private_desktop, @@ -698,6 +682,7 @@ impl SandboxManager { sandbox_policy_cwd, use_legacy_landlock, allow_proxy_network, + linux_sandbox_detached_children, ); let mut full_command = Vec::with_capacity(1 + args.len()); full_command.push(exe.to_string_lossy().to_string()); diff --git a/codex-rs/core/src/sandboxing/mod_tests.rs b/codex-rs/core/src/sandboxing/mod_tests.rs index 4d45dfb0080b..17f124a47dfc 100644 --- a/codex-rs/core/src/sandboxing/mod_tests.rs +++ b/codex-rs/core/src/sandboxing/mod_tests.rs @@ -174,6 +174,7 @@ fn transform_preserves_unrestricted_file_system_policy_for_restricted_network() #[cfg(target_os = "macos")] macos_seatbelt_profile_extensions: None, codex_linux_sandbox_exe: None, + linux_sandbox_detached_children: crate::exec::LinuxSandboxDetachedChildren::Disallow, use_legacy_landlock: false, windows_sandbox_level: WindowsSandboxLevel::Disabled, windows_sandbox_private_desktop: false, @@ -543,6 +544,7 @@ fn transform_additional_permissions_enable_network_for_external_sandbox() { #[cfg(target_os = "macos")] macos_seatbelt_profile_extensions: None, codex_linux_sandbox_exe: None, + linux_sandbox_detached_children: crate::exec::LinuxSandboxDetachedChildren::Disallow, use_legacy_landlock: false, windows_sandbox_level: WindowsSandboxLevel::Disabled, windows_sandbox_private_desktop: false, @@ -616,6 +618,7 @@ fn transform_additional_permissions_preserves_denied_entries() { #[cfg(target_os = "macos")] macos_seatbelt_profile_extensions: None, codex_linux_sandbox_exe: None, + linux_sandbox_detached_children: crate::exec::LinuxSandboxDetachedChildren::Disallow, use_legacy_landlock: false, windows_sandbox_level: WindowsSandboxLevel::Disabled, windows_sandbox_private_desktop: false, diff --git a/codex-rs/core/src/tools/js_repl/mod.rs b/codex-rs/core/src/tools/js_repl/mod.rs index 392f311ce21f..b23122cf25fe 100644 --- a/codex-rs/core/src/tools/js_repl/mod.rs +++ b/codex-rs/core/src/tools/js_repl/mod.rs @@ -1069,6 +1069,8 @@ impl JsReplManager { #[cfg(target_os = "macos")] macos_seatbelt_profile_extensions: None, codex_linux_sandbox_exe: turn.codex_linux_sandbox_exe.as_ref(), + linux_sandbox_detached_children: + crate::exec::LinuxSandboxDetachedChildren::Disallow, use_legacy_landlock: turn.features.use_legacy_landlock(), windows_sandbox_level: turn.windows_sandbox_level, windows_sandbox_private_desktop: turn 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 89bf4d424c3a..32ec7bad60e2 100644 --- a/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs +++ b/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs @@ -1060,6 +1060,8 @@ impl CoreShellCommandExecutor { #[cfg(target_os = "macos")] macos_seatbelt_profile_extensions, codex_linux_sandbox_exe: self.codex_linux_sandbox_exe.as_ref(), + linux_sandbox_detached_children: + crate::exec::LinuxSandboxDetachedChildren::Disallow, 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 bf386a12d0b6..07fb7ec17eae 100644 --- a/codex-rs/core/src/tools/sandboxing.rs +++ b/codex-rs/core/src/tools/sandboxing.rs @@ -355,6 +355,8 @@ impl<'a> SandboxAttempt<'a> { #[cfg(target_os = "macos")] macos_seatbelt_profile_extensions: None, codex_linux_sandbox_exe: self.codex_linux_sandbox_exe, + linux_sandbox_detached_children: + crate::exec::LinuxSandboxDetachedChildren::Disallow, use_legacy_landlock: self.use_legacy_landlock, windows_sandbox_level: self.windows_sandbox_level, windows_sandbox_private_desktop: self.windows_sandbox_private_desktop, From a6397bbd16454bb542e87a6746a0c27235d756d6 Mon Sep 17 00:00:00 2001 From: viyatb-oai Date: Tue, 17 Mar 2026 23:24:44 -0700 Subject: [PATCH 09/11] test(linux-sandbox): annotate landlock bool args --- codex-rs/core/src/landlock_tests.rs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/codex-rs/core/src/landlock_tests.rs b/codex-rs/core/src/landlock_tests.rs index b53b59897ff7..436170f1c8f5 100644 --- a/codex-rs/core/src/landlock_tests.rs +++ b/codex-rs/core/src/landlock_tests.rs @@ -11,8 +11,8 @@ fn legacy_landlock_flag_is_included_when_requested() { command.clone(), command_cwd, cwd, - false, - false, + /*use_legacy_landlock*/ false, + /*allow_network_for_proxy*/ false, LinuxSandboxDetachedChildren::Disallow, ); assert_eq!( @@ -24,8 +24,8 @@ fn legacy_landlock_flag_is_included_when_requested() { command, command_cwd, cwd, - true, - false, + /*use_legacy_landlock*/ true, + /*allow_network_for_proxy*/ false, LinuxSandboxDetachedChildren::Disallow, ); assert_eq!( @@ -44,8 +44,8 @@ fn proxy_flag_is_included_when_requested() { command, command_cwd, cwd, - true, - true, + /*use_legacy_landlock*/ true, + /*allow_network_for_proxy*/ true, LinuxSandboxDetachedChildren::Disallow, ); assert_eq!( @@ -64,8 +64,8 @@ fn detached_children_flag_is_included_when_requested() { command, command_cwd, cwd, - false, - false, + /*use_legacy_landlock*/ false, + /*allow_network_for_proxy*/ false, LinuxSandboxDetachedChildren::Allow, ); assert_eq!( @@ -90,8 +90,8 @@ fn split_policy_flags_are_included() { &file_system_sandbox_policy, network_sandbox_policy, cwd, - true, - false, + /*use_legacy_landlock*/ true, + /*allow_network_for_proxy*/ false, LinuxSandboxDetachedChildren::Disallow, ); From 96d855a45699b18513219f415a42adbde34fabfe Mon Sep 17 00:00:00 2001 From: viyatb-oai Date: Wed, 18 Mar 2026 00:48:40 -0700 Subject: [PATCH 10/11] fix(cli): update debug sandbox linux args --- codex-rs/cli/src/debug_sandbox.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/codex-rs/cli/src/debug_sandbox.rs b/codex-rs/cli/src/debug_sandbox.rs index 64169327f55c..b32747d1aefa 100644 --- a/codex-rs/cli/src/debug_sandbox.rs +++ b/codex-rs/cli/src/debug_sandbox.rs @@ -10,6 +10,7 @@ use codex_core::config::Config; use codex_core::config::ConfigBuilder; use codex_core::config::ConfigOverrides; use codex_core::config::NetworkProxyAuditMetadata; +use codex_core::exec::LinuxSandboxDetachedChildren; use codex_core::exec_env::create_env; use codex_core::landlock::create_linux_sandbox_command_args_for_policies; #[cfg(target_os = "macos")] @@ -251,7 +252,7 @@ async fn run_command_under_sandbox( &config.permissions.file_system_sandbox_policy, config.permissions.network_sandbox_policy, sandbox_policy_cwd.as_path(), - false, + /*enforce_managed_network*/ false, network.as_ref(), None, ); @@ -287,6 +288,7 @@ async fn run_command_under_sandbox( sandbox_policy_cwd.as_path(), use_legacy_landlock, /*allow_network_for_proxy*/ false, + LinuxSandboxDetachedChildren::Disallow, ); let network_policy = config.permissions.network_sandbox_policy; spawn_debug_sandbox_child( @@ -496,7 +498,7 @@ mod tests { let legacy_config = build_debug_sandbox_config( Vec::new(), ConfigOverrides { - sandbox_mode: Some(create_sandbox_mode(false)), + sandbox_mode: Some(create_sandbox_mode(/*full_auto*/ false)), ..Default::default() }, Some(codex_home_path.clone()), @@ -506,7 +508,7 @@ mod tests { let config = load_debug_sandbox_config_with_codex_home( Vec::new(), None, - false, + /*full_auto*/ false, Some(codex_home_path), ) .await?; @@ -540,7 +542,7 @@ mod tests { let err = load_debug_sandbox_config_with_codex_home( Vec::new(), None, - true, + /*full_auto*/ true, Some(codex_home.path().to_path_buf()), ) .await From 4b36e32f13d607799602da16db6068509e4be450 Mon Sep 17 00:00:00 2001 From: viyatb-oai Date: Tue, 7 Apr 2026 10:41:53 -0700 Subject: [PATCH 11/11] test: fix Linux-only sandbox test initializers Co-authored-by: Codex noreply@openai.com --- codex-rs/linux-sandbox/src/linux_run_main_tests.rs | 1 + codex-rs/sandboxing/src/manager_tests.rs | 1 + 2 files changed, 2 insertions(+) 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 bb5df87888fc..9b6cad7c9611 100644 --- a/codex-rs/linux-sandbox/src/linux_run_main_tests.rs +++ b/codex-rs/linux-sandbox/src/linux_run_main_tests.rs @@ -93,6 +93,7 @@ fn rewrites_inner_command_path_when_bwrap_lacks_argv0() { BwrapOptions { mount_proc: true, network_mode: BwrapNetworkMode::FullAccess, + process_lifetime: BwrapProcessLifetime::TerminateWithParent, }, ) .args; diff --git a/codex-rs/sandboxing/src/manager_tests.rs b/codex-rs/sandboxing/src/manager_tests.rs index 234c68fd5b6e..328043095d30 100644 --- a/codex-rs/sandboxing/src/manager_tests.rs +++ b/codex-rs/sandboxing/src/manager_tests.rs @@ -272,6 +272,7 @@ fn transform_linux_seccomp_request( network: None, sandbox_policy_cwd: cwd.as_path(), codex_linux_sandbox_exe: Some(codex_linux_sandbox_exe), + linux_sandbox_detached_children: LinuxSandboxDetachedChildren::Disallow, use_legacy_landlock: false, windows_sandbox_level: WindowsSandboxLevel::Disabled, windows_sandbox_private_desktop: false,