diff --git a/codex-rs/linux-sandbox/src/bwrap_output_relay.rs b/codex-rs/linux-sandbox/src/bwrap_output_relay.rs new file mode 100644 index 0000000000..307f5385da --- /dev/null +++ b/codex-rs/linux-sandbox/src/bwrap_output_relay.rs @@ -0,0 +1,167 @@ +use std::os::fd::AsRawFd; +use std::os::fd::FromRawFd; +use std::os::fd::OwnedFd; + +/// Relays output from a bubblewrap process without waiting for orphaned +/// descendants that keep bubblewrap's PID-namespace reaper alive. +pub(crate) struct BwrapOutputRelay { + stdout_read: OwnedFd, + stdout_write: OwnedFd, + stderr_read: OwnedFd, + stderr_write: OwnedFd, +} + +impl BwrapOutputRelay { + pub(crate) fn new() -> Self { + let (stdout_read, stdout_write) = create_pipe("stdout"); + let (stderr_read, stderr_write) = create_pipe("stderr"); + Self { + stdout_read, + stdout_write, + stderr_read, + stderr_write, + } + } + + pub(crate) fn redirect_child_output(self) { + let Self { + stdout_read, + stdout_write, + stderr_read, + stderr_write, + } = self; + drop(stdout_read); + drop(stderr_read); + dup2_or_panic(stdout_write.as_raw_fd(), libc::STDOUT_FILENO, "stdout"); + dup2_or_panic(stderr_write.as_raw_fd(), libc::STDERR_FILENO, "stderr"); + } + + pub(crate) fn forward_until_child_exit(self, pid: libc::pid_t) -> libc::c_int { + let Self { + stdout_read, + stdout_write, + stderr_read, + stderr_write, + } = self; + drop(stdout_write); + drop(stderr_write); + set_nonblocking(stdout_read.as_raw_fd(), "stdout"); + set_nonblocking(stderr_read.as_raw_fd(), "stderr"); + + loop { + forward_available(stdout_read.as_raw_fd(), libc::STDOUT_FILENO, "stdout"); + forward_available(stderr_read.as_raw_fd(), libc::STDERR_FILENO, "stderr"); + if let Some(status) = try_wait_for_child(pid) { + forward_available(stdout_read.as_raw_fd(), libc::STDOUT_FILENO, "stdout"); + forward_available(stderr_read.as_raw_fd(), libc::STDERR_FILENO, "stderr"); + return status; + } + poll_for_output(&stdout_read, &stderr_read); + } + } +} + +fn create_pipe(stream: &str) -> (OwnedFd, OwnedFd) { + let mut pipe_fds = [0; 2]; + if unsafe { libc::pipe2(pipe_fds.as_mut_ptr(), libc::O_CLOEXEC) } < 0 { + let err = std::io::Error::last_os_error(); + panic!("failed to create bubblewrap {stream} relay pipe: {err}"); + } + + // SAFETY: pipe2 initialized both returned file descriptors and transfers + // their ownership to this function. + unsafe { + ( + OwnedFd::from_raw_fd(pipe_fds[0]), + OwnedFd::from_raw_fd(pipe_fds[1]), + ) + } +} + +fn dup2_or_panic(source: libc::c_int, target: libc::c_int, stream: &str) { + if unsafe { libc::dup2(source, target) } < 0 { + let err = std::io::Error::last_os_error(); + panic!("failed to redirect bubblewrap {stream}: {err}"); + } +} + +fn set_nonblocking(fd: libc::c_int, stream: &str) { + let flags = unsafe { libc::fcntl(fd, libc::F_GETFL) }; + if flags < 0 || unsafe { libc::fcntl(fd, libc::F_SETFL, flags | libc::O_NONBLOCK) } < 0 { + let err = std::io::Error::last_os_error(); + panic!("failed to configure bubblewrap {stream} relay pipe: {err}"); + } +} + +fn forward_available(source: libc::c_int, destination: libc::c_int, stream: &str) { + let mut buffer = [0_u8; 8192]; + loop { + let bytes_read = unsafe { libc::read(source, buffer.as_mut_ptr().cast(), buffer.len()) }; + if bytes_read > 0 { + write_all(destination, &buffer[..bytes_read as usize], stream); + continue; + } + if bytes_read == 0 { + return; + } + let err = std::io::Error::last_os_error(); + match err.kind() { + std::io::ErrorKind::Interrupted => continue, + std::io::ErrorKind::WouldBlock => return, + _ => panic!("failed to read bubblewrap {stream} output: {err}"), + } + } +} + +fn write_all(fd: libc::c_int, mut bytes: &[u8], stream: &str) { + while !bytes.is_empty() { + let bytes_written = unsafe { libc::write(fd, bytes.as_ptr().cast(), bytes.len()) }; + if bytes_written > 0 { + bytes = &bytes[bytes_written as usize..]; + continue; + } + let err = std::io::Error::last_os_error(); + if err.kind() == std::io::ErrorKind::Interrupted { + continue; + } + panic!("failed to forward bubblewrap {stream} output: {err}"); + } +} + +fn try_wait_for_child(pid: libc::pid_t) -> Option { + let mut status = 0; + let result = unsafe { libc::waitpid(pid, &mut status, libc::WNOHANG) }; + if result == pid { + return Some(status); + } + if result == 0 { + return None; + } + let err = std::io::Error::last_os_error(); + if err.kind() == std::io::ErrorKind::Interrupted { + return None; + } + panic!("waitpid failed for bubblewrap child: {err}"); +} + +fn poll_for_output(stdout_read: &OwnedFd, stderr_read: &OwnedFd) { + let mut poll_fds = [ + libc::pollfd { + fd: stdout_read.as_raw_fd(), + events: libc::POLLIN | libc::POLLHUP, + revents: 0, + }, + libc::pollfd { + fd: stderr_read.as_raw_fd(), + events: libc::POLLIN | libc::POLLHUP, + revents: 0, + }, + ]; + let result = unsafe { libc::poll(poll_fds.as_mut_ptr(), poll_fds.len() as _, 10) }; + if result < 0 { + let err = std::io::Error::last_os_error(); + if err.kind() != std::io::ErrorKind::Interrupted { + panic!("failed to poll bubblewrap output relay pipes: {err}"); + } + } +} diff --git a/codex-rs/linux-sandbox/src/lib.rs b/codex-rs/linux-sandbox/src/lib.rs index 478cd6c379..550aff4233 100644 --- a/codex-rs/linux-sandbox/src/lib.rs +++ b/codex-rs/linux-sandbox/src/lib.rs @@ -10,6 +10,8 @@ mod bundled_bwrap; #[cfg(target_os = "linux")] mod bwrap; #[cfg(target_os = "linux")] +mod bwrap_output_relay; +#[cfg(target_os = "linux")] mod exec_util; #[cfg(target_os = "linux")] mod landlock; diff --git a/codex-rs/linux-sandbox/src/linux_run_main.rs b/codex-rs/linux-sandbox/src/linux_run_main.rs index b5161fa770..f0292f691b 100644 --- a/codex-rs/linux-sandbox/src/linux_run_main.rs +++ b/codex-rs/linux-sandbox/src/linux_run_main.rs @@ -21,6 +21,7 @@ use crate::bwrap::BwrapNetworkMode; use crate::bwrap::BwrapOptions; use crate::bwrap::BwrapProcessLifetime; use crate::bwrap::create_bwrap_command_args; +use crate::bwrap_output_relay::BwrapOutputRelay; use crate::landlock::apply_permission_profile_to_current_thread; use crate::launcher::exec_bwrap; use crate::launcher::preferred_bwrap_supports_argv0; @@ -326,6 +327,7 @@ fn ensure_legacy_landlock_mode_supports_policy( } } +#[expect(clippy::too_many_arguments)] fn run_bwrap_with_proc_fallback( sandbox_policy_cwd: &Path, command_cwd: Option<&Path>, @@ -369,7 +371,7 @@ fn run_bwrap_with_proc_fallback( ) .unwrap_or_else(|err| exit_with_bwrap_build_error(err)); apply_inner_command_argv0(&mut bwrap_args.args); - run_or_exec_bwrap(bwrap_args); + run_or_exec_bwrap(bwrap_args, process_lifetime); } fn bwrap_network_mode( @@ -500,16 +502,23 @@ fn resolve_true_command() -> String { "true".to_string() } -fn run_or_exec_bwrap(bwrap_args: crate::bwrap::BwrapArgs) -> ! { - if bwrap_args.synthetic_mount_targets.is_empty() +fn run_or_exec_bwrap( + bwrap_args: crate::bwrap::BwrapArgs, + process_lifetime: BwrapProcessLifetime, +) -> ! { + if process_lifetime == BwrapProcessLifetime::TerminateWithParent + && bwrap_args.synthetic_mount_targets.is_empty() && bwrap_args.protected_create_targets.is_empty() { exec_bwrap(bwrap_args.args, bwrap_args.preserved_files); } - run_bwrap_in_child_with_synthetic_mount_cleanup(bwrap_args); + run_bwrap_in_child_with_synthetic_mount_cleanup(bwrap_args, process_lifetime); } -fn run_bwrap_in_child_with_synthetic_mount_cleanup(bwrap_args: crate::bwrap::BwrapArgs) -> ! { +fn run_bwrap_in_child_with_synthetic_mount_cleanup( + bwrap_args: crate::bwrap::BwrapArgs, + process_lifetime: BwrapProcessLifetime, +) -> ! { let crate::bwrap::BwrapArgs { args, preserved_files, @@ -521,6 +530,8 @@ fn run_bwrap_in_child_with_synthetic_mount_cleanup(bwrap_args: crate::bwrap::Bwr let protected_create_registrations = register_protected_create_targets(&protected_create_targets); let exec_start_pipe = create_exec_start_pipe(!protected_create_targets.is_empty()); + let output_relay = (process_lifetime == BwrapProcessLifetime::AllowDetachedChildren) + .then(BwrapOutputRelay::new); let parent_pid = unsafe { libc::getpid() }; let pid = unsafe { libc::fork() }; if pid < 0 { @@ -531,6 +542,9 @@ fn run_bwrap_in_child_with_synthetic_mount_cleanup(bwrap_args: crate::bwrap::Bwr if pid == 0 { reset_forwarded_signal_handlers_to_default(); setup_signal_mask.restore(); + if let Some(output_relay) = output_relay { + output_relay.redirect_child_output(); + } let setpgid_res = unsafe { libc::setpgid(0, 0) }; if setpgid_res < 0 { let err = std::io::Error::last_os_error(); @@ -546,7 +560,10 @@ fn run_bwrap_in_child_with_synthetic_mount_cleanup(bwrap_args: crate::bwrap::Bwr let signal_forwarders = install_bwrap_signal_forwarders(pid); release_child_exec_start(exec_start_pipe[1]); setup_signal_mask.restore(); - let status = wait_for_bwrap_child(pid); + let status = match output_relay { + Some(output_relay) => output_relay.forward_until_child_exit(pid), + None => wait_for_bwrap_child(pid), + }; let cleanup_signal_mask = ForwardedSignalMask::block(); BWRAP_CHILD_PID.store(0, Ordering::SeqCst); let protected_create_monitor_violation = protected_create_monitor