close_fds/iterfds/
mod.rs

1mod fditer;
2pub use fditer::FdIter;
3
4#[cfg(any(
5    target_os = "linux",
6    target_os = "macos",
7    target_os = "ios",
8    target_os = "freebsd",
9    target_os = "netbsd",
10    target_os = "solaris",
11    target_os = "illumos",
12))]
13mod dirfd;
14
15/// A "builder" to construct an [`FdIter`] with custom parameters.
16///
17/// # Warnings
18///
19/// **TL;DR**: Don't use `FdIter`/`FdIterBuilder` in multithreaded programs unless you know what
20/// you're doing, and avoid opening/closing file descriptors while consuming an `FdIter`.
21///
22/// 1. File descriptors that are opened *during* iteration may or may not be included in the results
23///    (exact behavior is platform-specific and depends on several factors).
24///
25/// 2. **IMPORTANT**: On some platforms, if other threads open file descriptors at very specific
26///    times during a call to `FdIter::next()`, that may result in other file descriptors being
27///    skipped. Use with caution. (If this is a problem for you, set `.threadsafe(true)`, which
28///    avoids this issue).
29///
30/// 3. *Closing* file descriptors during iteration (in the same thread or in another thread) will
31///    not affect the iterator's ability to list other open file descriptors (if it does, that is a
32///    bug). However, in most cases you should use
33///    [`CloseFdsBuilder`](./struct.CloseFdsBuilder.html) to do this.
34///
35/// 4. Some of the file descriptors yielded by this iterator may be in active use by other sections
36///    of code. Be very careful about which operations you perform on them.
37///
38///    If your program is multi-threaded, this is especially true, since a file descriptor returned
39///    by this iterator may have been closed by the time your code tries to do something with it.
40#[derive(Clone, Debug)]
41pub struct FdIterBuilder {
42    possible: bool,
43    #[cfg(any(target_os = "freebsd", target_os = "openbsd"))]
44    skip_nfds: bool,
45    #[cfg(any(
46        target_os = "linux",
47        target_os = "macos",
48        target_os = "ios",
49        target_os = "freebsd",
50        target_os = "netbsd",
51        target_os = "solaris",
52        target_os = "illumos",
53    ))]
54    dirfd: bool,
55}
56
57impl FdIterBuilder {
58    /// Create a new builder.
59    ///
60    /// `minfd` specifies the number of the file descriptor at which iteration will begin.
61    #[inline]
62    pub fn new() -> Self {
63        Self {
64            possible: false,
65            #[cfg(any(target_os = "freebsd", target_os = "openbsd"))]
66            skip_nfds: false,
67            #[cfg(any(
68                target_os = "linux",
69                target_os = "macos",
70                target_os = "ios",
71                target_os = "freebsd",
72                target_os = "netbsd",
73                target_os = "solaris",
74                target_os = "illumos",
75            ))]
76            dirfd: true,
77        }
78    }
79
80    /// Set whether the returned `FdIter` is allowed to yield invalid file descriptors for
81    /// efficiency (default is `false`).
82    ///
83    /// If this flag is set, the caller is responsible for checking if the returned file descriptors
84    /// are valid.
85    ///
86    /// # Proper usage
87    ///
88    /// You should only use this flag if you immediately perform an operation on each file
89    /// descriptor that implicitly checks if the file descriptor is valid.
90    #[inline]
91    pub fn possible(&mut self, possible: bool) -> &mut Self {
92        self.possible = possible;
93        self
94    }
95
96    /// Set whether the returned `FdIter` needs to behave reliably in multithreaded programs
97    /// (default is `false`).
98    ///
99    /// If other threads open file descriptors at specific times, an `FdIter` may skip over other
100    /// file descriptors. Setting `.threadsafe(true)` prevents this, but may come at the cost of
101    /// significantly increased performance on some platforms (because the code which may behave
102    /// strangely in the presence of threads provides a potential performance improvement).
103    ///
104    /// Currently, setting this flag will only affect performance on 1) OpenBSD and 2) FreeBSD
105    /// without an `fdescfs` mounted on `/dev/fd`.
106    #[allow(unused_variables)]
107    #[inline]
108    pub fn threadsafe(&mut self, threadsafe: bool) -> &mut Self {
109        #[cfg(any(target_os = "freebsd", target_os = "openbsd"))]
110        {
111            self.skip_nfds = threadsafe;
112        }
113        self
114    }
115
116    /// Set whether returned `FdIter` is allowed to look at special files for speedups (default is
117    /// `true`).
118    ///
119    /// On some systems, `/dev/fd` and/or `/proc/self/fd` provide an accurate view of the file
120    /// descriptors that the current process has open; if this flag is set to `true` then those
121    /// may be examined as an optimization.
122    ///
123    /// It may be desirable to set this to `false` e.g. if `chroot()`ing into an environment where
124    /// untrusted code may be able to replace `/proc` or `/dev`. However, on some platforms (such
125    /// as Linux<5.9 and macOS) setting this to `false` may significantly decrease performance.
126    #[allow(unused_variables)]
127    #[inline]
128    pub fn allow_filesystem(&mut self, fs: bool) -> &mut Self {
129        #[cfg(any(
130            target_os = "linux",
131            target_os = "macos",
132            target_os = "ios",
133            target_os = "freebsd",
134            target_os = "netbsd",
135            target_os = "solaris",
136            target_os = "illumos",
137        ))]
138        {
139            self.dirfd = fs;
140        }
141        self
142    }
143
144    /// Create an `FdIter` that iterates over the open file descriptors starting at `minfd`.
145    pub fn iter_from(&self, mut minfd: libc::c_int) -> FdIter {
146        if minfd < 0 {
147            minfd = 0;
148        }
149
150        FdIter {
151            curfd: minfd,
152            possible: self.possible,
153            maxfd: None,
154            #[cfg(any(target_os = "freebsd", target_os = "openbsd"))]
155            skip_nfds: self.skip_nfds,
156            #[cfg(any(
157                target_os = "linux",
158                target_os = "macos",
159                target_os = "ios",
160                target_os = "freebsd",
161                target_os = "netbsd",
162                target_os = "solaris",
163                target_os = "illumos",
164            ))]
165            dirfd_iter: if self.dirfd {
166                dirfd::DirFdIter::open(minfd)
167            } else {
168                None
169            },
170        }
171    }
172}
173
174impl Default for FdIterBuilder {
175    #[inline]
176    fn default() -> Self {
177        Self::new()
178    }
179}
180
181/// Iterate over all open file descriptors for the current process, starting at `minfd`. The file
182/// descriptors are guaranteed to be returned in ascending order.
183///
184/// This is equivalent to `FdIterBuilder::new().iter_from(minfd)`.
185///
186/// See the warnings for [`FdIterBuilder`].
187#[inline]
188pub fn iter_open_fds(minfd: libc::c_int) -> FdIter {
189    FdIterBuilder::new().iter_from(minfd)
190}
191
192/// Equivalent to [`iter_open_fds()`], but behaves more reliably in multithreaded programs (at the
193/// cost of decreased performance on some platforms).
194///
195/// This is equivalent to `FdIterBuilder::new().threadsafe(true).iter_from(minfd)`.
196///
197/// See [`FdIterBuilder::threadsafe()`] for more information.
198#[inline]
199pub fn iter_open_fds_threadsafe(minfd: libc::c_int) -> FdIter {
200    FdIterBuilder::new().threadsafe(true).iter_from(minfd)
201}
202
203/// Identical to `iter_open_fds()`, but may -- for efficiency -- yield invalid file descriptors.
204///
205/// This is equivalent to `FdIterBuilder::new().possible(true).iter_from(minfd)`.
206///
207/// See [`FdIterBuilder::possible()`] for more information.
208#[inline]
209pub fn iter_possible_fds(minfd: libc::c_int) -> FdIter {
210    FdIterBuilder::new().possible(true).iter_from(minfd)
211}
212
213/// Identical to `iter_open_fds_threadsafe()`, but may -- for efficiency -- yield invalid file
214/// descriptors.
215///
216/// This is equivalent to `FdIterBuilder::new().possible(true).threadafe(true).iter_from(minfd)`.
217///
218/// See [`FdIterBuilder::possible()`] and [`FdIterBuilder::threadsafe()`] for more information.
219#[inline]
220pub fn iter_possible_fds_threadsafe(minfd: libc::c_int) -> FdIter {
221    FdIterBuilder::new()
222        .possible(true)
223        .threadsafe(true)
224        .iter_from(minfd)
225}
226
227#[cfg(test)]
228mod tests {
229    use super::*;
230
231    fn open_files() -> [libc::c_int; 10] {
232        let mut fds = [-1; 10];
233        for cur_fd in fds.iter_mut() {
234            *cur_fd = unsafe { libc::open("/\0".as_ptr() as *const libc::c_char, libc::O_RDONLY) };
235            assert!(*cur_fd >= 0);
236        }
237        fds
238    }
239
240    unsafe fn close_files(fds: &[libc::c_int]) {
241        for &fd in fds {
242            libc::close(fd);
243        }
244    }
245
246    #[test]
247    fn test_size_hint_open() {
248        test_size_hint_generic(FdIterBuilder::new().threadsafe(false).iter_from(0));
249        test_size_hint_generic(FdIterBuilder::new().threadsafe(true).iter_from(0));
250
251        let fds = open_files();
252        test_size_hint_generic(FdIterBuilder::new().threadsafe(false).iter_from(0));
253        test_size_hint_generic(FdIterBuilder::new().threadsafe(true).iter_from(0));
254        unsafe {
255            close_files(&fds);
256        }
257    }
258
259    #[test]
260    fn test_size_hint_possible() {
261        test_size_hint_generic(
262            FdIterBuilder::new()
263                .possible(true)
264                .threadsafe(false)
265                .iter_from(0),
266        );
267        test_size_hint_generic(
268            FdIterBuilder::new()
269                .possible(true)
270                .threadsafe(true)
271                .iter_from(0),
272        );
273
274        let fds = open_files();
275        test_size_hint_generic(
276            FdIterBuilder::new()
277                .possible(true)
278                .threadsafe(false)
279                .iter_from(0),
280        );
281        test_size_hint_generic(
282            FdIterBuilder::new()
283                .possible(true)
284                .threadsafe(true)
285                .iter_from(0),
286        );
287        unsafe {
288            close_files(&fds);
289        }
290    }
291
292    fn test_size_hint_generic(mut fditer: FdIter) {
293        let (mut init_low, mut init_high) = fditer.size_hint();
294        if let Some(init_high) = init_high {
295            // Sanity check
296            assert!(init_high >= init_low);
297        }
298
299        let mut i = 0;
300        while let Some(_fd) = fditer.next() {
301            let (cur_low, cur_high) = fditer.size_hint();
302
303            // Adjust them so they're comparable to init_low and init_high
304            let adj_low = cur_low + i + 1;
305            let adj_high = if let Some(cur_high) = cur_high {
306                // Sanity check
307                assert!(cur_high >= cur_low);
308
309                Some(cur_high + i + 1)
310            } else {
311                None
312            };
313
314            // Now we adjust init_low and init_high to be the most restrictive limits that we've
315            // received so far.
316            if adj_low > init_low {
317                init_low = adj_low;
318            }
319
320            if let Some(adj_high) = adj_high {
321                if let Some(ihigh) = init_high {
322                    if adj_high < ihigh {
323                        init_high = Some(adj_high);
324                    }
325                } else {
326                    init_high = Some(adj_high);
327                }
328            }
329
330            i += 1;
331        }
332
333        // At the end, the lower boundary should be 0. The upper boundary can be anything.
334        let (final_low, _) = fditer.size_hint();
335        assert_eq!(final_low, 0);
336
337        // Now make sure that the actual count falls within the boundaries we were given
338        assert!(i >= init_low);
339        if let Some(init_high) = init_high {
340            assert!(i <= init_high);
341        }
342    }
343
344    #[test]
345    fn test_fused_open() {
346        test_fused_generic(FdIterBuilder::new().threadsafe(false).iter_from(0));
347        test_fused_generic(FdIterBuilder::new().threadsafe(true).iter_from(0));
348
349        let fds = open_files();
350        test_fused_generic(FdIterBuilder::new().threadsafe(false).iter_from(0));
351        test_fused_generic(FdIterBuilder::new().threadsafe(true).iter_from(0));
352        unsafe {
353            close_files(&fds);
354        }
355    }
356
357    #[test]
358    fn test_fused_possible() {
359        test_fused_generic(
360            FdIterBuilder::new()
361                .possible(true)
362                .threadsafe(false)
363                .iter_from(0),
364        );
365        test_fused_generic(
366            FdIterBuilder::new()
367                .possible(true)
368                .threadsafe(true)
369                .iter_from(0),
370        );
371
372        let fds = open_files();
373        test_fused_generic(
374            FdIterBuilder::new()
375                .possible(true)
376                .threadsafe(false)
377                .iter_from(0),
378        );
379        test_fused_generic(
380            FdIterBuilder::new()
381                .possible(true)
382                .threadsafe(true)
383                .iter_from(0),
384        );
385
386        unsafe {
387            close_files(&fds);
388        }
389    }
390
391    fn test_fused_generic(mut fditer: FdIter) {
392        // Exhaust the iterator
393        fditer.by_ref().count();
394        assert_eq!(fditer.next(), None);
395    }
396}