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