diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index 928272082884..8d9193d92849 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -291,6 +291,7 @@ use codex_rmcp_client::perform_oauth_login_return_url; use codex_rollout::state_db::StateDbHandle; use codex_rollout::state_db::get_state_db; use codex_rollout::state_db::reconcile_rollout; +use codex_sandboxing::LinuxSandboxDetachedChildren; use codex_state::StateRuntime; use codex_state::ThreadMetadata; use codex_state::ThreadMetadataBuilder; @@ -1978,6 +1979,7 @@ impl CodexMessageProcessor { effective_network_sandbox_policy, sandbox_cwd.as_path(), &codex_linux_sandbox_exe, + LinuxSandboxDetachedChildren::Disallow, use_legacy_landlock, ) { Ok(exec_request) => { diff --git a/codex-rs/cli/src/debug_sandbox.rs b/codex-rs/cli/src/debug_sandbox.rs index adb205eefab9..1d96bc886a97 100644 --- a/codex-rs/cli/src/debug_sandbox.rs +++ b/codex-rs/cli/src/debug_sandbox.rs @@ -16,6 +16,7 @@ use codex_core::spawn::CODEX_SANDBOX_ENV_VAR; use codex_core::spawn::CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR; use codex_protocol::config_types::SandboxMode; use codex_protocol::permissions::NetworkSandboxPolicy; +use codex_sandboxing::LinuxSandboxDetachedChildren; use codex_sandboxing::landlock::create_linux_sandbox_command_args_for_policies; #[cfg(target_os = "macos")] use codex_sandboxing::seatbelt::create_seatbelt_command_args_for_policies; @@ -289,6 +290,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( diff --git a/codex-rs/core/src/exec.rs b/codex-rs/core/src/exec.rs index 8a5572a36ed0..6bed8f862843 100644 --- a/codex-rs/core/src/exec.rs +++ b/codex-rs/core/src/exec.rs @@ -38,6 +38,7 @@ use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::ExecCommandOutputDeltaEvent; use codex_protocol::protocol::ExecOutputStream; use codex_protocol::protocol::SandboxPolicy; +use codex_sandboxing::LinuxSandboxDetachedChildren; use codex_sandboxing::SandboxCommand; use codex_sandboxing::SandboxManager; use codex_sandboxing::SandboxTransformRequest; @@ -220,6 +221,7 @@ pub async fn process_exec_tool_call( network_sandbox_policy, sandbox_cwd, codex_linux_sandbox_exe, + LinuxSandboxDetachedChildren::Allow, use_legacy_landlock, )?; @@ -229,6 +231,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, @@ -236,6 +239,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; @@ -294,6 +298,7 @@ pub fn build_exec_request( network: network.as_ref(), sandbox_policy_cwd: sandbox_cwd, 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 95f107be9c7d..050ab83ffaaa 100644 --- a/codex-rs/core/src/landlock.rs +++ b/codex-rs/core/src/landlock.rs @@ -5,6 +5,7 @@ use codex_network_proxy::NetworkProxy; use codex_protocol::permissions::FileSystemSandboxPolicy; use codex_protocol::permissions::NetworkSandboxPolicy; use codex_protocol::protocol::SandboxPolicy; +use codex_sandboxing::LinuxSandboxDetachedChildren; use codex_sandboxing::landlock::CODEX_LINUX_SANDBOX_ARG0; use codex_sandboxing::landlock::allow_network_for_proxy; use codex_sandboxing::landlock::create_linux_sandbox_command_args_for_policies; @@ -48,6 +49,7 @@ where sandbox_policy_cwd, use_legacy_landlock, allow_network_for_proxy(/*enforce_managed_network*/ false), + LinuxSandboxDetachedChildren::Disallow, ); let codex_linux_sandbox_exe = codex_linux_sandbox_exe.as_ref(); // Preserve the helper alias when we already have it; otherwise force argv0 diff --git a/codex-rs/core/src/tools/handlers/multi_agents_tests.rs b/codex-rs/core/src/tools/handlers/multi_agents_tests.rs index 7fec0893209e..5f09c248a406 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents_tests.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents_tests.rs @@ -129,6 +129,7 @@ async fn wait_for_turn_aborted( EventMsg::TurnAborted(TurnAbortedEvent { turn_id: Some(ref turn_id), ref reason, + .. }) if turn_id == expected_turn_id && *reason == expected_reason ) { break; diff --git a/codex-rs/core/src/tools/js_repl/mod.rs b/codex-rs/core/src/tools/js_repl/mod.rs index 0cfbd2a60ccb..8ec4a8f65c18 100644 --- a/codex-rs/core/src/tools/js_repl/mod.rs +++ b/codex-rs/core/src/tools/js_repl/mod.rs @@ -41,6 +41,7 @@ use crate::original_image_detail::normalize_output_image_detail; use crate::sandboxing::ExecOptions; use crate::tools::ToolRouter; use crate::tools::context::SharedTurnDiffTracker; +use codex_sandboxing::LinuxSandboxDetachedChildren; use codex_sandboxing::SandboxCommand; use codex_sandboxing::SandboxManager; use codex_sandboxing::SandboxTransformRequest; @@ -1069,6 +1070,7 @@ impl JsReplManager { network: None, sandbox_policy_cwd: &turn.cwd, codex_linux_sandbox_exe: turn.codex_linux_sandbox_exe.as_ref(), + linux_sandbox_detached_children: 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 b52fd8c863b1..9c1da2d46eca 100644 --- a/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs +++ b/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs @@ -32,6 +32,7 @@ use codex_protocol::protocol::GuardianCommandSource; use codex_protocol::protocol::NetworkPolicyRuleAction; use codex_protocol::protocol::ReviewDecision; use codex_protocol::protocol::SandboxPolicy; +use codex_sandboxing::LinuxSandboxDetachedChildren; use codex_sandboxing::SandboxCommand; use codex_sandboxing::SandboxManager; use codex_sandboxing::SandboxTransformRequest; @@ -845,6 +846,7 @@ impl CoreShellCommandExecutor { network: self.network.as_ref(), sandbox_policy_cwd: &self.sandbox_policy_cwd, codex_linux_sandbox_exe: self.codex_linux_sandbox_exe.as_ref(), + linux_sandbox_detached_children: 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 c2671aabf0cf..7becf255ff40 100644 --- a/codex-rs/core/src/tools/sandboxing.rs +++ b/codex-rs/core/src/tools/sandboxing.rs @@ -21,6 +21,7 @@ use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::ReviewDecision; #[cfg(test)] use codex_protocol::protocol::SandboxPolicy; +use codex_sandboxing::LinuxSandboxDetachedChildren; use codex_sandboxing::SandboxCommand; use codex_sandboxing::SandboxManager; use codex_sandboxing::SandboxTransformError; @@ -349,6 +350,7 @@ impl<'a> SandboxAttempt<'a> { network, sandbox_policy_cwd: self.sandbox_cwd, codex_linux_sandbox_exe: self.codex_linux_sandbox_exe, + linux_sandbox_detached_children: 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, diff --git a/codex-rs/linux-sandbox/src/bwrap.rs b/codex-rs/linux-sandbox/src/bwrap.rs index 0709324f64aa..075a09c2b6e5 100644 --- a/codex-rs/linux-sandbox/src/bwrap.rs +++ b/codex-rs/linux-sandbox/src/bwrap.rs @@ -46,6 +46,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 { @@ -53,10 +56,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 { @@ -121,7 +132,6 @@ 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(), @@ -130,6 +140,9 @@ fn create_bwrap_flags_full_filesystem(command: Vec, options: BwrapOption "--unshare-user".to_string(), "--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()); } @@ -160,12 +173,14 @@ 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()); 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()); } @@ -620,6 +635,7 @@ mod tests { BwrapOptions { mount_proc: true, network_mode: BwrapNetworkMode::FullAccess, + process_lifetime: BwrapProcessLifetime::TerminateWithParent, }, ) .expect("create bwrap args"); @@ -638,6 +654,7 @@ mod tests { BwrapOptions { mount_proc: true, network_mode: BwrapNetworkMode::ProxyOnly, + process_lifetime: BwrapProcessLifetime::TerminateWithParent, }, ) .expect("create bwrap args"); @@ -646,12 +663,12 @@ mod tests { args.args, vec![ "--new-session".to_string(), - "--die-with-parent".to_string(), "--bind".to_string(), "/".to_string(), "/".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(), @@ -661,6 +678,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 8f39837de1b9..fd806f26bb99 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::launcher::exec_bwrap; @@ -80,6 +81,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`. @@ -109,6 +117,7 @@ pub fn run_main() -> ! { apply_seccomp_then_exec, allow_network_for_proxy, proxy_route_spec, + allow_detached_children, no_proc, command, } = LandlockCommand::parse(); @@ -196,14 +205,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, + options, ); } @@ -402,32 +418,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, + 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, - }; let mut bwrap_args = build_bwrap_argv( inner, file_system_sandbox_policy, @@ -547,6 +555,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 96c3c8a737af..9b6cad7c9611 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; @@ -61,7 +64,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(), @@ -69,6 +71,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(), @@ -90,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; @@ -157,6 +161,7 @@ fn inserts_unshare_net_when_network_isolation_requested() { BwrapOptions { mount_proc: true, network_mode: BwrapNetworkMode::Isolated, + process_lifetime: BwrapProcessLifetime::TerminateWithParent, }, ) .args; @@ -174,12 +179,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( diff --git a/codex-rs/linux-sandbox/tests/suite/landlock.rs b/codex-rs/linux-sandbox/tests/suite/landlock.rs index ffe0d9e57dc9..d397895cbe25 100644 --- a/codex-rs/linux-sandbox/tests/suite/landlock.rs +++ b/codex-rs/linux-sandbox/tests/suite/landlock.rs @@ -22,6 +22,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. @@ -318,6 +319,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(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 sandbox_ignores_missing_writable_roots_under_bwrap() { if should_skip_bwrap_tests().await { diff --git a/codex-rs/sandboxing/src/landlock.rs b/codex-rs/sandboxing/src/landlock.rs index d948d23d1fce..3910642fb901 100644 --- a/codex-rs/sandboxing/src/landlock.rs +++ b/codex-rs/sandboxing/src/landlock.rs @@ -1,3 +1,4 @@ +use crate::LinuxSandboxDetachedChildren; use codex_protocol::permissions::FileSystemSandboxPolicy; use codex_protocol::permissions::NetworkSandboxPolicy; use codex_protocol::protocol::SandboxPolicy; @@ -31,6 +32,7 @@ pub 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}")); @@ -65,6 +67,9 @@ pub 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 @@ -79,6 +84,7 @@ 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() @@ -101,6 +107,9 @@ 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/sandboxing/src/landlock_tests.rs b/codex-rs/sandboxing/src/landlock_tests.rs index b1dd6236ab0f..8a201e2a29c5 100644 --- a/codex-rs/sandboxing/src/landlock_tests.rs +++ b/codex-rs/sandboxing/src/landlock_tests.rs @@ -13,6 +13,7 @@ fn legacy_landlock_flag_is_included_when_requested() { cwd, /*use_legacy_landlock*/ false, /*allow_network_for_proxy*/ false, + LinuxSandboxDetachedChildren::Disallow, ); assert_eq!( default_bwrap.contains(&"--use-legacy-landlock".to_string()), @@ -25,6 +26,7 @@ fn legacy_landlock_flag_is_included_when_requested() { cwd, /*use_legacy_landlock*/ true, /*allow_network_for_proxy*/ false, + LinuxSandboxDetachedChildren::Disallow, ); assert_eq!( legacy_landlock.contains(&"--use-legacy-landlock".to_string()), @@ -44,6 +46,7 @@ fn proxy_flag_is_included_when_requested() { cwd, /*use_legacy_landlock*/ true, /*allow_network_for_proxy*/ true, + LinuxSandboxDetachedChildren::Disallow, ); assert_eq!( args.contains(&"--allow-network-for-proxy".to_string()), @@ -51,6 +54,26 @@ fn proxy_flag_is_included_when_requested() { ); } +#[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, + /*use_legacy_landlock*/ false, + /*allow_network_for_proxy*/ 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()]; @@ -69,6 +92,7 @@ fn split_policy_flags_are_included() { cwd, /*use_legacy_landlock*/ true, /*allow_network_for_proxy*/ false, + LinuxSandboxDetachedChildren::Disallow, ); assert_eq!( diff --git a/codex-rs/sandboxing/src/lib.rs b/codex-rs/sandboxing/src/lib.rs index 244f65bb0a17..71346966d866 100644 --- a/codex-rs/sandboxing/src/lib.rs +++ b/codex-rs/sandboxing/src/lib.rs @@ -10,6 +10,7 @@ pub mod seatbelt; pub use bwrap::find_system_bwrap_in_path; #[cfg(target_os = "linux")] pub use bwrap::system_bwrap_warning; +pub use manager::LinuxSandboxDetachedChildren; pub use manager::SandboxCommand; pub use manager::SandboxExecRequest; pub use manager::SandboxManager; diff --git a/codex-rs/sandboxing/src/manager.rs b/codex-rs/sandboxing/src/manager.rs index c89e7cd9be65..8766f71999ec 100644 --- a/codex-rs/sandboxing/src/manager.rs +++ b/codex-rs/sandboxing/src/manager.rs @@ -46,6 +46,13 @@ pub enum SandboxablePreference { Forbid, } +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub enum LinuxSandboxDetachedChildren { + Allow, + #[default] + Disallow, +} + pub fn get_platform_sandbox(windows_sandbox_enabled: bool) -> Option { if cfg!(target_os = "macos") { Some(SandboxType::MacosSeatbelt) @@ -101,6 +108,7 @@ pub struct SandboxTransformRequest<'a> { pub network: Option<&'a NetworkProxy>, pub sandbox_policy_cwd: &'a Path, 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, @@ -178,6 +186,7 @@ impl SandboxManager { network, sandbox_policy_cwd, codex_linux_sandbox_exe, + linux_sandbox_detached_children, use_legacy_landlock, windows_sandbox_level, windows_sandbox_private_desktop, @@ -228,6 +237,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(os_string_to_command_component(exe.as_os_str().to_owned())); diff --git a/codex-rs/sandboxing/src/manager_tests.rs b/codex-rs/sandboxing/src/manager_tests.rs index 6fc34c96378c..328043095d30 100644 --- a/codex-rs/sandboxing/src/manager_tests.rs +++ b/codex-rs/sandboxing/src/manager_tests.rs @@ -1,3 +1,4 @@ +use super::LinuxSandboxDetachedChildren; use super::SandboxCommand; use super::SandboxManager; use super::SandboxTransformRequest; @@ -94,6 +95,7 @@ fn transform_preserves_unrestricted_file_system_policy_for_restricted_network() network: None, sandbox_policy_cwd: cwd.as_path(), codex_linux_sandbox_exe: None, + linux_sandbox_detached_children: LinuxSandboxDetachedChildren::Disallow, use_legacy_landlock: false, windows_sandbox_level: WindowsSandboxLevel::Disabled, windows_sandbox_private_desktop: false, @@ -146,6 +148,7 @@ fn transform_additional_permissions_enable_network_for_external_sandbox() { network: None, sandbox_policy_cwd: cwd.as_path(), codex_linux_sandbox_exe: None, + linux_sandbox_detached_children: LinuxSandboxDetachedChildren::Disallow, use_legacy_landlock: false, windows_sandbox_level: WindowsSandboxLevel::Disabled, windows_sandbox_private_desktop: false, @@ -214,6 +217,7 @@ fn transform_additional_permissions_preserves_denied_entries() { network: None, sandbox_policy_cwd: cwd.as_path(), codex_linux_sandbox_exe: None, + linux_sandbox_detached_children: LinuxSandboxDetachedChildren::Disallow, use_legacy_landlock: false, windows_sandbox_level: WindowsSandboxLevel::Disabled, windows_sandbox_private_desktop: false, @@ -268,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,