mirror of
https://github.com/openai/codex.git
synced 2026-08-23 13:09:46 +00:00
Harden Linux managed proxy helper lifecycles (#36771)
## Why Managed proxy helpers can keep captured standard streams open after the sandboxed command exits. Proxy readiness can also fail when inherited standard descriptors are already closed, and zombie owners can leave stale socket directories behind. ## What changed - Detach bridge and cleanup-worker standard I/O to `/dev/null`. - Move readiness pipe descriptors above the standard descriptor range. - Treat zombie processes as exited when cleaning proxy socket directories. - Move helper lifecycle handling into a dedicated module. ## Testing Add coverage for output release after command exit, readiness with closed standard descriptors, zombie detection, and stale socket cleanup. GitOrigin-RevId: 9f4081cba1b73442f02a895473c0383a795fa30e
This commit is contained in:
@@ -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")]
|
||||
|
||||
255
codex-rs/linux-sandbox/src/proxy_lifecycle.rs
Normal file
255
codex-rs/linux-sandbox/src/proxy_lifecycle.rs
Normal file
@@ -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<u32> {
|
||||
let suffix = file_name.strip_prefix(PROXY_SOCKET_DIR_PREFIX)?;
|
||||
let (pid_raw, _) = suffix.split_once('-')?;
|
||||
pid_raw.parse::<u32>().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<libc::pid_t>,
|
||||
) -> 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<libc::c_int> {
|
||||
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;
|
||||
91
codex-rs/linux-sandbox/src/proxy_lifecycle_tests.rs
Normal file
91
codex-rs/linux-sandbox/src/proxy_lifecycle_tests.rs
Normal file
@@ -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);
|
||||
}
|
||||
@@ -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<u32> {
|
||||
let suffix = file_name.strip_prefix(PROXY_SOCKET_DIR_PREFIX)?;
|
||||
let (pid_raw, _) = suffix.split_once('-')?;
|
||||
pid_raw.parse::<u32>().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<libc::pid_t>,
|
||||
) -> 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,6 +128,20 @@ async fn run_linux_sandbox_direct(
|
||||
env: HashMap<String, String>,
|
||||
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<String, String>,
|
||||
) -> 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]
|
||||
|
||||
Reference in New Issue
Block a user