pub(crate) trait CloseInheritedFds {
fn close_inherited_fds(&mut self) -> &mut Self;
}
#[cfg(unix)]
impl CloseInheritedFds for std::process::Command {
fn close_inherited_fds(&mut self) -> &mut Self {
use std::os::unix::process::CommandExt;
let mut fds = InheritedFds::new();
unsafe {
self.pre_exec(move || {
fds.mark_cloexec();
Ok(())
})
}
}
}
#[cfg(unix)]
impl CloseInheritedFds for tokio::process::Command {
fn close_inherited_fds(&mut self) -> &mut Self {
let mut fds = InheritedFds::new();
unsafe {
self.pre_exec(move || {
fds.mark_cloexec();
Ok(())
})
}
}
}
#[cfg(windows)]
impl CloseInheritedFds for std::process::Command {
fn close_inherited_fds(&mut self) -> &mut Self {
self
}
}
#[cfg(windows)]
impl CloseInheritedFds for tokio::process::Command {
fn close_inherited_fds(&mut self) -> &mut Self {
self
}
}
#[cfg(unix)]
struct InheritedFds {
scan_end: libc::c_int,
#[cfg(target_os = "macos")]
listing: Vec<libc::proc_fdinfo>,
}
#[cfg(unix)]
impl InheritedFds {
fn new() -> Self {
let open_max = unsafe { libc::sysconf(libc::_SC_OPEN_MAX) };
let open_fds: Vec<libc::c_int> = std::fs::read_dir("/dev/fd")
.into_iter()
.flatten()
.filter_map(|entry| entry.ok()?.file_name().to_str()?.parse().ok())
.collect();
Self {
#[cfg(target_os = "macos")]
listing: vec![
libc::proc_fdinfo {
proc_fd: 0,
proc_fdtype: 0,
};
open_fds.len() * 2 + 64
],
scan_end: fd_scan_end(open_max, open_fds),
}
}
fn mark_cloexec(&mut self) {
#[cfg(any(target_os = "linux", target_os = "android"))]
{
let ret = unsafe {
libc::syscall(
libc::SYS_close_range,
3 as libc::c_uint,
libc::c_uint::MAX,
libc::CLOSE_RANGE_CLOEXEC as libc::c_uint,
)
};
if ret == 0 {
return;
}
}
#[cfg(target_os = "macos")]
if self.mark_listed() {
return;
}
cloexec_fd_scan(self.scan_end);
}
#[cfg(target_os = "macos")]
fn mark_listed(&mut self) -> bool {
let entry_size = std::mem::size_of::<libc::proc_fdinfo>();
let Ok(capacity) = libc::c_int::try_from(self.listing.len() * entry_size) else {
return false;
};
let filled = unsafe {
libc::proc_pidinfo(
libc::getpid(),
libc::PROC_PIDLISTFDS,
0,
self.listing.as_mut_ptr().cast(),
capacity,
)
};
if filled <= 0 || filled >= capacity {
return false;
}
for info in &self.listing[..filled as usize / entry_size] {
if info.proc_fd >= 3 {
set_cloexec(info.proc_fd);
}
}
true
}
}
#[cfg(unix)]
fn fd_scan_end(
open_max: libc::c_long,
open_fds: impl IntoIterator<Item = libc::c_int>,
) -> libc::c_int {
let limit = if open_max <= 0 {
1 << 16
} else {
open_max.min(1 << 20) as libc::c_int
};
let past_highest_open = open_fds
.into_iter()
.max()
.map_or(0, |fd| fd.saturating_add(1));
limit.max(past_highest_open)
}
#[cfg(unix)]
fn cloexec_fd_scan(scan_end: libc::c_int) {
for fd in 3..scan_end {
set_cloexec(fd);
}
}
#[cfg(unix)]
fn set_cloexec(fd: libc::c_int) {
unsafe {
let flags = libc::fcntl(fd, libc::F_GETFD);
if flags >= 0 && flags & libc::FD_CLOEXEC == 0 {
libc::fcntl(fd, libc::F_SETFD, flags | libc::FD_CLOEXEC);
}
}
}
#[cfg(all(test, unix))]
mod tests {
use super::*;
use std::os::unix::process::CommandExt;
use std::process::{Command, Stdio};
use std::sync::{Mutex, MutexGuard, PoisonError};
static INHERITABLE: Mutex<()> = Mutex::new(());
fn inheritable() -> MutexGuard<'static, ()> {
INHERITABLE.lock().unwrap_or_else(PoisonError::into_inner)
}
fn inheritable_pipe() -> [libc::c_int; 2] {
let mut fds = [0; 2];
assert_eq!(unsafe { libc::pipe(fds.as_mut_ptr()) }, 0);
fds
}
fn close_pipe(fds: [libc::c_int; 2]) {
unsafe {
libc::close(fds[0]);
libc::close(fds[1]);
}
}
fn sees_fd(fd: libc::c_int) -> Command {
let mut cmd = Command::new("sh");
cmd.arg("-c")
.arg(format!("[ -e /dev/fd/{fd} ]"))
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null());
cmd
}
#[test]
fn fd_scan_end_reaches_open_fds_above_the_fd_limit() {
assert_eq!(fd_scan_end(256, [0, 1, 2, 900]), 901);
assert_eq!(fd_scan_end(256, [0, 1, 2, 10]), 256);
assert_eq!(fd_scan_end(-1, []), 1 << 16);
assert_eq!(fd_scan_end(libc::c_long::MAX, []), 1 << 20);
}
#[test]
fn close_inherited_fds_hides_inherited_fds_from_children() {
let _guard = inheritable();
let pipe = inheritable_pipe();
let without = sees_fd(pipe[1]).status().unwrap().success();
let with = sees_fd(pipe[1])
.close_inherited_fds()
.status()
.unwrap()
.success();
close_pipe(pipe);
assert!(without, "control: child should inherit fd {}", pipe[1]);
assert!(!with, "fd {} leaked past close_inherited_fds", pipe[1]);
}
#[test]
fn cloexec_fd_scan_hides_inherited_fds_from_children() {
let _guard = inheritable();
let pipe = inheritable_pipe();
let scan_end = InheritedFds::new().scan_end;
let mut cmd = sees_fd(pipe[1]);
unsafe {
cmd.pre_exec(move || {
cloexec_fd_scan(scan_end);
Ok(())
});
}
let with_scan = cmd.status().unwrap().success();
close_pipe(pipe);
assert!(!with_scan, "fd {} leaked past cloexec_fd_scan", pipe[1]);
}
#[cfg(target_os = "macos")]
#[test]
fn mark_listed_hides_inherited_fds_from_children() {
let _guard = inheritable();
let pipe = inheritable_pipe();
let mut fds = InheritedFds::new();
let mut cmd = sees_fd(pipe[1]);
unsafe {
cmd.pre_exec(move || {
if fds.mark_listed() {
Ok(())
} else {
Err(std::io::Error::other("proc_pidinfo listing failed"))
}
});
}
let status = cmd.status();
close_pipe(pipe);
assert!(
!status.unwrap().success(),
"fd {} leaked past mark_listed",
pipe[1]
);
}
#[test]
fn close_inherited_fds_hides_a_listening_socket_from_tokio_children() {
use std::os::fd::AsRawFd;
let _guard = inheritable();
let listener = std::net::TcpListener::bind(("127.0.0.1", 0)).unwrap();
let fd = listener.as_raw_fd();
unsafe {
let flags = libc::fcntl(fd, libc::F_GETFD);
assert_eq!(libc::fcntl(fd, libc::F_SETFD, flags & !libc::FD_CLOEXEC), 0);
}
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap();
let child_sees_socket = |close: bool| {
let mut cmd = tokio::process::Command::from(sees_fd(fd));
if close {
cmd.close_inherited_fds();
}
runtime
.block_on(async { cmd.status().await })
.unwrap()
.success()
};
let without = child_sees_socket(false);
let with = child_sees_socket(true);
drop(listener);
assert!(without, "control: child should inherit the socket");
assert!(!with, "listening socket leaked past close_inherited_fds");
}
}