• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
19 use crate::fd::{AsFd, BorrowedFd, OwnedFd};
20 use crate::ffi::CStr;
21 use crate::fs::{
22     cwd, fstat, fstatfs, major, openat, renameat, Dir, FileType, Mode, OFlags, Stat,
23     PROC_SUPER_MAGIC,
24 };
25 use crate::io;
26 use crate::path::DecInt;
27 use crate::process::getpid;
28 #[cfg(feature = "rustc-dep-of-std")]
29 use core::lazy::OnceCell;
30 #[cfg(not(feature = "rustc-dep-of-std"))]
31 use once_cell::sync::OnceCell;
32 
33 /// Linux's procfs always uses inode 1 for its root directory.
34 const PROC_ROOT_INO: u64 = 1;
35 
36 // Identify an entry within "/proc", to determine which anomalies to check for.
37 #[derive(Copy, Clone, Debug)]
38 enum Kind {
39     Proc,
40     Pid,
41     Fd,
42     File,
43 }
44 
45 /// Check a subdirectory of "/proc" for anomalies.
check_proc_entry( kind: Kind, entry: BorrowedFd<'_>, proc_stat: Option<&Stat>, ) -> io::Result<Stat>46 fn 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`.
check_proc_entry_with_stat( kind: Kind, entry: BorrowedFd<'_>, entry_stat: Stat, proc_stat: Option<&Stat>, ) -> io::Result<Stat>56 fn 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 
check_proc_root(entry: BorrowedFd<'_>, stat: &Stat) -> io::Result<()>105 fn 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 
check_proc_subdir( entry: BorrowedFd<'_>, stat: &Stat, proc_stat: Option<&Stat>, ) -> io::Result<()>129 fn 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 
check_proc_file(stat: &Stat, proc_stat: Option<&Stat>) -> io::Result<()>148 fn 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 
check_proc_nonroot(stat: &Stat, proc_stat: Option<&Stat>) -> io::Result<()>159 fn 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.
check_procfs(file: BorrowedFd<'_>) -> io::Result<()>174 fn 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.
is_mountpoint(file: BorrowedFd<'_>) -> bool187 fn 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`.
proc_opendirat<P: crate::path::Arg, Fd: AsFd>(dirfd: Fd, path: P) -> io::Result<OwnedFd>197 fn 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
proc() -> io::Result<(BorrowedFd<'static>, &'static Stat)>213 fn 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
proc_self() -> io::Result<(BorrowedFd<'static>, &'static Stat)>239 fn 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")))]
proc_self_fd() -> io::Result<BorrowedFd<'static>>270 pub 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 
291 type StaticFd = OnceCell<(OwnedFd, Stat)>;
292 
293 #[inline]
new_static_fd(fd: OwnedFd, stat: Stat) -> (OwnedFd, Stat)294 fn 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
proc_self_fdinfo() -> io::Result<(BorrowedFd<'static>, &'static Stat)>308 fn 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")))]
proc_self_fdinfo_fd<Fd: AsFd>(fd: Fd) -> io::Result<OwnedFd>339 pub fn proc_self_fdinfo_fd<Fd: AsFd>(fd: Fd) -> io::Result<OwnedFd> {
340     _proc_self_fdinfo(fd.as_fd())
341 }
342 
_proc_self_fdinfo(fd: BorrowedFd<'_>) -> io::Result<OwnedFd>343 fn _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")))]
proc_self_pagemap() -> io::Result<OwnedFd>362 pub 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")))]
proc_self_maps() -> io::Result<OwnedFd>377 pub 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")))]
proc_self_status() -> io::Result<OwnedFd>392 pub fn proc_self_status() -> io::Result<OwnedFd> {
393     proc_self_file(cstr!("status"))
394 }
395 
396 /// Open a file under `/proc/self`.
proc_self_file(name: &CStr) -> io::Result<OwnedFd>397 fn 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.
open_and_check_file(dir: BorrowedFd, dir_stat: &Stat, name: &CStr) -> io::Result<OwnedFd>403 fn 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