rustix_linux_procfs/
lib.rs

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