mirror of
https://github.com/openai/codex.git
synced 2026-10-21 17:48:44 +00:00
fix(linux-sandbox): relay detached bwrap output
Co-authored-by: Codex noreply@openai.com
This commit is contained in:
co-authored by
Codex noreply@openai.com
parent
15b3795166
commit
e9c66b523a
@@ -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<libc::c_int> {
|
||||
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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user