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