use std::os::unix::io::RawFd;
const FIRST_INTERNAL_FD: RawFd = 10;
const FIRST_SCRIPT_FD: RawFd = 3;
const SCRIPT_FD_SLOTS: usize = (FIRST_INTERNAL_FD - FIRST_SCRIPT_FD) as usize;
const FD_SCAN_LIMIT: RawFd = 64;
fn fd_is_open(fd: RawFd) -> bool {
unsafe { libc::fcntl(fd, libc::F_GETFD) != -1 }
}
fn inherited_fds() -> &'static [bool] {
static INHERITED: std::sync::OnceLock<Vec<bool>> = std::sync::OnceLock::new();
INHERITED.get_or_init(|| {
(0..FD_SCAN_LIMIT)
.map(|fd| fd >= FIRST_INTERNAL_FD && fd_is_open(fd))
.collect()
})
}
pub struct LowFdGuard {
held: Vec<RawFd>,
}
impl LowFdGuard {
pub fn new() -> Self {
let _ = inherited_fds();
let mut held = Vec::with_capacity(SCRIPT_FD_SLOTS);
unsafe {
let devnull = libc::open(c"/dev/null".as_ptr(), libc::O_RDWR | libc::O_CLOEXEC);
if devnull < 0 {
return Self { held };
}
let devnull_is_held = (FIRST_SCRIPT_FD..FIRST_INTERNAL_FD).contains(&devnull);
if devnull_is_held {
held.push(devnull);
}
while held.len() < SCRIPT_FD_SLOTS {
let fd = libc::fcntl(devnull, libc::F_DUPFD_CLOEXEC, FIRST_SCRIPT_FD);
if fd < 0 {
break;
}
if fd >= FIRST_INTERNAL_FD {
libc::close(fd);
break;
}
held.push(fd);
}
if !devnull_is_held {
libc::close(devnull);
}
}
tracing::trace!(?held, "lowfd: reserved script descriptors");
Self { held }
}
}
impl Drop for LowFdGuard {
fn drop(&mut self) {
for &fd in &self.held {
unsafe {
libc::close(fd);
}
}
}
}
pub fn register_internal_fds() {
let inherited = inherited_fds();
for fd in FIRST_INTERNAL_FD..FD_SCAN_LIMIT {
if inherited[fd as usize] || !fd_is_open(fd) {
continue;
}
if crate::ported::utils::fdtable_get(fd) != crate::ported::zsh_h::FDT_UNUSED {
continue; }
crate::ported::utils::check_fd_table(fd); crate::ported::utils::fdtable_set(fd, crate::ported::zsh_h::FDT_INTERNAL); tracing::debug!(fd, "lowfd: registered shell-internal descriptor");
}
}
impl Default for LowFdGuard {
fn default() -> Self {
Self::new()
}
}
pub fn with_high_fds<T>(f: impl FnOnce() -> T) -> T {
let out = {
let _guard = LowFdGuard::new();
f()
};
register_internal_fds();
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn an_internal_open_is_registered_in_the_fdtable() {
let _s = SERIAL.lock().unwrap_or_else(|e| e.into_inner());
use std::os::unix::io::AsRawFd;
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join("internal");
let _file = with_high_fds(|| std::fs::File::create(&path).expect("create"));
let fd = _file.as_raw_fd();
assert!(
fd >= FIRST_INTERNAL_FD,
"the guard must push a shell-internal open above the script range; landed on {fd}"
);
assert!(
fd < FD_SCAN_LIMIT,
"test descriptor {fd} is past the sweep bound, the assertion below would be vacuous"
);
assert_eq!(
crate::ported::utils::fdtable_get(fd),
crate::ported::zsh_h::FDT_INTERNAL,
"fd {fd} is the shell's own and must be marked FDT_INTERNAL (c:Src/utils.c:2009)"
);
}
use std::os::unix::fs::MetadataExt as _;
use std::os::unix::io::AsRawFd;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
static SERIAL: Mutex<()> = Mutex::new(());
fn open_script_fds() -> Vec<RawFd> {
(FIRST_SCRIPT_FD..FIRST_INTERNAL_FD)
.filter(|&fd| unsafe { libc::fcntl(fd, libc::F_GETFD) } != -1)
.collect()
}
#[test]
fn guard_pushes_opens_above_the_script_range() {
let _s = SERIAL.lock().unwrap_or_else(|e| e.into_inner());
let guard = LowFdGuard::new();
let f = std::fs::File::open("/dev/null").expect("open /dev/null");
assert!(
f.as_raw_fd() >= FIRST_INTERNAL_FD,
"open inside the guard landed on fd {}, inside the script range",
f.as_raw_fd()
);
drop(f);
drop(guard);
}
#[test]
fn guard_restores_the_script_range_exactly() {
let _s = SERIAL.lock().unwrap_or_else(|e| e.into_inner());
let before = open_script_fds();
{
let _g = LowFdGuard::new();
}
assert_eq!(
before,
open_script_fds(),
"guard leaked or freed descriptors it did not take"
);
}
#[test]
fn concurrent_guards_never_steal_a_descriptor_from_another_thread() {
let _s = SERIAL.lock().unwrap_or_else(|e| e.into_inner());
let path = std::env::temp_dir().join(format!("zshrs-lowfd-{}", std::process::id()));
std::fs::write(&path, b"sentinel").expect("write probe file");
let want_ino = std::fs::metadata(&path).expect("stat probe file").ino();
let c_path = std::ffi::CString::new(path.as_os_str().as_encoded_bytes()).unwrap();
let stop = Arc::new(AtomicBool::new(false));
let claimers: Vec<_> = (0..3)
.map(|_| {
let stop = Arc::clone(&stop);
std::thread::spawn(move || {
while !stop.load(Ordering::Relaxed) {
drop(LowFdGuard::new());
}
})
})
.collect();
let mut stolen = None;
for _ in 0..200_000 {
let fd = unsafe { libc::open(c_path.as_ptr(), libc::O_RDONLY | libc::O_CLOEXEC) };
if fd < 0 {
continue;
}
if fd < FIRST_INTERNAL_FD {
std::thread::yield_now();
let mut st: libc::stat = unsafe { std::mem::zeroed() };
if unsafe { libc::fstat(fd, &mut st) } == 0 && st.st_ino != want_ino {
stolen = Some(fd);
}
}
unsafe { libc::close(fd) };
if stolen.is_some() {
break;
}
}
stop.store(true, Ordering::Relaxed);
for t in claimers {
let _ = t.join();
}
let _ = std::fs::remove_file(&path);
assert!(
stolen.is_none(),
"a LowFdGuard on another thread took over live fd {:?}",
stolen
);
}
#[test]
fn guard_still_works_in_a_fork_child_while_other_threads_hold_it() {
let _s = SERIAL.lock().unwrap_or_else(|e| e.into_inner());
let stop = Arc::new(AtomicBool::new(false));
let hammers: Vec<_> = (0..4)
.map(|_| {
let stop = Arc::clone(&stop);
std::thread::spawn(move || {
while !stop.load(Ordering::Relaxed) {
drop(LowFdGuard::new());
}
})
})
.collect();
let mut stuck = 0;
let mut failed = 0;
for _ in 0..20 {
let pid = unsafe { libc::fork() };
if pid == 0 {
let g = LowFdGuard::new();
let fd = unsafe { libc::open(c"/dev/null".as_ptr(), libc::O_RDONLY) };
let ok = fd >= FIRST_INTERNAL_FD;
unsafe { libc::close(fd) };
drop(g);
unsafe { libc::_exit(i32::from(!ok)) };
}
assert!(pid > 0, "fork failed");
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5);
let mut status = 0;
loop {
let r = unsafe { libc::waitpid(pid, &mut status, libc::WNOHANG) };
if r == pid {
if libc::WEXITSTATUS(status) != 0 {
failed += 1;
}
break;
}
if std::time::Instant::now() > deadline {
stuck += 1;
unsafe { libc::kill(pid, libc::SIGKILL) };
unsafe { libc::waitpid(pid, &mut status, 0) };
break;
}
std::thread::sleep(std::time::Duration::from_millis(2));
}
}
stop.store(true, Ordering::Relaxed);
for t in hammers {
let _ = t.join();
}
assert_eq!(stuck, 0, "LowFdGuard::new() hung in a fork child");
assert_eq!(
failed, 0,
"an open inside a fork child's guard landed in the script fd range"
);
}
#[test]
fn reserved_descriptors_are_close_on_exec() {
let _s = SERIAL.lock().unwrap_or_else(|e| e.into_inner());
let before = open_script_fds();
let guard = LowFdGuard::new();
for &fd in &guard.held {
let flags = unsafe { libc::fcntl(fd, libc::F_GETFD) };
assert!(flags != -1, "reserved fd {fd} is not open");
assert!(
flags & libc::FD_CLOEXEC != 0,
"reserved fd {fd} would leak through exec"
);
}
for fd in &before {
assert!(
!guard.held.contains(fd),
"guard claimed fd {fd}, which was already open"
);
}
drop(guard);
}
}