diff --git a/codex-rs/linux-sandbox/src/lib.rs b/codex-rs/linux-sandbox/src/lib.rs index 478cd6c379..2cbc07f6da 100644 --- a/codex-rs/linux-sandbox/src/lib.rs +++ b/codex-rs/linux-sandbox/src/lib.rs @@ -18,6 +18,8 @@ mod launcher; #[cfg(target_os = "linux")] mod linux_run_main; #[cfg(target_os = "linux")] +mod proxy_lifecycle; +#[cfg(target_os = "linux")] mod proxy_routing; #[cfg(target_os = "linux")] diff --git a/codex-rs/linux-sandbox/src/proxy_lifecycle.rs b/codex-rs/linux-sandbox/src/proxy_lifecycle.rs new file mode 100644 index 0000000000..82fae4de0d --- /dev/null +++ b/codex-rs/linux-sandbox/src/proxy_lifecycle.rs @@ -0,0 +1,255 @@ +use std::io; +use std::path::Path; +use std::path::PathBuf; +use std::time::Duration; + +pub(crate) const PROXY_SOCKET_DIR_PREFIX: &str = "codex-linux-sandbox-proxy-"; + +pub(crate) fn cleanup_stale_proxy_socket_dirs_in(temp_dir: &Path) -> io::Result<()> { + for entry in std::fs::read_dir(temp_dir)? { + let entry = match entry { + Ok(entry) => entry, + Err(_) => continue, + }; + let file_type = match entry.file_type() { + Ok(file_type) => file_type, + Err(_) => continue, + }; + if !file_type.is_dir() { + continue; + } + + let file_name = entry.file_name(); + let file_name = file_name.to_string_lossy(); + let Some(owner_pid) = parse_proxy_socket_dir_owner_pid(file_name.as_ref()) else { + continue; + }; + if is_pid_alive(owner_pid) { + continue; + } + + let _ = cleanup_proxy_socket_dir(entry.path().as_path()); + } + + Ok(()) +} + +fn parse_proxy_socket_dir_owner_pid(file_name: &str) -> Option { + let suffix = file_name.strip_prefix(PROXY_SOCKET_DIR_PREFIX)?; + let (pid_raw, _) = suffix.split_once('-')?; + pid_raw.parse::().ok().filter(|pid| *pid != 0) +} + +fn is_pid_alive(pid: u32) -> bool { + let Ok(pid) = libc::pid_t::try_from(pid) else { + return false; + }; + is_pid_alive_raw(pid) +} + +fn is_pid_alive_raw(pid: libc::pid_t) -> bool { + is_pid_alive_in_proc_root(pid, Path::new("/proc")) +} + +fn is_pid_alive_in_proc_root(pid: libc::pid_t, proc_root: &Path) -> bool { + let status = unsafe { libc::kill(pid, 0) }; + if status != 0 { + let err = io::Error::last_os_error(); + return !matches!(err.raw_os_error(), Some(libc::ESRCH)); + } + + match std::fs::read_to_string(proc_root.join(format!("{pid}/stat"))) { + Ok(status) => !matches!( + status + .rsplit_once(") ") + .and_then(|(_, fields)| fields.chars().next()), + Some('Z') + ), + Err(err) if err.kind() == io::ErrorKind::NotFound => !proc_root.is_dir(), + Err(_) => true, + } +} + +pub(crate) fn spawn_proxy_socket_dir_cleanup_worker( + socket_dir: PathBuf, + host_bridge_pids: Vec, +) -> io::Result<()> { + let pid = unsafe { libc::fork() }; + if pid < 0 { + return Err(io::Error::last_os_error()); + } + + if pid == 0 { + if detach_bridge_stdio().is_err() { + unsafe { libc::_exit(1) }; + } + + loop { + if host_bridge_pids + .iter() + .copied() + .all(|bridge_pid| !is_pid_alive_raw(bridge_pid)) + { + break; + } + std::thread::sleep(Duration::from_millis(100)); + } + + let _ = cleanup_proxy_socket_dir(socket_dir.as_path()); + unsafe { libc::_exit(0) }; + } + + Ok(()) +} + +fn cleanup_proxy_socket_dir(socket_dir: &Path) -> io::Result<()> { + for _ in 0..20 { + match std::fs::remove_dir_all(socket_dir) { + Ok(()) => return Ok(()), + Err(err) if err.kind() == io::ErrorKind::NotFound => return Ok(()), + Err(_) => std::thread::sleep(Duration::from_millis(100)), + } + } + + match std::fs::remove_dir_all(socket_dir) { + Ok(()) => Ok(()), + Err(err) if err.kind() == io::ErrorKind::NotFound => Ok(()), + Err(err) => Err(err), + } +} + +pub(crate) fn harden_bridge_process() -> io::Result<()> { + detach_bridge_stdio()?; + set_parent_death_signal()?; + codex_process_hardening::disable_process_dumping() +} + +fn set_parent_death_signal() -> io::Result<()> { + let res = unsafe { libc::prctl(libc::PR_SET_PDEATHSIG, libc::SIGTERM) }; + if res != 0 { + Err(io::Error::last_os_error()) + } else if unsafe { libc::getppid() } == 1 { + Err(io::Error::other("parent process already exited")) + } else { + Ok(()) + } +} + +fn detach_bridge_stdio() -> io::Result<()> { + let null_read_fd = unsafe { libc::open(c"/dev/null".as_ptr(), libc::O_RDONLY) }; + if null_read_fd < 0 { + let err = io::Error::last_os_error(); + if unsafe { libc::fcntl(libc::STDIN_FILENO, libc::F_GETFD) } < 0 { + return Err(err); + } + return redirect_bridge_output(libc::STDIN_FILENO); + } + + let null_write_fd = unsafe { libc::open(c"/dev/null".as_ptr(), libc::O_WRONLY) }; + if null_write_fd < 0 { + if unsafe { libc::dup2(null_read_fd, libc::STDIN_FILENO) } < 0 { + let err = io::Error::last_os_error(); + if null_read_fd > libc::STDERR_FILENO { + let _ = close_fd(null_read_fd); + } + return Err(err); + } + let result = redirect_bridge_output(null_read_fd); + if null_read_fd > libc::STDERR_FILENO { + let _ = close_fd(null_read_fd); + } + return result; + } + + for (source_fd, stream_fd) in [ + (null_read_fd, libc::STDIN_FILENO), + (null_write_fd, libc::STDOUT_FILENO), + (null_write_fd, libc::STDERR_FILENO), + ] { + if unsafe { libc::dup2(source_fd, stream_fd) } < 0 { + let err = io::Error::last_os_error(); + if null_read_fd > libc::STDERR_FILENO { + let _ = close_fd(null_read_fd); + } + if null_write_fd > libc::STDERR_FILENO { + let _ = close_fd(null_write_fd); + } + return Err(err); + } + } + + if null_read_fd > libc::STDERR_FILENO { + close_fd(null_read_fd)?; + } + if null_write_fd > libc::STDERR_FILENO { + close_fd(null_write_fd)?; + } + + Ok(()) +} + +fn redirect_bridge_output(source_fd: libc::c_int) -> io::Result<()> { + for stream_fd in [libc::STDOUT_FILENO, libc::STDERR_FILENO] { + if unsafe { libc::dup2(source_fd, stream_fd) } < 0 { + return Err(io::Error::last_os_error()); + } + } + Ok(()) +} + +pub(crate) fn create_ready_pipe() -> io::Result<(libc::c_int, libc::c_int)> { + let mut pipe_fds = [0; 2]; + let res = unsafe { libc::pipe2(pipe_fds.as_mut_ptr(), libc::O_CLOEXEC) }; + if res != 0 { + return Err(io::Error::last_os_error()); + } + + let read_fd = match move_fd_above_stdio(pipe_fds[0]) { + Ok(fd) => fd, + Err(err) => { + let _ = close_fd(pipe_fds[0]); + let _ = close_fd(pipe_fds[1]); + return Err(err); + } + }; + let write_fd = match move_fd_above_stdio(pipe_fds[1]) { + Ok(fd) => fd, + Err(err) => { + let _ = close_fd(read_fd); + let _ = close_fd(pipe_fds[1]); + return Err(err); + } + }; + + Ok((read_fd, write_fd)) +} + +fn move_fd_above_stdio(fd: libc::c_int) -> io::Result { + if fd > libc::STDERR_FILENO { + return Ok(fd); + } + + let relocated_fd = unsafe { libc::fcntl(fd, libc::F_DUPFD_CLOEXEC, libc::STDERR_FILENO + 1) }; + if relocated_fd < 0 { + return Err(io::Error::last_os_error()); + } + + if let Err(err) = close_fd(fd) { + let _ = close_fd(relocated_fd); + return Err(err); + } + + Ok(relocated_fd) +} + +pub(crate) fn close_fd(fd: libc::c_int) -> io::Result<()> { + let res = unsafe { libc::close(fd) }; + if res < 0 { + return Err(io::Error::last_os_error()); + } + Ok(()) +} + +#[cfg(test)] +#[path = "proxy_lifecycle_tests.rs"] +mod tests; diff --git a/codex-rs/linux-sandbox/src/proxy_lifecycle_tests.rs b/codex-rs/linux-sandbox/src/proxy_lifecycle_tests.rs new file mode 100644 index 0000000000..a18e26fdae --- /dev/null +++ b/codex-rs/linux-sandbox/src/proxy_lifecycle_tests.rs @@ -0,0 +1,91 @@ +use super::PROXY_SOCKET_DIR_PREFIX; +use super::cleanup_proxy_socket_dir; +use super::cleanup_stale_proxy_socket_dirs_in; +use super::is_pid_alive_in_proc_root; +use super::is_pid_alive_raw; +use super::parse_proxy_socket_dir_owner_pid; +use pretty_assertions::assert_eq; +use std::process::Command; +use std::time::Duration; +use std::time::Instant; + +#[test] +fn cleanup_proxy_socket_dir_removes_bridge_artifacts() { + let root = tempfile::tempdir().expect("tempdir should create"); + let socket_dir = root.path().join("codex-linux-sandbox-proxy-test"); + std::fs::create_dir(&socket_dir).expect("socket dir should create"); + let marker = socket_dir.join("bridge.sock"); + std::fs::write(&marker, b"test").expect("marker should write"); + + cleanup_proxy_socket_dir(socket_dir.as_path()).expect("cleanup should succeed"); + + assert_eq!(socket_dir.exists(), false); +} + +#[test] +fn parse_proxy_socket_dir_owner_pid_reads_owner_pid() { + assert_eq!( + parse_proxy_socket_dir_owner_pid("codex-linux-sandbox-proxy-1234-0"), + Some(1234) + ); + assert_eq!( + parse_proxy_socket_dir_owner_pid("codex-linux-sandbox-proxy-1234-1000-0"), + Some(1234) + ); + assert_eq!( + parse_proxy_socket_dir_owner_pid("codex-linux-sandbox-proxy-x"), + None + ); + assert_eq!(parse_proxy_socket_dir_owner_pid("not-a-proxy-dir"), None); +} + +#[test] +fn cleanup_stale_proxy_socket_dirs_removes_dead_pid_directories() { + let root = tempfile::tempdir().expect("tempdir should create"); + let dead_dir = root + .path() + .join(format!("{PROXY_SOCKET_DIR_PREFIX}{}-0", u32::MAX)); + std::fs::create_dir(&dead_dir).expect("dead dir should create"); + + let alive_dir = root + .path() + .join(format!("{PROXY_SOCKET_DIR_PREFIX}{}-1", std::process::id())); + std::fs::create_dir(&alive_dir).expect("alive dir should create"); + + let unrelated_dir = root.path().join("unrelated-proxy-dir"); + std::fs::create_dir(&unrelated_dir).expect("unrelated dir should create"); + + cleanup_stale_proxy_socket_dirs_in(root.path()).expect("stale cleanup should succeed"); + + assert_eq!(dead_dir.exists(), false); + assert_eq!(alive_dir.exists(), true); + assert_eq!(unrelated_dir.exists(), true); +} + +#[test] +fn missing_procfs_keeps_live_process_alive() { + let root = tempfile::tempdir().expect("tempdir should create"); + let missing_proc_root = root.path().join("missing-proc"); + let pid = libc::pid_t::try_from(std::process::id()).expect("current pid should fit"); + + assert_eq!(is_pid_alive_in_proc_root(pid, &missing_proc_root), true); +} + +#[test] +fn zombie_process_is_not_alive() { + let mut child = Command::new("sh") + .arg("-c") + .arg("exit 0") + .spawn() + .expect("child should spawn"); + let pid = libc::pid_t::try_from(child.id()).expect("child pid should fit"); + let deadline = Instant::now() + Duration::from_secs(5); + + while is_pid_alive_raw(pid) && Instant::now() < deadline { + std::thread::sleep(Duration::from_millis(10)); + } + let zombie_was_recognized = !is_pid_alive_raw(pid); + child.wait().expect("child should be reaped"); + + assert_eq!(zombie_was_recognized, true); +} diff --git a/codex-rs/linux-sandbox/src/proxy_routing.rs b/codex-rs/linux-sandbox/src/proxy_routing.rs index 245207c496..2d0c4e35a4 100644 --- a/codex-rs/linux-sandbox/src/proxy_routing.rs +++ b/codex-rs/linux-sandbox/src/proxy_routing.rs @@ -1,3 +1,9 @@ +use crate::proxy_lifecycle::PROXY_SOCKET_DIR_PREFIX; +use crate::proxy_lifecycle::cleanup_stale_proxy_socket_dirs_in; +use crate::proxy_lifecycle::close_fd; +use crate::proxy_lifecycle::create_ready_pipe; +use crate::proxy_lifecycle::harden_bridge_process; +use crate::proxy_lifecycle::spawn_proxy_socket_dir_cleanup_worker; use codex_network_proxy::PROXY_ATTRIBUTION_TOKEN_ENV_KEY; use codex_network_proxy::write_attribution_frame; use codex_utils_absolute_path::AbsolutePathBuf; @@ -25,7 +31,6 @@ use std::os::unix::net::UnixListener; use std::os::unix::net::UnixStream; use std::path::Path; use std::path::PathBuf; -use std::time::Duration; use url::Url; const PROXY_ENV_KEYS: &[&str] = &[ @@ -47,7 +52,6 @@ const PROXY_ENV_KEYS: &[&str] = &[ "DOCKER_HTTPS_PROXY", ]; -const PROXY_SOCKET_DIR_PREFIX: &str = "codex-linux-sandbox-proxy-"; const HOST_BRIDGE_READY: u8 = 1; const LOOPBACK_INTERFACE_NAME: &[u8] = b"lo"; // Linux sockaddr_un.sun_path allows 108 bytes, including the trailing NUL. @@ -367,101 +371,6 @@ fn ensure_private_proxy_socket_parent_dir(path: &Path) -> io::Result<()> { std::fs::set_permissions(path, Permissions::from_mode(0o700)) } -fn cleanup_stale_proxy_socket_dirs_in(temp_dir: &Path) -> io::Result<()> { - for entry in std::fs::read_dir(temp_dir)? { - let entry = match entry { - Ok(entry) => entry, - Err(_) => continue, - }; - let file_type = match entry.file_type() { - Ok(file_type) => file_type, - Err(_) => continue, - }; - if !file_type.is_dir() { - continue; - } - - let file_name = entry.file_name(); - let file_name = file_name.to_string_lossy(); - let Some(owner_pid) = parse_proxy_socket_dir_owner_pid(file_name.as_ref()) else { - continue; - }; - if is_pid_alive(owner_pid) { - continue; - } - - let _ = cleanup_proxy_socket_dir(entry.path().as_path()); - } - - Ok(()) -} - -fn parse_proxy_socket_dir_owner_pid(file_name: &str) -> Option { - let suffix = file_name.strip_prefix(PROXY_SOCKET_DIR_PREFIX)?; - let (pid_raw, _) = suffix.split_once('-')?; - pid_raw.parse::().ok().filter(|pid| *pid != 0) -} - -fn is_pid_alive(pid: u32) -> bool { - let Ok(pid) = libc::pid_t::try_from(pid) else { - return false; - }; - is_pid_alive_raw(pid) -} - -fn is_pid_alive_raw(pid: libc::pid_t) -> bool { - let status = unsafe { libc::kill(pid, 0) }; - if status == 0 { - return true; - } - let err = io::Error::last_os_error(); - !matches!(err.raw_os_error(), Some(libc::ESRCH)) -} - -fn spawn_proxy_socket_dir_cleanup_worker( - socket_dir: PathBuf, - host_bridge_pids: Vec, -) -> io::Result<()> { - let pid = unsafe { libc::fork() }; - if pid < 0 { - return Err(io::Error::last_os_error()); - } - - if pid == 0 { - loop { - if host_bridge_pids - .iter() - .copied() - .all(|bridge_pid| !is_pid_alive_raw(bridge_pid)) - { - break; - } - std::thread::sleep(Duration::from_millis(100)); - } - - let _ = cleanup_proxy_socket_dir(socket_dir.as_path()); - unsafe { libc::_exit(0) }; - } - - Ok(()) -} - -fn cleanup_proxy_socket_dir(socket_dir: &Path) -> io::Result<()> { - for _ in 0..20 { - match std::fs::remove_dir_all(socket_dir) { - Ok(()) => return Ok(()), - Err(err) if err.kind() == io::ErrorKind::NotFound => return Ok(()), - Err(_) => std::thread::sleep(Duration::from_millis(100)), - } - } - - match std::fs::remove_dir_all(socket_dir) { - Ok(()) => Ok(()), - Err(err) if err.kind() == io::ErrorKind::NotFound => Ok(()), - Err(err) => Err(err), - } -} - fn spawn_host_bridge( endpoint: SocketAddr, uds_path: &Path, @@ -667,22 +576,6 @@ fn ensure_loopback_interface_up() -> io::Result<()> { close_fd(fd) } -fn set_parent_death_signal() -> io::Result<()> { - let res = unsafe { libc::prctl(libc::PR_SET_PDEATHSIG, libc::SIGTERM) }; - if res != 0 { - Err(io::Error::last_os_error()) - } else if unsafe { libc::getppid() } == 1 { - Err(io::Error::other("parent process already exited")) - } else { - Ok(()) - } -} - -fn harden_bridge_process() -> io::Result<()> { - set_parent_death_signal()?; - codex_process_hardening::disable_process_dumping() -} - fn proxy_bidirectional(mut tcp_stream: TcpStream, mut unix_stream: UnixStream) -> io::Result<()> { let mut tcp_reader = tcp_stream.try_clone()?; let mut unix_writer = unix_stream.try_clone()?; @@ -701,36 +594,15 @@ fn proxy_bidirectional(mut tcp_stream: TcpStream, mut unix_stream: UnixStream) - Ok(()) } -fn create_ready_pipe() -> io::Result<(libc::c_int, libc::c_int)> { - let mut pipe_fds = [0; 2]; - let res = unsafe { libc::pipe2(pipe_fds.as_mut_ptr(), libc::O_CLOEXEC) }; - if res != 0 { - return Err(io::Error::last_os_error()); - } - Ok((pipe_fds[0], pipe_fds[1])) -} - -fn close_fd(fd: libc::c_int) -> io::Result<()> { - let res = unsafe { libc::close(fd) }; - if res < 0 { - return Err(io::Error::last_os_error()); - } - Ok(()) -} - #[cfg(test)] mod tests { use super::PROXY_ATTRIBUTION_TOKEN_ENV_KEY; - use super::PROXY_SOCKET_DIR_PREFIX; use super::ProxyRouteEntry; use super::ProxyRouteSpec; - use super::cleanup_proxy_socket_dir; - use super::cleanup_stale_proxy_socket_dirs_in; use super::default_proxy_port; use super::extract_attribution_token_and_plan; use super::is_proxy_env_key; use super::parse_loopback_proxy_endpoint; - use super::parse_proxy_socket_dir_owner_pid; use super::plan_proxy_routes; use super::proxy_socket_paths_fit; use super::rewrite_proxy_env_value; @@ -850,19 +722,6 @@ mod tests { ); } - #[test] - fn cleanup_proxy_socket_dir_removes_bridge_artifacts() { - let root = tempfile::tempdir().expect("tempdir should create"); - let socket_dir = root.path().join("codex-linux-sandbox-proxy-test"); - std::fs::create_dir(&socket_dir).expect("socket dir should create"); - let marker = socket_dir.join("bridge.sock"); - std::fs::write(&marker, b"test").expect("marker should write"); - - cleanup_proxy_socket_dir(socket_dir.as_path()).expect("cleanup should succeed"); - - assert_eq!(socket_dir.exists(), false); - } - #[test] fn proxy_route_spec_serialization_omits_proxy_urls() { let spec = ProxyRouteSpec { @@ -878,44 +737,4 @@ mod tests { r#"{"routes":[{"env_key":"HTTP_PROXY","uds_path":"/tmp/proxy-route-0.sock"}]}"# ); } - - #[test] - fn parse_proxy_socket_dir_owner_pid_reads_owner_pid() { - assert_eq!( - parse_proxy_socket_dir_owner_pid("codex-linux-sandbox-proxy-1234-0"), - Some(1234) - ); - assert_eq!( - parse_proxy_socket_dir_owner_pid("codex-linux-sandbox-proxy-1234-1000-0"), - Some(1234) - ); - assert_eq!( - parse_proxy_socket_dir_owner_pid("codex-linux-sandbox-proxy-x"), - None - ); - assert_eq!(parse_proxy_socket_dir_owner_pid("not-a-proxy-dir"), None); - } - - #[test] - fn cleanup_stale_proxy_socket_dirs_removes_dead_pid_directories() { - let root = tempfile::tempdir().expect("tempdir should create"); - let dead_dir = root - .path() - .join(format!("{PROXY_SOCKET_DIR_PREFIX}{}-0", u32::MAX)); - std::fs::create_dir(&dead_dir).expect("dead dir should create"); - - let alive_dir = root - .path() - .join(format!("{PROXY_SOCKET_DIR_PREFIX}{}-1", std::process::id())); - std::fs::create_dir(&alive_dir).expect("alive dir should create"); - - let unrelated_dir = root.path().join("unrelated-proxy-dir"); - std::fs::create_dir(&unrelated_dir).expect("unrelated dir should create"); - - cleanup_stale_proxy_socket_dirs_in(root.path()).expect("stale cleanup should succeed"); - - assert_eq!(dead_dir.exists(), false); - assert_eq!(alive_dir.exists(), true); - assert_eq!(unrelated_dir.exists(), true); - } } diff --git a/codex-rs/linux-sandbox/tests/suite/managed_proxy.rs b/codex-rs/linux-sandbox/tests/suite/managed_proxy.rs index d009af4a79..658f2e1a2e 100644 --- a/codex-rs/linux-sandbox/tests/suite/managed_proxy.rs +++ b/codex-rs/linux-sandbox/tests/suite/managed_proxy.rs @@ -128,6 +128,20 @@ async fn run_linux_sandbox_direct( env: HashMap, timeout_ms: u64, ) -> Output { + let mut command = + linux_sandbox_command(command, permission_profile, allow_network_for_proxy, env); + tokio::time::timeout(Duration::from_millis(timeout_ms), command.output()) + .await + .expect("sandbox command should not time out") + .expect("sandbox command should execute") +} + +fn linux_sandbox_command( + command: &[&str], + permission_profile: &PermissionProfile, + allow_network_for_proxy: bool, + env: HashMap, +) -> Command { let cwd = std::env::current_dir().expect("current directory should exist"); let permission_profile_json = serde_json::to_string(permission_profile).expect("permission profile should serialize"); @@ -152,10 +166,69 @@ async fn run_linux_sandbox_direct( .stdin(Stdio::null()) .stdout(Stdio::piped()) .stderr(Stdio::piped()); - tokio::time::timeout(Duration::from_millis(timeout_ms), cmd.output()) + cmd +} + +#[tokio::test] +async fn managed_proxy_bridges_release_command_output_after_exit() { + if let Some(skip_reason) = managed_proxy_skip_reason().await { + eprintln!("skipping managed proxy test: {skip_reason}"); + return; + } + + let mut env = create_env_from_core_vars(); + strip_proxy_env(&mut env); + env.insert("HTTP_PROXY".to_string(), "http://127.0.0.1:9".to_string()); + + let output = run_linux_sandbox_direct( + &["bash", "-c", "printf 'bridge output closed\\n'"], + &PermissionProfile::Disabled, + /*allow_network_for_proxy*/ true, + env, + NETWORK_TIMEOUT_MS, + ) + .await; + + assert_eq!(output.status.success(), true); + assert_eq!(output.stdout, b"bridge output closed\n"); +} + +#[tokio::test] +async fn managed_proxy_readiness_survives_closed_standard_descriptors() { + if let Some(skip_reason) = managed_proxy_skip_reason().await { + eprintln!("skipping managed proxy test: {skip_reason}"); + return; + } + + let mut env = create_env_from_core_vars(); + strip_proxy_env(&mut env); + env.insert("HTTP_PROXY".to_string(), "http://127.0.0.1:9".to_string()); + + let mut command = linux_sandbox_command( + &["bash", "-c", "true"], + &PermissionProfile::Disabled, + /*allow_network_for_proxy*/ true, + env, + ); + unsafe { + command.pre_exec(|| { + libc::close(libc::STDIN_FILENO); + libc::close(libc::STDOUT_FILENO); + Ok(()) + }); + } + + let output = tokio::time::timeout(Duration::from_millis(NETWORK_TIMEOUT_MS), command.output()) .await .expect("sandbox command should not time out") - .expect("sandbox command should execute") + .expect("sandbox command should execute"); + + assert_eq!( + output.status.success(), + true, + "managed proxy readiness should survive closed standard descriptors; stderr={}", + String::from_utf8_lossy(&output.stderr) + ); } #[tokio::test]