diff --git a/codex-rs/sandboxing/src/bwrap.rs b/codex-rs/sandboxing/src/bwrap.rs index 3435c6d19..840bb3e62 100644 --- a/codex-rs/sandboxing/src/bwrap.rs +++ b/codex-rs/sandboxing/src/bwrap.rs @@ -1,9 +1,16 @@ use crate::policy_transforms::should_require_platform_sandbox; use codex_protocol::models::PermissionProfile; +use std::io::ErrorKind; +use std::io::Read; +use std::os::fd::AsRawFd; use std::path::Path; use std::path::PathBuf; use std::process::Command; use std::process::Output; +use std::process::Stdio; +use std::thread; +use std::time::Duration; +use std::time::Instant; const SYSTEM_BWRAP_PROGRAM: &str = "bwrap"; const MISSING_BWRAP_WARNING: &str = concat!( @@ -26,6 +33,9 @@ const USER_NAMESPACE_FAILURES: [&str; 4] = [ "setting up uid map: Permission denied", "No permissions to create a new namespace", ]; +const SYSTEM_BWRAP_PROBE_TIMEOUT: Duration = Duration::from_millis(500); +const SYSTEM_BWRAP_PROBE_POLL_INTERVAL: Duration = Duration::from_millis(50); +const SYSTEM_BWRAP_PROBE_STDERR_LIMIT_BYTES: u64 = 64 * 1024; pub fn system_bwrap_warning(permission_profile: &PermissionProfile) -> Option { if !should_warn_about_system_bwrap(permission_profile) { @@ -54,15 +64,15 @@ fn system_bwrap_warning_for_path(system_bwrap_path: Option<&Path>) -> Option bool { - let output = match Command::new(system_bwrap_path) +fn system_bwrap_has_user_namespace_access(system_bwrap_path: &Path, timeout: Duration) -> bool { + let mut child = match Command::new(system_bwrap_path) .args([ "--unshare-user", "--unshare-net", @@ -71,13 +81,58 @@ fn system_bwrap_has_user_namespace_access(system_bwrap_path: &Path) -> bool { "/", "/bin/true", ]) - .output() + .stdout(Stdio::null()) + .stderr(Stdio::piped()) + .spawn() { - Ok(output) => output, + Ok(child) => child, Err(_) => return true, }; - output.status.success() || !is_user_namespace_failure(&output) + let deadline = Instant::now() + timeout; + loop { + match child.try_wait() { + Ok(Some(status)) => { + let stderr = child.stderr.take().map_or_else(Vec::new, |stderr| { + let fd = stderr.as_raw_fd(); + let flags = unsafe { libc::fcntl(fd, libc::F_GETFL) }; + if flags < 0 + || unsafe { libc::fcntl(fd, libc::F_SETFL, flags | libc::O_NONBLOCK) } < 0 + { + return Vec::new(); + } + + let mut bytes = Vec::new(); + let mut stderr = stderr.take(SYSTEM_BWRAP_PROBE_STDERR_LIMIT_BYTES); + if let Err(err) = stderr.read_to_end(&mut bytes) + && err.kind() != ErrorKind::WouldBlock + { + return bytes; + } + bytes + }); + let output = Output { + status, + stdout: Vec::new(), + stderr, + }; + return output.status.success() || !is_user_namespace_failure(&output); + } + Ok(None) => { + if Instant::now() >= deadline { + let _ = child.kill(); + let _ = child.wait(); + return true; + } + thread::sleep(SYSTEM_BWRAP_PROBE_POLL_INTERVAL); + } + Err(_) => { + let _ = child.kill(); + let _ = child.wait(); + return true; + } + } + } } pub(crate) fn is_wsl1() -> bool { diff --git a/codex-rs/sandboxing/src/bwrap_tests.rs b/codex-rs/sandboxing/src/bwrap_tests.rs index f36848e1e..3c7a50392 100644 --- a/codex-rs/sandboxing/src/bwrap_tests.rs +++ b/codex-rs/sandboxing/src/bwrap_tests.rs @@ -2,6 +2,8 @@ use super::*; use pretty_assertions::assert_eq; use std::path::Path; use std::path::PathBuf; +use std::time::Duration; +use std::time::Instant; use tempfile::tempdir; #[test] @@ -44,6 +46,43 @@ exit 1 assert_eq!(system_bwrap_warning_for_path(Some(fake_bwrap_path)), None); } +#[test] +fn system_bwrap_probe_times_out_without_reporting_a_warning() { + let fake_bwrap = write_fake_bwrap( + r#"#!/bin/sh +sleep 1 +exit 0 +"#, + ); + let fake_bwrap_path: &Path = fake_bwrap.as_ref(); + let started_at = Instant::now(); + + assert!(system_bwrap_has_user_namespace_access( + fake_bwrap_path, + Duration::from_millis(10), + )); + assert!(started_at.elapsed() < Duration::from_millis(500)); +} + +#[test] +fn system_bwrap_probe_does_not_wait_for_descendants_holding_stderr_open() { + let fake_bwrap = write_fake_bwrap( + r#"#!/bin/sh +echo 'No permissions to create a new namespace' >&2 +sleep 1 & +exit 1 +"#, + ); + let fake_bwrap_path: &Path = fake_bwrap.as_ref(); + let started_at = Instant::now(); + + assert!(!system_bwrap_has_user_namespace_access( + fake_bwrap_path, + Duration::from_millis(100), + )); + assert!(started_at.elapsed() < Duration::from_millis(500)); +} + #[test] fn detects_wsl1_proc_version_formats() { assert!(proc_version_indicates_wsl1(