]>
Commit | Line | Data |
---|---|---|
064997fb FG |
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 | ||
487cf647 | 19 | use crate::fd::{AsFd, BorrowedFd, OwnedFd}; |
064997fb FG |
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 | }; | |
487cf647 | 25 | use crate::io; |
064997fb | 26 | use crate::path::DecInt; |
487cf647 | 27 | use crate::process::getpid; |
064997fb FG |
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. | |
46 | fn check_proc_entry( | |
47 | kind: Kind, | |
48 | entry: BorrowedFd<'_>, | |
49 | proc_stat: Option<&Stat>, | |
064997fb FG |
50 | ) -> io::Result<Stat> { |
51 | let entry_stat = fstat(entry)?; | |
487cf647 | 52 | check_proc_entry_with_stat(kind, entry, entry_stat, proc_stat) |
064997fb FG |
53 | } |
54 | ||
55 | /// Check a subdirectory of "/proc" for anomalies, using the provided `Stat`. | |
56 | fn check_proc_entry_with_stat( | |
57 | kind: Kind, | |
58 | entry: BorrowedFd<'_>, | |
59 | entry_stat: Stat, | |
60 | proc_stat: Option<&Stat>, | |
064997fb FG |
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 | ||
064997fb FG |
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 | ||
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 | ||
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 | ||
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 | ||
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. | |
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 | ||
49aad941 | 184 | /// Check whether the given directory handle is a mount point. |
064997fb | 185 | fn is_mountpoint(file: BorrowedFd<'_>) -> bool { |
49aad941 FG |
186 | // We use a `renameat` call that would otherwise fail, but which fails with |
187 | // `XDEV` first if it would cross a mount point. | |
064997fb FG |
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`. | |
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. | |
487cf647 | 199 | // Also for `NOATIME` see the comment in `open_and_check_file`. |
064997fb FG |
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 | |
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"))?; | |
487cf647 FG |
222 | let proc_stat = |
223 | check_proc_entry(Kind::Proc, proc.as_fd(), None).map_err(|_err| io::Errno::NOTSUP)?; | |
064997fb FG |
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 | |
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 | ||
487cf647 | 247 | let pid = getpid(); |
064997fb FG |
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()))?; | |
487cf647 FG |
252 | let proc_self_stat = check_proc_entry(Kind::Pid, proc_self.as_fd(), Some(proc_stat)) |
253 | .map_err(|_err| io::Errno::NOTSUP)?; | |
064997fb FG |
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")))] | |
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 | ||
487cf647 | 278 | let (proc_self, _proc_self_stat) = proc_self()?; |
064997fb FG |
279 | |
280 | // Open "/proc/self/fd". | |
281 | let proc_self_fd = proc_opendirat(proc_self, cstr!("fd"))?; | |
487cf647 FG |
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)?; | |
064997fb FG |
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] | |
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 | |
308 | fn proc_self_fdinfo() -> io::Result<(BorrowedFd<'static>, &'static Stat)> { | |
487cf647 | 309 | static PROC_SELF_FDINFO: StaticFd = StaticFd::new(); |
064997fb FG |
310 | |
311 | PROC_SELF_FDINFO | |
312 | .get_or_try_init(|| { | |
313 | let (_, proc_stat) = proc()?; | |
314 | ||
487cf647 | 315 | let (proc_self, _proc_self_stat) = proc_self()?; |
064997fb FG |
316 | |
317 | // Open "/proc/self/fdinfo". | |
318 | let proc_self_fdinfo = proc_opendirat(proc_self, cstr!("fdinfo"))?; | |
487cf647 FG |
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)?; | |
064997fb FG |
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")))] | |
339 | pub fn proc_self_fdinfo_fd<Fd: AsFd>(fd: Fd) -> io::Result<OwnedFd> { | |
340 | _proc_self_fdinfo(fd.as_fd()) | |
341 | } | |
342 | ||
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")))] | |
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")))] | |
377 | pub fn proc_self_maps() -> io::Result<OwnedFd> { | |
378 | proc_self_file(cstr!("maps")) | |
379 | } | |
380 | ||
487cf647 FG |
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")))] | |
392 | pub fn proc_self_status() -> io::Result<OwnedFd> { | |
393 | proc_self_file(cstr!("status")) | |
394 | } | |
395 | ||
064997fb FG |
396 | /// Open a file under `/proc/self`. |
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. | |
403 | fn open_and_check_file(dir: BorrowedFd, dir_stat: &Stat, name: &CStr) -> io::Result<OwnedFd> { | |
404 | let (_, proc_stat) = proc()?; | |
405 | ||
487cf647 FG |
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)?; | |
064997fb FG |
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. | |
487cf647 FG |
446 | let _ = |
447 | check_proc_entry_with_stat(Kind::File, file.as_fd(), file_stat, Some(proc_stat))?; | |
064997fb FG |
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 | } |