xref: /third_party/rust/crates/rustix/src/io/procfs.rs (revision b8a62b91)
1//! Utilities for working with `/proc`, where Linux's `procfs` is typically
2//! mounted. `/proc` serves as an adjunct to Linux's main syscall surface area,
3//! providing additional features with an awkward interface.
4//!
5//! This module does a considerable amount of work to determine whether `/proc`
6//! is mounted, with actual `procfs`, and without any additional mount points
7//! on top of the paths we open.
8//!
9//! Why all the effort to detect bind mount points? People are doing all kinds
10//! of things with Linux containers these days, with many different privilege
11//! schemes, and we want to avoid making any unnecessary assumptions. Rustix
12//! and its users will sometimes use procfs *implicitly* (when Linux gives them
13//! no better options), in ways that aren't obvious from their public APIs.
14//! These filesystem accesses might not be visible to someone auditing the main
15//! code of an application for places which may be influenced by the filesystem
16//! namespace. So with the checking here, they may fail, but they won't be able
17//! to succeed with bogus results.
18
19use crate::fd::{AsFd, BorrowedFd, OwnedFd};
20use crate::ffi::CStr;
21use crate::fs::{
22    cwd, fstat, fstatfs, major, openat, renameat, Dir, FileType, Mode, OFlags, Stat,
23    PROC_SUPER_MAGIC,
24};
25use crate::io;
26use crate::path::DecInt;
27use crate::process::getpid;
28#[cfg(feature = "rustc-dep-of-std")]
29use core::lazy::OnceCell;
30#[cfg(not(feature = "rustc-dep-of-std"))]
31use once_cell::sync::OnceCell;
32
33/// Linux's procfs always uses inode 1 for its root directory.
34const PROC_ROOT_INO: u64 = 1;
35
36// Identify an entry within "/proc", to determine which anomalies to check for.
37#[derive(Copy, Clone, Debug)]
38enum Kind {
39    Proc,
40    Pid,
41    Fd,
42    File,
43}
44
45/// Check a subdirectory of "/proc" for anomalies.
46fn check_proc_entry(
47    kind: Kind,
48    entry: BorrowedFd<'_>,
49    proc_stat: Option<&Stat>,
50) -> io::Result<Stat> {
51    let entry_stat = fstat(entry)?;
52    check_proc_entry_with_stat(kind, entry, entry_stat, proc_stat)
53}
54
55/// Check a subdirectory of "/proc" for anomalies, using the provided `Stat`.
56fn check_proc_entry_with_stat(
57    kind: Kind,
58    entry: BorrowedFd<'_>,
59    entry_stat: Stat,
60    proc_stat: Option<&Stat>,
61) -> io::Result<Stat> {
62    // Check the filesystem magic.
63    check_procfs(entry)?;
64
65    match kind {
66        Kind::Proc => check_proc_root(entry, &entry_stat)?,
67        Kind::Pid | Kind::Fd => check_proc_subdir(entry, &entry_stat, proc_stat)?,
68        Kind::File => check_proc_file(&entry_stat, proc_stat)?,
69    }
70
71    // "/proc" directories are typically mounted r-xr-xr-x.
72    // "/proc/self/fd" is r-x------. Allow them to have fewer permissions, but
73    // not more.
74    let expected_mode = if let Kind::Fd = kind { 0o500 } else { 0o555 };
75    if entry_stat.st_mode & 0o777 & !expected_mode != 0 {
76        return Err(io::Errno::NOTSUP);
77    }
78
79    match kind {
80        Kind::Fd => {
81            // Check that the "/proc/self/fd" directory doesn't have any extraneous
82            // links into it (which might include unexpected subdirectories).
83            if entry_stat.st_nlink != 2 {
84                return Err(io::Errno::NOTSUP);
85            }
86        }
87        Kind::Pid | Kind::Proc => {
88            // Check that the "/proc" and "/proc/self" directories aren't empty.
89            if entry_stat.st_nlink <= 2 {
90                return Err(io::Errno::NOTSUP);
91            }
92        }
93        Kind::File => {
94            // Check that files in procfs don't have extraneous hard links to
95            // them (which might indicate hard links to other things).
96            if entry_stat.st_nlink != 1 {
97                return Err(io::Errno::NOTSUP);
98            }
99        }
100    }
101
102    Ok(entry_stat)
103}
104
105fn check_proc_root(entry: BorrowedFd<'_>, stat: &Stat) -> io::Result<()> {
106    // We use `O_DIRECTORY` for proc directories, so open should fail if we
107    // don't get a directory when we expect one.
108    assert_eq!(FileType::from_raw_mode(stat.st_mode), FileType::Directory);
109
110    // Check the root inode number.
111    if stat.st_ino != PROC_ROOT_INO {
112        return Err(io::Errno::NOTSUP);
113    }
114
115    // Proc is a non-device filesystem, so check for major number 0.
116    // <https://www.kernel.org/doc/Documentation/admin-guide/devices.txt>
117    if major(stat.st_dev) != 0 {
118        return Err(io::Errno::NOTSUP);
119    }
120
121    // Check that "/proc" is a mountpoint.
122    if !is_mountpoint(entry) {
123        return Err(io::Errno::NOTSUP);
124    }
125
126    Ok(())
127}
128
129fn check_proc_subdir(
130    entry: BorrowedFd<'_>,
131    stat: &Stat,
132    proc_stat: Option<&Stat>,
133) -> io::Result<()> {
134    // We use `O_DIRECTORY` for proc directories, so open should fail if we
135    // don't get a directory when we expect one.
136    assert_eq!(FileType::from_raw_mode(stat.st_mode), FileType::Directory);
137
138    check_proc_nonroot(stat, proc_stat)?;
139
140    // Check that subdirectories of "/proc" are not mount points.
141    if is_mountpoint(entry) {
142        return Err(io::Errno::NOTSUP);
143    }
144
145    Ok(())
146}
147
148fn check_proc_file(stat: &Stat, proc_stat: Option<&Stat>) -> io::Result<()> {
149    // Check that we have a regular file.
150    if FileType::from_raw_mode(stat.st_mode) != FileType::RegularFile {
151        return Err(io::Errno::NOTSUP);
152    }
153
154    check_proc_nonroot(stat, proc_stat)?;
155
156    Ok(())
157}
158
159fn check_proc_nonroot(stat: &Stat, proc_stat: Option<&Stat>) -> io::Result<()> {
160    // Check that we haven't been linked back to the root of "/proc".
161    if stat.st_ino == PROC_ROOT_INO {
162        return Err(io::Errno::NOTSUP);
163    }
164
165    // Check that we're still in procfs.
166    if stat.st_dev != proc_stat.unwrap().st_dev {
167        return Err(io::Errno::NOTSUP);
168    }
169
170    Ok(())
171}
172
173/// Check that `file` is opened on a `procfs` filesystem.
174fn check_procfs(file: BorrowedFd<'_>) -> io::Result<()> {
175    let statfs = fstatfs(file)?;
176    let f_type = statfs.f_type;
177    if f_type != PROC_SUPER_MAGIC {
178        return Err(io::Errno::NOTSUP);
179    }
180
181    Ok(())
182}
183
184/// Check whether the given directory handle is a mount point. We use a
185/// `renameat` call that would otherwise fail, but which fails with `EXDEV`
186/// first if it would cross a mount point.
187fn is_mountpoint(file: BorrowedFd<'_>) -> bool {
188    let err = renameat(file, cstr!("../."), file, cstr!(".")).unwrap_err();
189    match err {
190        io::Errno::XDEV => true,  // the rename failed due to crossing a mount point
191        io::Errno::BUSY => false, // the rename failed normally
192        _ => panic!("Unexpected error from `renameat`: {:?}", err),
193    }
194}
195
196/// Open a directory in `/proc`, mapping all errors to `io::Errno::NOTSUP`.
197fn proc_opendirat<P: crate::path::Arg, Fd: AsFd>(dirfd: Fd, path: P) -> io::Result<OwnedFd> {
198    // We could add `PATH`|`NOATIME` here but Linux 2.6.32 doesn't support it.
199    // Also for `NOATIME` see the comment in `open_and_check_file`.
200    let oflags = OFlags::NOFOLLOW | OFlags::DIRECTORY | OFlags::CLOEXEC | OFlags::NOCTTY;
201    openat(dirfd, path, oflags, Mode::empty()).map_err(|_err| io::Errno::NOTSUP)
202}
203
204/// Returns a handle to Linux's `/proc` directory.
205///
206/// This ensures that `/proc` is procfs, that nothing is mounted on top of it,
207/// and that it looks normal. It also returns the `Stat` of `/proc`.
208///
209/// # References
210///  - [Linux]
211///
212/// [Linux]: https://man7.org/linux/man-pages/man5/proc.5.html
213fn proc() -> io::Result<(BorrowedFd<'static>, &'static Stat)> {
214    static PROC: StaticFd = StaticFd::new();
215
216    // `OnceBox` is "racey" in that the initialization function may run
217    // multiple times. We're ok with that, since the initialization function
218    // has no side effects.
219    PROC.get_or_try_init(|| {
220        // Open "/proc".
221        let proc = proc_opendirat(cwd(), cstr!("/proc"))?;
222        let proc_stat =
223            check_proc_entry(Kind::Proc, proc.as_fd(), None).map_err(|_err| io::Errno::NOTSUP)?;
224
225        Ok(new_static_fd(proc, proc_stat))
226    })
227    .map(|(fd, stat)| (fd.as_fd(), stat))
228}
229
230/// Returns a handle to Linux's `/proc/self` directory.
231///
232/// This ensures that `/proc/self` is procfs, that nothing is mounted on top of
233/// it, and that it looks normal. It also returns the `Stat` of `/proc/self`.
234///
235/// # References
236///  - [Linux]
237///
238/// [Linux]: https://man7.org/linux/man-pages/man5/proc.5.html
239fn proc_self() -> io::Result<(BorrowedFd<'static>, &'static Stat)> {
240    static PROC_SELF: StaticFd = StaticFd::new();
241
242    // The init function here may run multiple times; see above.
243    PROC_SELF
244        .get_or_try_init(|| {
245            let (proc, proc_stat) = proc()?;
246
247            let pid = getpid();
248
249            // Open "/proc/self". Use our pid to compute the name rather than literally
250            // using "self", as "self" is a symlink.
251            let proc_self = proc_opendirat(proc, DecInt::new(pid.as_raw_nonzero().get()))?;
252            let proc_self_stat = check_proc_entry(Kind::Pid, proc_self.as_fd(), Some(proc_stat))
253                .map_err(|_err| io::Errno::NOTSUP)?;
254
255            Ok(new_static_fd(proc_self, proc_self_stat))
256        })
257        .map(|(owned, stat)| (owned.as_fd(), stat))
258}
259
260/// Returns a handle to Linux's `/proc/self/fd` directory.
261///
262/// This ensures that `/proc/self/fd` is `procfs`, that nothing is mounted on
263/// top of it, and that it looks normal.
264///
265/// # References
266///  - [Linux]
267///
268/// [Linux]: https://man7.org/linux/man-pages/man5/proc.5.html
269#[cfg_attr(doc_cfg, doc(cfg(feature = "procfs")))]
270pub fn proc_self_fd() -> io::Result<BorrowedFd<'static>> {
271    static PROC_SELF_FD: StaticFd = StaticFd::new();
272
273    // The init function here may run multiple times; see above.
274    PROC_SELF_FD
275        .get_or_try_init(|| {
276            let (_, proc_stat) = proc()?;
277
278            let (proc_self, _proc_self_stat) = proc_self()?;
279
280            // Open "/proc/self/fd".
281            let proc_self_fd = proc_opendirat(proc_self, cstr!("fd"))?;
282            let proc_self_fd_stat =
283                check_proc_entry(Kind::Fd, proc_self_fd.as_fd(), Some(proc_stat))
284                    .map_err(|_err| io::Errno::NOTSUP)?;
285
286            Ok(new_static_fd(proc_self_fd, proc_self_fd_stat))
287        })
288        .map(|(owned, _stat)| owned.as_fd())
289}
290
291type StaticFd = OnceCell<(OwnedFd, Stat)>;
292
293#[inline]
294fn new_static_fd(fd: OwnedFd, stat: Stat) -> (OwnedFd, Stat) {
295    (fd, stat)
296}
297
298/// Returns a handle to Linux's `/proc/self/fdinfo` directory.
299///
300/// This ensures that `/proc/self/fdinfo` is `procfs`, that nothing is mounted
301/// on top of it, and that it looks normal. It also returns the `Stat` of
302/// `/proc/self/fd`.
303///
304/// # References
305///  - [Linux]
306///
307/// [Linux]: https://man7.org/linux/man-pages/man5/proc.5.html
308fn proc_self_fdinfo() -> io::Result<(BorrowedFd<'static>, &'static Stat)> {
309    static PROC_SELF_FDINFO: StaticFd = StaticFd::new();
310
311    PROC_SELF_FDINFO
312        .get_or_try_init(|| {
313            let (_, proc_stat) = proc()?;
314
315            let (proc_self, _proc_self_stat) = proc_self()?;
316
317            // Open "/proc/self/fdinfo".
318            let proc_self_fdinfo = proc_opendirat(proc_self, cstr!("fdinfo"))?;
319            let proc_self_fdinfo_stat =
320                check_proc_entry(Kind::Fd, proc_self_fdinfo.as_fd(), Some(proc_stat))
321                    .map_err(|_err| io::Errno::NOTSUP)?;
322
323            Ok((proc_self_fdinfo, proc_self_fdinfo_stat))
324        })
325        .map(|(owned, stat)| (owned.as_fd(), stat))
326}
327
328/// Returns a handle to a Linux `/proc/self/fdinfo/<fd>` file.
329///
330/// This ensures that `/proc/self/fdinfo/<fd>` is `procfs`, that nothing is
331/// mounted on top of it, and that it looks normal.
332///
333/// # References
334///  - [Linux]
335///
336/// [Linux]: https://man7.org/linux/man-pages/man5/proc.5.html
337#[inline]
338#[cfg_attr(doc_cfg, doc(cfg(feature = "procfs")))]
339pub fn proc_self_fdinfo_fd<Fd: AsFd>(fd: Fd) -> io::Result<OwnedFd> {
340    _proc_self_fdinfo(fd.as_fd())
341}
342
343fn _proc_self_fdinfo(fd: BorrowedFd<'_>) -> io::Result<OwnedFd> {
344    let (proc_self_fdinfo, proc_self_fdinfo_stat) = proc_self_fdinfo()?;
345    let fd_str = DecInt::from_fd(fd);
346    open_and_check_file(proc_self_fdinfo, proc_self_fdinfo_stat, fd_str.as_c_str())
347}
348
349/// Returns a handle to a Linux `/proc/self/pagemap` file.
350///
351/// This ensures that `/proc/self/pagemap` is `procfs`, that nothing is
352/// mounted on top of it, and that it looks normal.
353///
354/// # References
355///  - [Linux]
356///  - [Linux pagemap]
357///
358/// [Linux]: https://man7.org/linux/man-pages/man5/proc.5.html
359/// [Linux pagemap]: https://www.kernel.org/doc/Documentation/vm/pagemap.txt
360#[inline]
361#[cfg_attr(doc_cfg, doc(cfg(feature = "procfs")))]
362pub fn proc_self_pagemap() -> io::Result<OwnedFd> {
363    proc_self_file(cstr!("pagemap"))
364}
365
366/// Returns a handle to a Linux `/proc/self/maps` file.
367///
368/// This ensures that `/proc/self/maps` is `procfs`, that nothing is
369/// mounted on top of it, and that it looks normal.
370///
371/// # References
372///  - [Linux]
373///
374/// [Linux]: https://man7.org/linux/man-pages/man5/proc.5.html
375#[inline]
376#[cfg_attr(doc_cfg, doc(cfg(feature = "procfs")))]
377pub fn proc_self_maps() -> io::Result<OwnedFd> {
378    proc_self_file(cstr!("maps"))
379}
380
381/// Returns a handle to a Linux `/proc/self/status` file.
382///
383/// This ensures that `/proc/self/status` is `procfs`, that nothing is
384/// mounted on top of it, and that it looks normal.
385///
386/// # References
387///  - [Linux]
388///
389/// [Linux]: https://man7.org/linux/man-pages/man5/proc.5.html
390#[inline]
391#[cfg_attr(doc_cfg, doc(cfg(feature = "procfs")))]
392pub fn proc_self_status() -> io::Result<OwnedFd> {
393    proc_self_file(cstr!("status"))
394}
395
396/// Open a file under `/proc/self`.
397fn proc_self_file(name: &CStr) -> io::Result<OwnedFd> {
398    let (proc_self, proc_self_stat) = proc_self()?;
399    open_and_check_file(proc_self, proc_self_stat, name)
400}
401
402/// Open a procfs file within in `dir` and check it for bind mounts.
403fn open_and_check_file(dir: BorrowedFd, dir_stat: &Stat, name: &CStr) -> io::Result<OwnedFd> {
404    let (_, proc_stat) = proc()?;
405
406    // Don't use `NOATIME`, because it [requires us to own the file], and when
407    // a process sets itself non-dumpable Linux changes the user:group of its
408    // `/proc/<pid>` files [to root:root].
409    //
410    // [requires us to own the file]: https://man7.org/linux/man-pages/man2/openat.2.html
411    // [to root:root]: https://man7.org/linux/man-pages/man5/proc.5.html
412    let oflags = OFlags::RDONLY | OFlags::CLOEXEC | OFlags::NOFOLLOW | OFlags::NOCTTY;
413    let file = openat(dir, name, oflags, Mode::empty()).map_err(|_err| io::Errno::NOTSUP)?;
414    let file_stat = fstat(&file)?;
415
416    // `is_mountpoint` only works on directory mount points, not file mount
417    // points. To detect file mount points, scan the parent directory to see
418    // if we can find a regular file with an inode and name that matches the
419    // file we just opened. If we can't find it, there could be a file bind
420    // mount on top of the file we want.
421    //
422    // As we scan, we also check for ".", to make sure it's the same directory
423    // as our original directory, to detect mount points, since
424    // `Dir::read_from` reopens ".".
425    //
426    // TODO: With Linux 5.8 we might be able to use `statx` and
427    // `STATX_ATTR_MOUNT_ROOT` to detect mountpoints directly instead of doing
428    // this scanning.
429    let dir = Dir::read_from(dir).map_err(|_err| io::Errno::NOTSUP)?;
430
431    // Confirm that we got the same inode.
432    let dot_stat = dir.stat().map_err(|_err| io::Errno::NOTSUP)?;
433    if (dot_stat.st_dev, dot_stat.st_ino) != (dir_stat.st_dev, dir_stat.st_ino) {
434        return Err(io::Errno::NOTSUP);
435    }
436
437    let mut found_file = false;
438    let mut found_dot = false;
439    for entry in dir {
440        let entry = entry.map_err(|_err| io::Errno::NOTSUP)?;
441        if entry.ino() == file_stat.st_ino
442            && entry.file_type() == FileType::RegularFile
443            && entry.file_name() == name
444        {
445            // We found the file. Proceed to check the file handle.
446            let _ =
447                check_proc_entry_with_stat(Kind::File, file.as_fd(), file_stat, Some(proc_stat))?;
448
449            found_file = true;
450        } else if entry.ino() == dir_stat.st_ino
451            && entry.file_type() == FileType::Directory
452            && entry.file_name() == cstr!(".")
453        {
454            // We found ".", and it's the right ".".
455            found_dot = true;
456        }
457    }
458
459    if found_file && found_dot {
460        Ok(file)
461    } else {
462        Err(io::Errno::NOTSUP)
463    }
464}
465