diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
commit | 26a029d407be480d791972afb5975cf62c9360a6 (patch) | |
tree | f435a8308119effd964b339f76abb83a57c29483 /third_party/rust/rustix/src/procfs.rs | |
parent | Initial commit. (diff) | |
download | firefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz firefox-26a029d407be480d791972afb5975cf62c9360a6.zip |
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'third_party/rust/rustix/src/procfs.rs')
-rw-r--r-- | third_party/rust/rustix/src/procfs.rs | 517 |
1 files changed, 517 insertions, 0 deletions
diff --git a/third_party/rust/rustix/src/procfs.rs b/third_party/rust/rustix/src/procfs.rs new file mode 100644 index 0000000000..38f0911975 --- /dev/null +++ b/third_party/rust/rustix/src/procfs.rs @@ -0,0 +1,517 @@ +//! Utilities for working with `/proc`, where Linux's `procfs` is typically +//! mounted. +//! +//! `/proc` serves as an adjunct to Linux's main syscall surface area, +//! providing additional features with an awkward interface. +//! +//! This module does a considerable amount of work to determine whether `/proc` +//! is mounted, with actual `procfs`, and without any additional mount points +//! on top of the paths we open. +//! +//! Why all the effort to detect bind mount points? People are doing all kinds +//! of things with Linux containers these days, with many different privilege +//! schemes, and we want to avoid making any unnecessary assumptions. Rustix +//! and its users will sometimes use procfs *implicitly* (when Linux gives them +//! no better options), in ways that aren't obvious from their public APIs. +//! These filesystem accesses might not be visible to someone auditing the main +//! code of an application for places which may be influenced by the filesystem +//! namespace. So with the checking here, they may fail, but they won't be able +//! to succeed with bogus results. + +use crate::fd::{AsFd, BorrowedFd, OwnedFd}; +use crate::ffi::CStr; +use crate::fs::{ + fstat, fstatfs, major, openat, renameat, FileType, FsWord, Mode, OFlags, RawDir, Stat, CWD, + PROC_SUPER_MAGIC, +}; +use crate::io; +use crate::path::DecInt; +#[cfg(feature = "rustc-dep-of-std")] +use core::lazy::OnceCell; +use core::mem::MaybeUninit; +#[cfg(not(feature = "rustc-dep-of-std"))] +use once_cell::sync::OnceCell; + +/// Linux's procfs always uses inode 1 for its root directory. +const PROC_ROOT_INO: u64 = 1; + +// Identify an entry within "/proc", to determine which anomalies to check for. +#[derive(Copy, Clone, Debug)] +enum Kind { + Proc, + Pid, + Fd, + File, + Symlink, +} + +/// Check a subdirectory of "/proc" for anomalies. +fn check_proc_entry( + kind: Kind, + entry: BorrowedFd<'_>, + proc_stat: Option<&Stat>, +) -> io::Result<Stat> { + let entry_stat = fstat(entry)?; + check_proc_entry_with_stat(kind, entry, entry_stat, proc_stat) +} + +/// Check a subdirectory of "/proc" for anomalies, using the provided `Stat`. +fn check_proc_entry_with_stat( + kind: Kind, + entry: BorrowedFd<'_>, + entry_stat: Stat, + proc_stat: Option<&Stat>, +) -> io::Result<Stat> { + // Check the filesystem magic. + check_procfs(entry)?; + + match kind { + Kind::Proc => check_proc_root(entry, &entry_stat)?, + Kind::Pid | Kind::Fd => check_proc_subdir(entry, &entry_stat, proc_stat)?, + Kind::File => check_proc_file(&entry_stat, proc_stat)?, + Kind::Symlink => check_proc_symlink(&entry_stat, proc_stat)?, + } + + // "/proc" directories are typically mounted r-xr-xr-x. + // "/proc/self/fd" is r-x------. Allow them to have fewer permissions, but + // not more. + match kind { + Kind::Symlink => { + // On Linux, symlinks don't have their own permissions. + } + _ => { + let expected_mode = if let Kind::Fd = kind { 0o500 } else { 0o555 }; + if entry_stat.st_mode & 0o777 & !expected_mode != 0 { + return Err(io::Errno::NOTSUP); + } + } + } + + match kind { + Kind::Fd => { + // Check that the "/proc/self/fd" directory doesn't have any + // extraneous links into it (which might include unexpected + // subdirectories). + if entry_stat.st_nlink != 2 { + return Err(io::Errno::NOTSUP); + } + } + Kind::Pid | Kind::Proc => { + // Check that the "/proc" and "/proc/self" directories aren't + // empty. + if entry_stat.st_nlink <= 2 { + return Err(io::Errno::NOTSUP); + } + } + Kind::File => { + // Check that files in procfs don't have extraneous hard links to + // them (which might indicate hard links to other things). + if entry_stat.st_nlink != 1 { + return Err(io::Errno::NOTSUP); + } + } + Kind::Symlink => { + // Check that symlinks in procfs don't have extraneous hard links + // to them (which might indicate hard links to other things). + if entry_stat.st_nlink != 1 { + return Err(io::Errno::NOTSUP); + } + } + } + + Ok(entry_stat) +} + +fn check_proc_root(entry: BorrowedFd<'_>, stat: &Stat) -> io::Result<()> { + // We use `O_DIRECTORY` for proc directories, so open should fail if we + // don't get a directory when we expect one. + assert_eq!(FileType::from_raw_mode(stat.st_mode), FileType::Directory); + + // Check the root inode number. + if stat.st_ino != PROC_ROOT_INO { + return Err(io::Errno::NOTSUP); + } + + // Proc is a non-device filesystem, so check for major number 0. + // <https://www.kernel.org/doc/Documentation/admin-guide/devices.txt> + if major(stat.st_dev) != 0 { + return Err(io::Errno::NOTSUP); + } + + // Check that "/proc" is a mountpoint. + if !is_mountpoint(entry) { + return Err(io::Errno::NOTSUP); + } + + Ok(()) +} + +fn check_proc_subdir( + entry: BorrowedFd<'_>, + stat: &Stat, + proc_stat: Option<&Stat>, +) -> io::Result<()> { + // We use `O_DIRECTORY` for proc directories, so open should fail if we + // don't get a directory when we expect one. + assert_eq!(FileType::from_raw_mode(stat.st_mode), FileType::Directory); + + check_proc_nonroot(stat, proc_stat)?; + + // Check that subdirectories of "/proc" are not mount points. + if is_mountpoint(entry) { + return Err(io::Errno::NOTSUP); + } + + Ok(()) +} + +fn check_proc_file(stat: &Stat, proc_stat: Option<&Stat>) -> io::Result<()> { + // Check that we have a regular file. + if FileType::from_raw_mode(stat.st_mode) != FileType::RegularFile { + return Err(io::Errno::NOTSUP); + } + + check_proc_nonroot(stat, proc_stat)?; + + Ok(()) +} + +fn check_proc_symlink(stat: &Stat, proc_stat: Option<&Stat>) -> io::Result<()> { + // Check that we have a symbolic link. + if FileType::from_raw_mode(stat.st_mode) != FileType::Symlink { + return Err(io::Errno::NOTSUP); + } + + check_proc_nonroot(stat, proc_stat)?; + + Ok(()) +} + +fn check_proc_nonroot(stat: &Stat, proc_stat: Option<&Stat>) -> io::Result<()> { + // Check that we haven't been linked back to the root of "/proc". + if stat.st_ino == PROC_ROOT_INO { + return Err(io::Errno::NOTSUP); + } + + // Check that we're still in procfs. + if stat.st_dev != proc_stat.unwrap().st_dev { + return Err(io::Errno::NOTSUP); + } + + Ok(()) +} + +/// Check that `file` is opened on a `procfs` filesystem. +fn check_procfs(file: BorrowedFd<'_>) -> io::Result<()> { + let statfs = fstatfs(file)?; + let f_type = statfs.f_type; + if f_type != FsWord::from(PROC_SUPER_MAGIC) { + return Err(io::Errno::NOTSUP); + } + + Ok(()) +} + +/// Check whether the given directory handle is a mount point. +fn is_mountpoint(file: BorrowedFd<'_>) -> bool { + // We use a `renameat` call that would otherwise fail, but which fails with + // `XDEV` first if it would cross a mount point. + let err = renameat(file, cstr!("../."), file, cstr!(".")).unwrap_err(); + match err { + io::Errno::XDEV => true, // the rename failed due to crossing a mount point + io::Errno::BUSY => false, // the rename failed normally + _ => panic!("Unexpected error from `renameat`: {:?}", err), + } +} + +/// Open a directory in `/proc`, mapping all errors to `io::Errno::NOTSUP`. +fn proc_opendirat<P: crate::path::Arg, Fd: AsFd>(dirfd: Fd, path: P) -> io::Result<OwnedFd> { + // We don't add `PATH` here because that disables `DIRECTORY`. And we don't + // add `NOATIME` for the same reason as the comment in `open_and_check_file`. + let oflags = OFlags::NOFOLLOW | OFlags::DIRECTORY | OFlags::CLOEXEC | OFlags::NOCTTY; + openat(dirfd, path, oflags, Mode::empty()).map_err(|_err| io::Errno::NOTSUP) +} + +/// Returns a handle to Linux's `/proc` directory. +/// +/// This ensures that `/proc` is procfs, that nothing is mounted on top of it, +/// and that it looks normal. It also returns the `Stat` of `/proc`. +/// +/// # References +/// - [Linux] +/// +/// [Linux]: https://man7.org/linux/man-pages/man5/proc.5.html +fn proc() -> io::Result<(BorrowedFd<'static>, &'static Stat)> { + static PROC: StaticFd = StaticFd::new(); + + // `OnceBox` is "racey" in that the initialization function may run + // multiple times. We're ok with that, since the initialization function + // has no side effects. + PROC.get_or_try_init(|| { + // Open "/proc". + let proc = proc_opendirat(CWD, cstr!("/proc"))?; + let proc_stat = + check_proc_entry(Kind::Proc, proc.as_fd(), None).map_err(|_err| io::Errno::NOTSUP)?; + + Ok(new_static_fd(proc, proc_stat)) + }) + .map(|(fd, stat)| (fd.as_fd(), stat)) +} + +/// Returns a handle to Linux's `/proc/self` directory. +/// +/// This ensures that `/proc/self` is procfs, that nothing is mounted on top of +/// it, and that it looks normal. It also returns the `Stat` of `/proc/self`. +/// +/// # References +/// - [Linux] +/// +/// [Linux]: https://man7.org/linux/man-pages/man5/proc.5.html +#[allow(unsafe_code)] +fn proc_self() -> io::Result<(BorrowedFd<'static>, &'static Stat)> { + static PROC_SELF: StaticFd = StaticFd::new(); + + // The init function here may run multiple times; see above. + PROC_SELF + .get_or_try_init(|| { + let (proc, proc_stat) = proc()?; + + // `getpid` would return our pid in our own pid namespace, so + // instead use `readlink` on the `self` symlink to learn our pid in + // the procfs namespace. + let self_symlink = open_and_check_file(proc, proc_stat, cstr!("self"), Kind::Symlink)?; + let mut buf = [MaybeUninit::<u8>::uninit(); 20]; + let len = crate::backend::fs::syscalls::readlinkat( + self_symlink.as_fd(), + cstr!(""), + &mut buf, + )?; + let pid: &[u8] = unsafe { core::mem::transmute(&buf[..len]) }; + + // Open "/proc/self". Use our pid to compute the name rather than + // literally using "self", as "self" is a symlink. + let proc_self = proc_opendirat(proc, pid)?; + let proc_self_stat = check_proc_entry(Kind::Pid, proc_self.as_fd(), Some(proc_stat)) + .map_err(|_err| io::Errno::NOTSUP)?; + + Ok(new_static_fd(proc_self, proc_self_stat)) + }) + .map(|(owned, stat)| (owned.as_fd(), stat)) +} + +/// Returns a handle to Linux's `/proc/self/fd` directory. +/// +/// This ensures that `/proc/self/fd` is `procfs`, that nothing is mounted on +/// top of it, and that it looks normal. +/// +/// # References +/// - [Linux] +/// +/// [Linux]: https://man7.org/linux/man-pages/man5/proc.5.html +#[cfg_attr(doc_cfg, doc(cfg(feature = "procfs")))] +pub fn proc_self_fd() -> io::Result<BorrowedFd<'static>> { + static PROC_SELF_FD: StaticFd = StaticFd::new(); + + // The init function here may run multiple times; see above. + PROC_SELF_FD + .get_or_try_init(|| { + let (_, proc_stat) = proc()?; + + let (proc_self, _proc_self_stat) = proc_self()?; + + // Open "/proc/self/fd". + let proc_self_fd = proc_opendirat(proc_self, cstr!("fd"))?; + let proc_self_fd_stat = + check_proc_entry(Kind::Fd, proc_self_fd.as_fd(), Some(proc_stat)) + .map_err(|_err| io::Errno::NOTSUP)?; + + Ok(new_static_fd(proc_self_fd, proc_self_fd_stat)) + }) + .map(|(owned, _stat)| owned.as_fd()) +} + +type StaticFd = OnceCell<(OwnedFd, Stat)>; + +#[inline] +fn new_static_fd(fd: OwnedFd, stat: Stat) -> (OwnedFd, Stat) { + (fd, stat) +} + +/// Returns a handle to Linux's `/proc/self/fdinfo` directory. +/// +/// This ensures that `/proc/self/fdinfo` is `procfs`, that nothing is mounted +/// on top of it, and that it looks normal. It also returns the `Stat` of +/// `/proc/self/fd`. +/// +/// # References +/// - [Linux] +/// +/// [Linux]: https://man7.org/linux/man-pages/man5/proc.5.html +fn proc_self_fdinfo() -> io::Result<(BorrowedFd<'static>, &'static Stat)> { + static PROC_SELF_FDINFO: StaticFd = StaticFd::new(); + + PROC_SELF_FDINFO + .get_or_try_init(|| { + let (_, proc_stat) = proc()?; + + let (proc_self, _proc_self_stat) = proc_self()?; + + // Open "/proc/self/fdinfo". + let proc_self_fdinfo = proc_opendirat(proc_self, cstr!("fdinfo"))?; + let proc_self_fdinfo_stat = + check_proc_entry(Kind::Fd, proc_self_fdinfo.as_fd(), Some(proc_stat)) + .map_err(|_err| io::Errno::NOTSUP)?; + + Ok((proc_self_fdinfo, proc_self_fdinfo_stat)) + }) + .map(|(owned, stat)| (owned.as_fd(), stat)) +} + +/// Returns a handle to a Linux `/proc/self/fdinfo/<fd>` file. +/// +/// This ensures that `/proc/self/fdinfo/<fd>` is `procfs`, that nothing is +/// mounted on top of it, and that it looks normal. +/// +/// # References +/// - [Linux] +/// +/// [Linux]: https://man7.org/linux/man-pages/man5/proc.5.html +#[inline] +#[cfg_attr(doc_cfg, doc(cfg(feature = "procfs")))] +pub fn proc_self_fdinfo_fd<Fd: AsFd>(fd: Fd) -> io::Result<OwnedFd> { + _proc_self_fdinfo(fd.as_fd()) +} + +fn _proc_self_fdinfo(fd: BorrowedFd<'_>) -> io::Result<OwnedFd> { + let (proc_self_fdinfo, proc_self_fdinfo_stat) = proc_self_fdinfo()?; + let fd_str = DecInt::from_fd(fd); + open_and_check_file( + proc_self_fdinfo, + proc_self_fdinfo_stat, + fd_str.as_c_str(), + Kind::File, + ) +} + +/// Returns a handle to a Linux `/proc/self/pagemap` file. +/// +/// This ensures that `/proc/self/pagemap` is `procfs`, that nothing is +/// mounted on top of it, and that it looks normal. +/// +/// # References +/// - [Linux] +/// - [Linux pagemap] +/// +/// [Linux]: https://man7.org/linux/man-pages/man5/proc.5.html +/// [Linux pagemap]: https://www.kernel.org/doc/Documentation/vm/pagemap.txt +#[inline] +#[cfg_attr(doc_cfg, doc(cfg(feature = "procfs")))] +pub fn proc_self_pagemap() -> io::Result<OwnedFd> { + proc_self_file(cstr!("pagemap")) +} + +/// Returns a handle to a Linux `/proc/self/maps` file. +/// +/// This ensures that `/proc/self/maps` is `procfs`, that nothing is +/// mounted on top of it, and that it looks normal. +/// +/// # References +/// - [Linux] +/// +/// [Linux]: https://man7.org/linux/man-pages/man5/proc.5.html +#[inline] +#[cfg_attr(doc_cfg, doc(cfg(feature = "procfs")))] +pub fn proc_self_maps() -> io::Result<OwnedFd> { + proc_self_file(cstr!("maps")) +} + +/// Returns a handle to a Linux `/proc/self/status` file. +/// +/// This ensures that `/proc/self/status` is `procfs`, that nothing is +/// mounted on top of it, and that it looks normal. +/// +/// # References +/// - [Linux] +/// +/// [Linux]: https://man7.org/linux/man-pages/man5/proc.5.html +#[inline] +#[cfg_attr(doc_cfg, doc(cfg(feature = "procfs")))] +pub fn proc_self_status() -> io::Result<OwnedFd> { + proc_self_file(cstr!("status")) +} + +/// Open a file under `/proc/self`. +fn proc_self_file(name: &CStr) -> io::Result<OwnedFd> { + let (proc_self, proc_self_stat) = proc_self()?; + open_and_check_file(proc_self, proc_self_stat, name, Kind::File) +} + +/// Open a procfs file within in `dir` and check it for bind mounts. +fn open_and_check_file( + dir: BorrowedFd<'_>, + dir_stat: &Stat, + name: &CStr, + kind: Kind, +) -> io::Result<OwnedFd> { + let (_, proc_stat) = proc()?; + + // Don't use `NOATIME`, because it [requires us to own the file], and when + // a process sets itself non-dumpable Linux changes the user:group of its + // `/proc/<pid>` files [to root:root]. + // + // [requires us to own the file]: https://man7.org/linux/man-pages/man2/openat.2.html + // [to root:root]: https://man7.org/linux/man-pages/man5/proc.5.html + let mut oflags = OFlags::RDONLY | OFlags::CLOEXEC | OFlags::NOFOLLOW | OFlags::NOCTTY; + if let Kind::Symlink = kind { + // Open symlinks with `O_PATH`. + oflags |= OFlags::PATH; + } + let file = openat(dir, name, oflags, Mode::empty()).map_err(|_err| io::Errno::NOTSUP)?; + let file_stat = fstat(&file)?; + + // `is_mountpoint` only works on directory mount points, not file mount + // points. To detect file mount points, scan the parent directory to see if + // we can find a regular file with an inode and name that matches the file + // we just opened. If we can't find it, there could be a file bind mount on + // top of the file we want. + // + // TODO: With Linux 5.8 we might be able to use `statx` and + // `STATX_ATTR_MOUNT_ROOT` to detect mountpoints directly instead of doing + // this scanning. + + let expected_type = match kind { + Kind::File => FileType::RegularFile, + Kind::Symlink => FileType::Symlink, + _ => unreachable!(), + }; + + let mut found_file = false; + let mut found_dot = false; + + let mut buf = [MaybeUninit::uninit(); 2048]; + let mut iter = RawDir::new(dir, &mut buf); + while let Some(entry) = iter.next() { + let entry = entry.map_err(|_err| io::Errno::NOTSUP)?; + if entry.ino() == file_stat.st_ino + && entry.file_type() == expected_type + && entry.file_name() == name + { + // We found the file. Proceed to check the file handle. + let _ = check_proc_entry_with_stat(kind, file.as_fd(), file_stat, Some(proc_stat))?; + + found_file = true; + } else if entry.ino() == dir_stat.st_ino + && entry.file_type() == FileType::Directory + && entry.file_name() == cstr!(".") + { + // We found ".", and it's the right ".". + found_dot = true; + } + } + + if found_file && found_dot { + Ok(file) + } else { + Err(io::Errno::NOTSUP) + } +} |