Skip to main content

zsh/extensions/
lowfd.rs

1//! Keep the shell's own file descriptors out of the user's fd space.
2//!
3//! zsh routes every internal open through `movefd()` (Src/utils.c:1990), which
4//! relocates the descriptor to >= 10 with `fcntl(fd, F_DUPFD, 10)` and records
5//! it as `FDT_INTERNAL`. The point is that fds 0-9 belong to the SCRIPT: `exec
6//! 3>out`, `read -u 4`, `print -u 3` and friends address them by number, and a
7//! shell that parks its own state there is squatting on the user's namespace.
8//!
9//! zshrs was squatting. A `-c` run held:
10//!
11//!     fd 3 -> ~/.zshrs/zshrs.log
12//!     fd 4 -> ~/.zshrs/zshrs_history.db          (sqlite)
13//!     fd 5,6,7 -> ~/.zshrs/compsys.db + -wal + -shm
14//!
15//! so `print -u 3 -r -- x` appended `x` to the shell's own LOG and reported
16//! success (zsh: `bad file number: 3`, status 1), and `exec 3>myfile` would
17//! dup2 over the log handle — `exec 4>myfile` over the live sqlite handle.
18//!
19//! `movefd` only works for a descriptor we own and can close. sqlite caches the
20//! fd number inside the connection, so it cannot be relocated after the fact.
21//! The fix therefore has to make the open LAND high: hold every low fd for the
22//! duration of the open, so the kernel's "lowest free descriptor" rule hands out
23//! >= 10, then release. That covers lazily-opened descriptors (sqlite's WAL and
24//! SHM appear on first use) as long as the open happens inside the guard.
25
26use std::os::unix::io::RawFd;
27
28/// The first descriptor the shell may use for itself — everything below belongs
29/// to the script. Matches C's `movefd` threshold (Src/utils.c:1994, `F_DUPFD, 10`).
30const FIRST_INTERNAL_FD: RawFd = 10;
31
32/// Occupies every currently-free descriptor below [`FIRST_INTERNAL_FD`] so that
33/// opens performed while it is alive are pushed above the user's fd range.
34/// Releases them on drop.
35///
36/// Descriptors that are ALREADY open are left alone — they may be the user's
37/// (`zshrs 3>&1 -c '…'` is legal), and stealing them would be the very bug this
38/// exists to prevent.
39pub struct LowFdGuard {
40    held: Vec<RawFd>,
41}
42
43impl LowFdGuard {
44    /// Reserve the free low descriptors. Cheap: at most seven `dup2` calls onto
45    /// `/dev/null`, and only for slots that are actually free.
46    pub fn new() -> Self {
47        let mut held = Vec::new();
48        // SAFETY: plain fd syscalls; every descriptor opened here is recorded and
49        // closed in `drop`.
50        unsafe {
51            let devnull = libc::open(c"/dev/null".as_ptr(), libc::O_RDWR | libc::O_CLOEXEC);
52            if devnull < 0 {
53                return Self { held };
54            }
55            // The /dev/null handle itself lands on the LOWEST free descriptor —
56            // i.e. inside the very range being reserved. Left there, the loop
57            // below would see that slot as "already open", skip it, and then free
58            // it again on drop, so the next open would land right back on it.
59            // Move the handle above the range first (this is `movefd` in miniature,
60            // c:Src/utils.c:1994 `fcntl(fd, F_DUPFD, 10)`).
61            let devnull = {
62                let hi = libc::fcntl(devnull, libc::F_DUPFD, FIRST_INTERNAL_FD);
63                if hi >= 0 {
64                    libc::close(devnull);
65                    hi
66                } else {
67                    devnull
68                }
69            };
70            for fd in 3..FIRST_INTERNAL_FD {
71                // Only claim descriptors that are closed. F_GETFD on a live fd
72                // succeeds, and that one is not ours to take.
73                if libc::fcntl(fd, libc::F_GETFD) != -1 {
74                    continue;
75                }
76                if libc::dup2(devnull, fd) == fd {
77                    held.push(fd);
78                }
79            }
80            libc::close(devnull);
81        }
82        Self { held }
83    }
84}
85
86impl Drop for LowFdGuard {
87    fn drop(&mut self) {
88        for &fd in &self.held {
89            // SAFETY: only descriptors this guard installed are closed.
90            unsafe {
91                libc::close(fd);
92            }
93        }
94    }
95}
96
97impl Default for LowFdGuard {
98    fn default() -> Self {
99        Self::new()
100    }
101}
102
103/// Run `f` with the low descriptors reserved, so anything it opens lands at or
104/// above [`FIRST_INTERNAL_FD`].
105pub fn with_high_fds<T>(f: impl FnOnce() -> T) -> T {
106    let _guard = LowFdGuard::new();
107    f()
108}