diff --git a/codex-rs/core/src/spawn.rs b/codex-rs/core/src/spawn.rs index d738f12232..3a2606f72b 100644 --- a/codex-rs/core/src/spawn.rs +++ b/codex-rs/core/src/spawn.rs @@ -73,6 +73,38 @@ pub(crate) async fn spawn_child_async( return Err(std::io::Error::last_os_error()); } + #[cfg(target_os = "macos")] + { + // macOS network stack (via reqwest/hyper) opens PF_SYSTEM control sockets + // such as com.apple.netsrc without setting FD_CLOEXEC. Those descriptors + // get inherited by shell tool children, which is surprising and lets the + // child talk to that kernel control socket. Close everything above stdio + // to keep those sockets (and any similar long-lived fds) out of subshells. + // We bound the sweep by min(RLIMIT_NOFILE, 128) because netsrc fds are + // low and this avoids a pathological case where another thread lowers + // the rlimit between opening the socket and spawning the child. If we ever + // move the reqwest traffic into a helper process, leaking these fds would + // be less concerning. + let mut max_fd = 128_i64; + let mut limit = libc::rlimit { + rlim_cur: 0, + rlim_max: 0, + }; + if libc::getrlimit(libc::RLIMIT_NOFILE, &mut limit) == 0 + && limit.rlim_cur != libc::RLIM_INFINITY + { + let soft_limit = limit.rlim_cur.min(i64::MAX as libc::rlim_t) as i64; + max_fd = max_fd.min(soft_limit); + } + + let bound = max_fd.max(3); + let mut fd = 3; + while (fd as i64) < bound { + libc::close(fd); + fd += 1; + } + } + // This relies on prctl(2), so it only works on Linux. #[cfg(target_os = "linux")] {