532 lines
18 KiB
Rust
532 lines
18 KiB
Rust
//! 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, seek, FileType, FsWord, Mode, OFlags, RawDir,
|
|
SeekFrom, 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::RDONLY | 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 “racy” 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(docsrs, 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(docsrs, 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(docsrs, 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(docsrs, 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(docsrs, 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;
|
|
|
|
// Open a new fd, so that if we're called on multiple threads, they don't
|
|
// share a seek position.
|
|
let oflags =
|
|
OFlags::RDONLY | OFlags::CLOEXEC | OFlags::NOFOLLOW | OFlags::NOCTTY | OFlags::DIRECTORY;
|
|
let dir = openat(dir, cstr!("."), oflags, Mode::empty()).map_err(|_err| io::Errno::NOTSUP)?;
|
|
let check_dir_stat = fstat(&dir)?;
|
|
if check_dir_stat.st_dev != dir_stat.st_dev || check_dir_stat.st_ino != dir_stat.st_ino {
|
|
return Err(io::Errno::NOTSUP);
|
|
}
|
|
|
|
// Position the directory iteration at the start.
|
|
seek(&dir, SeekFrom::Start(0))?;
|
|
|
|
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)
|
|
}
|
|
}
|