use std::os::raw::c_int;
use std::process::Command;
pub(crate) fn close_in_child(cmd: &mut Command) {
use std::os::unix::process::CommandExt;
unsafe {
cmd.pre_exec(|| {
close_matching(is_fuse);
Ok(())
});
}
}
fn is_fuse(fd: c_int) -> bool {
let mut st = std::mem::MaybeUninit::<libc::statfs>::uninit();
unsafe { libc::fstatfs(fd, st.as_mut_ptr()) == 0 && st.assume_init().f_type == 0x6573_5546 }
}
#[repr(C, align(8))]
struct DirentBuf([u8; 2048]);
fn close_matching(matches: fn(c_int) -> bool) {
let dir = unsafe {
libc::open(
c"/proc/self/fd".as_ptr(),
libc::O_RDONLY | libc::O_DIRECTORY | libc::O_CLOEXEC,
)
};
if dir < 0 {
return;
}
let mut buf = DirentBuf([0; 2048]);
loop {
let n = unsafe { libc::syscall(libc::SYS_getdents64, dir, buf.0.as_mut_ptr(), buf.0.len()) };
let Ok(n) = usize::try_from(n) else { break };
if n == 0 {
break;
}
let records = buf.0.get(..n).unwrap_or_default();
let mut at = 0;
while let Some(&[lo, hi]) = records.get(at + 16..at + 18) {
let len = usize::from(u16::from_ne_bytes([lo, hi]));
let Some(name) = records.get(at + 19..at + len) else {
break;
};
if let Some(fd) = parse_fd(name)
&& fd > 2
&& fd != dir
&& matches(fd)
{
unsafe { libc::close(fd) };
}
at += len;
}
}
unsafe { libc::close(dir) };
}
fn parse_fd(name: &[u8]) -> Option<c_int> {
let mut fd: c_int = 0;
let mut digits = 0;
for &b in name.iter().take_while(|&&b| b != 0) {
if !b.is_ascii_digit() {
return None;
}
fd = fd.checked_mul(10)?.checked_add(c_int::from(b - b'0'))?;
digits += 1;
}
(digits > 0).then_some(fd)
}
#[cfg(test)]
mod tests {
use super::*;
use std::os::fd::AsRawFd;
use std::os::unix::process::CommandExt;
use std::sync::atomic::{AtomicI32, Ordering};
static TARGET: AtomicI32 = AtomicI32::new(-1);
fn is_target(fd: c_int) -> bool {
fd == TARGET.load(Ordering::SeqCst)
}
fn child_sees(fd: c_int, close_it: bool) -> String {
let mut cmd = Command::new("sh");
cmd.args([
"-c",
&format!("if [ -e /proc/self/fd/{fd} ]; then echo open; else echo closed; fi"),
]);
if close_it {
unsafe {
cmd.pre_exec(|| {
close_matching(is_target);
Ok(())
});
}
}
let out = cmd.output().unwrap();
String::from_utf8(out.stdout).unwrap().trim().to_string()
}
#[test]
fn closes_the_matching_descriptor_in_the_child_only() {
let file = tempfile::tempfile().unwrap();
let fd = file.as_raw_fd();
assert_eq!(unsafe { libc::fcntl(fd, libc::F_SETFD, 0) }, 0);
TARGET.store(fd, Ordering::SeqCst);
assert_eq!(child_sees(fd, false), "open");
assert_eq!(child_sees(fd, true), "closed");
assert!(unsafe { libc::fcntl(fd, libc::F_GETFD) } >= 0);
}
#[test]
fn a_local_file_is_not_fuse() {
let file = tempfile::tempfile().unwrap();
assert!(!is_fuse(file.as_raw_fd()));
}
#[test]
fn parses_descriptor_names() {
assert_eq!(parse_fd(b"17\0"), Some(17));
assert_eq!(parse_fd(b"3\0\0\0"), Some(3));
assert_eq!(parse_fd(b".\0"), None);
assert_eq!(parse_fd(b"..\0"), None);
assert_eq!(parse_fd(b"\0"), None);
assert_eq!(parse_fd(b"99999999999\0"), None);
}
#[test]
fn plugins_start_with_the_hook() {
let mut cmd = Command::new("true");
close_in_child(&mut cmd);
assert!(cmd.status().unwrap().success());
}
}