pathrs/utils/fd.rs
1// SPDX-License-Identifier: MPL-2.0 OR LGPL-3.0-or-later
2/*
3 * libpathrs: safe path resolution on Linux
4 * Copyright (C) 2019-2025 SUSE LLC
5 * Copyright (C) 2026 Aleksa Sarai <cyphar@cyphar.com>
6 *
7 * == MPL-2.0 ==
8 *
9 * This Source Code Form is subject to the terms of the Mozilla Public
10 * License, v. 2.0. If a copy of the MPL was not distributed with this
11 * file, You can obtain one at https://mozilla.org/MPL/2.0/.
12 *
13 * Alternatively, this Source Code Form may also (at your option) be used
14 * under the terms of the GNU Lesser General Public License Version 3, as
15 * described below:
16 *
17 * == LGPL-3.0-or-later ==
18 *
19 * This program is free software: you can redistribute it and/or modify it
20 * under the terms of the GNU Lesser General Public License as published by
21 * the Free Software Foundation, either version 3 of the License, or (at
22 * your option) any later version.
23 *
24 * This program is distributed in the hope that it will be useful, but
25 * WITHOUT ANY WARRANTY; without even the implied warranty of
26 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
27 * Public License for more details.
28 *
29 * You should have received a copy of the GNU Lesser General Public License
30 * along with this program. If not, see <https://www.gnu.org/licenses/>.
31 */
32
33use crate::{
34 error::{Error, ErrorExt, ErrorImpl, ErrorKind},
35 flags::OpenFlags,
36 procfs::{self, ProcfsBase, ProcfsHandle},
37 syscalls,
38 utils::{self, MaybeOwnedFd, RawProcfsRoot},
39};
40
41use std::{
42 fs::{self, File},
43 io::Error as IOError,
44 os::unix::{
45 fs::MetadataExt,
46 io::{AsFd, AsRawFd, OwnedFd, RawFd},
47 },
48 path::{Path, PathBuf},
49 str::FromStr,
50};
51
52use rustix::fs::{self as rustix_fs, StatxFlags};
53
54pub(crate) struct Metadata(rustix_fs::Stat);
55
56impl Metadata {
57 pub(crate) fn is_symlink(&self) -> bool {
58 self.mode() & libc::S_IFMT == libc::S_IFLNK
59 }
60}
61
62#[allow(clippy::useless_conversion)] // 32-bit arches
63impl MetadataExt for Metadata {
64 fn dev(&self) -> u64 {
65 self.0.st_dev.into()
66 }
67
68 fn ino(&self) -> u64 {
69 self.0.st_ino.into()
70 }
71
72 fn mode(&self) -> u32 {
73 self.0.st_mode
74 }
75
76 fn nlink(&self) -> u64 {
77 self.0.st_nlink.into()
78 }
79
80 fn uid(&self) -> u32 {
81 self.0.st_uid
82 }
83
84 fn gid(&self) -> u32 {
85 self.0.st_gid
86 }
87
88 fn rdev(&self) -> u64 {
89 self.0.st_rdev.into()
90 }
91
92 fn size(&self) -> u64 {
93 self.0.st_size as u64
94 }
95
96 fn atime(&self) -> i64 {
97 self.0.st_atime
98 }
99
100 fn atime_nsec(&self) -> i64 {
101 self.0.st_atime_nsec as i64
102 }
103
104 fn mtime(&self) -> i64 {
105 self.0.st_mtime
106 }
107
108 fn mtime_nsec(&self) -> i64 {
109 self.0.st_mtime_nsec as i64
110 }
111
112 fn ctime(&self) -> i64 {
113 self.0.st_ctime
114 }
115
116 fn ctime_nsec(&self) -> i64 {
117 self.0.st_ctime_nsec as i64
118 }
119
120 fn blksize(&self) -> u64 {
121 self.0.st_blksize as u64
122 }
123
124 fn blocks(&self) -> u64 {
125 self.0.st_blocks as u64
126 }
127}
128
129pub(crate) trait FdExt: AsFd {
130 /// Equivalent to [`File::metadata`].
131 ///
132 /// [`File::metadata`]: std::fs::File::metadata
133 fn metadata(&self) -> Result<Metadata, Error>;
134
135 /// Re-open a file descriptor.
136 fn reopen(&self, procfs: &ProcfsHandle, flags: OpenFlags) -> Result<OwnedFd, Error>;
137
138 /// Get the path this RawFd is referencing.
139 ///
140 /// This is done through `readlink(/proc/self/fd)` and is naturally racy
141 /// (hence the name "unsafe"), so it's important to only use this with the
142 /// understanding that it only provides the guarantee that "at some point
143 /// during execution this was the path the fd pointed to" and
144 /// no more.
145 ///
146 /// NOTE: This method uses the [`ProcfsHandle`] to resolve the path. This
147 /// means that it is UNSAFE to use this method within any of our `procfs`
148 /// code!
149 fn as_unsafe_path(&self, procfs: &ProcfsHandle) -> Result<PathBuf, Error>;
150
151 /// Like [`FdExt::as_unsafe_path`], except that the lookup is done using the
152 /// basic host `/proc` mount. This is not safe against various races, and
153 /// thus MUST ONLY be used in codepaths that are not susceptible to those
154 /// kinds of attacks.
155 ///
156 /// Currently this should only be used by the `syscall::FrozenFd` logic
157 /// which saves the path a file descriptor references for error messages, as
158 /// well as in some test code.
159 fn as_unsafe_path_unchecked(&self) -> Result<PathBuf, Error>;
160
161 /// Check if the File is on a "dangerous" filesystem that might contain
162 /// magic-links.
163 fn is_magiclink_filesystem(&self) -> Result<bool, Error>;
164
165 /// Get information about the file descriptor from `fdinfo`.
166 ///
167 /// This parses the given `field` (**case-sensitive**) from
168 /// `/proc/thread-self/fdinfo/$fd` and returns a parsed version of the
169 /// value. If the field was not present in `fdinfo`, we return `Ok(None)`.
170 ///
171 /// Note that this method is not safe against an attacker that can modify
172 /// the mount table arbitrarily, though in practice it would be quite
173 /// difficult for an attacker to be able to consistently overmount every
174 /// `fdinfo` file for a process. This is mainly intended to be used within
175 /// [`fetch_mnt_id`] as a final fallback in the procfs resolver (hence no
176 /// [`ProcfsHandle`] argument) for pre-5.8 kernels.
177 fn get_fdinfo_field<T: FromStr>(
178 &self,
179 proc_rootfd: RawProcfsRoot<'_>,
180 want_field_name: &str,
181 ) -> Result<Option<T>, Error>
182 where
183 T::Err: Into<ErrorImpl> + Into<Error>;
184
185 // TODO: Add get_fdinfo which uses ProcfsHandle, for when we add
186 // RESOLVE_NO_XDEV support to Root::resolve.
187}
188
189/// Shorthand for reusing [`ProcfsBase::ProcThreadSelf`]'s compatibility checks
190/// to get a global-`/proc`-friendly subpath. Should only ever be used for
191/// `*_unchecked` functions -- [`ProcfsBase::ProcThreadSelf`] is the right thing
192/// to use in general.
193pub(in crate::utils) fn proc_threadself_subpath(
194 proc_rootfd: RawProcfsRoot<'_>,
195 subpath: &str,
196) -> PathBuf {
197 PathBuf::from(".")
198 .join(ProcfsBase::ProcThreadSelf.into_path(proc_rootfd))
199 .join(subpath.trim_start_matches('/'))
200}
201
202/// Get the right subpath in `/proc/self` for the given file descriptor
203/// (including those with "special" values, like `AT_FDCWD`).
204fn proc_subpath<Fd: AsRawFd>(fd: Fd) -> Result<String, Error> {
205 let fd = fd.as_raw_fd();
206 if fd == libc::AT_FDCWD {
207 Ok("cwd".to_string())
208 } else if fd.is_positive() {
209 Ok(format!("fd/{fd}"))
210 } else {
211 Err(ErrorImpl::InvalidArgument {
212 name: "fd".into(),
213 description: "must be positive or AT_FDCWD".into(),
214 })?
215 }
216}
217
218/// Set of filesystems' magic numbers that are considered "dangerous" (in that
219/// they can contain magic-links). This list should hopefully be exhaustive, but
220/// there's no real way of being sure since `nd_jump_link()` can be used by any
221/// non-mainline filesystem.
222///
223/// This list is correct from the [introduction of `nd_jump_link()` in Linux
224/// 3.6][kcommit-b5fb63c18315] up to Linux 6.11. Before Linux 3.6, the logic
225/// that became `nd_jump_link()` only existed in procfs. AppArmor [started using
226/// it in Linux 4.13 with the introduction of
227/// apparmorfs][kcommit-a481f4d91783].
228///
229/// [kcommit-b5fb63c18315]: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=b5fb63c18315c5510c1d0636179c057e0c761c77
230/// [kcommit-a481f4d91783]: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=a481f4d917835cad86701fc0d1e620c74bb5cd5f
231// TODO: Remove the explicit size once generic_arg_infer is stable.
232// <https://github.com/rust-lang/rust/issues/85077>
233const DANGEROUS_FILESYSTEMS: [rustix_fs::FsWord; 2] = [
234 rustix_fs::PROC_SUPER_MAGIC, // procfs
235 0x5a3c_69f0, // apparmorfs
236];
237
238impl<Fd: AsFd> FdExt for Fd {
239 fn metadata(&self) -> Result<Metadata, Error> {
240 let stat = syscalls::fstatat(self.as_fd(), "").map_err(|err| ErrorImpl::RawOsError {
241 operation: "get fd metadata".into(),
242 source: err,
243 })?;
244 Ok(Metadata(stat))
245 }
246
247 fn reopen(&self, procfs: &ProcfsHandle, mut flags: OpenFlags) -> Result<OwnedFd, Error> {
248 let fd = self.as_fd();
249
250 // For file descriptors referencing a symlink (i.e. opened with
251 // O_PATH|O_NOFOLLOW) there is no logic behind trying to do a "reopen"
252 // operation, and you just get confusing results because the reopen
253 // itself is done through a symlink. Even with O_EMPTYPATH you probably
254 // wouldn't ever want to re-open it (all you can get is another
255 // O_PATH|O_EMPTYPATH).
256 if self.metadata()?.is_symlink() {
257 Err(Error::from(ErrorImpl::OsError {
258 operation: "reopen".into(),
259 source: IOError::from_raw_os_error(libc::ELOOP),
260 }))
261 .wrap("symlink file handles cannot be reopened")?
262 }
263
264 // Now that we are sure the file descriptor is not a symlink, we can
265 // clear O_NOFOLLOW since it is a no-op (but due to the procfs reopening
266 // implementation, O_NOFOLLOW will cause strange behaviour).
267 flags.remove(OpenFlags::O_NOFOLLOW);
268
269 // TODO: Add support for O_EMPTYPATH once that exists...
270 procfs
271 .open_follow(ProcfsBase::ProcThreadSelf, proc_subpath(fd)?, flags)
272 .map(OwnedFd::from)
273 }
274
275 fn as_unsafe_path(&self, procfs: &ProcfsHandle) -> Result<PathBuf, Error> {
276 let fd = self.as_fd();
277 procfs.readlink(ProcfsBase::ProcThreadSelf, proc_subpath(fd)?)
278 }
279
280 fn as_unsafe_path_unchecked(&self) -> Result<PathBuf, Error> {
281 // "/proc/thread-self/fd/$n"
282 let fd_path = PathBuf::from("/proc").join(proc_threadself_subpath(
283 RawProcfsRoot::UnsafeGlobal,
284 &proc_subpath(self.as_fd())?,
285 ));
286
287 // Because this code is used within syscalls, we can't even check the
288 // filesystem type of /proc (unless we were to copy the logic here).
289 fs::read_link(&fd_path).map_err(|err| {
290 ErrorImpl::OsError {
291 operation: format!("readlink fd magic-link {fd_path:?}").into(),
292 source: err,
293 }
294 .into()
295 })
296 }
297
298 fn is_magiclink_filesystem(&self) -> Result<bool, Error> {
299 // There isn't a marker on a filesystem level to indicate whether
300 // nd_jump_link() is used internally. So, we just have to make an
301 // educated guess based on which mainline filesystems expose
302 // magic-links.
303 let stat = syscalls::fstatfs(self).map_err(|err| ErrorImpl::RawOsError {
304 operation: "check fstype of fd".into(),
305 source: err,
306 })?;
307 Ok(DANGEROUS_FILESYSTEMS.contains(&stat.f_type))
308 }
309
310 fn get_fdinfo_field<T: FromStr>(
311 &self,
312 proc_rootfd: RawProcfsRoot<'_>,
313 want_field_name: &str,
314 ) -> Result<Option<T>, Error>
315 where
316 T::Err: Into<ErrorImpl> + Into<Error>,
317 {
318 let fd = self.as_fd();
319 let fdinfo_path = match fd.as_raw_fd() {
320 // MSRV(1.66): Use ..=0 (half_open_range_patterns).
321 // MSRV(1.80): Use ..0 (exclusive_range_pattern).
322 fd @ libc::AT_FDCWD | fd @ RawFd::MIN..=0 => Err(ErrorImpl::OsError {
323 operation: format!("get relative procfs fdinfo path for fd {fd}").into(),
324 source: IOError::from_raw_os_error(libc::EBADF),
325 })?,
326 fd => proc_threadself_subpath(proc_rootfd, &format!("fdinfo/{fd}")),
327 };
328
329 let mut fdinfo_file: File = proc_rootfd
330 .open_beneath(fdinfo_path, OpenFlags::O_RDONLY)
331 .with_wrap(|| format!("open fd {} fdinfo", fd.as_raw_fd()))?
332 .into();
333
334 // As this is called from within fetch_mnt_id as a fallback, the only
335 // thing we can do here is verify that it is actually procfs. However,
336 // in practice it will be quite difficult for an attacker to over-mount
337 // every fdinfo file for a process.
338 procfs::verify_is_procfs(&fdinfo_file)?;
339
340 // Get the requested field -- this will also verify that the fdinfo
341 // contains an inode number that matches the original fd.
342 utils::fd_get_verify_fdinfo(&mut fdinfo_file, fd, want_field_name)
343 }
344}
345
346pub(crate) fn fetch_mnt_id(
347 proc_rootfd: RawProcfsRoot<'_>,
348 dirfd: impl AsFd,
349 path: impl AsRef<Path>,
350) -> Result<u64, Error> {
351 let dirfd = dirfd.as_fd();
352 let path = path.as_ref();
353
354 // The most ideal method of fetching mount IDs for a file descriptor (or
355 // subpath) is statx(2) with STATX_MNT_ID_UNIQUE, as it provides a globally
356 // unique 64-bit identifier for a mount that cannot be recycled without
357 // having to interact with procfs (which is important since this code is
358 // called within procfs, so we cannot use ProcfsHandle to protect against
359 // attacks).
360 //
361 // Unfortunately, STATX_MNT_ID_UNIQUE was added in Linux 6.8, so we need to
362 // have some fallbacks. STATX_MNT_ID is (for the most part) just as good for
363 // our usecase (since we operate relative to a file descriptor, the mount ID
364 // shouldn't be recycled while we keep the file descriptor open). This helps
365 // a fair bit, but STATX_MNT_ID was still only added in Linux 5.8, and so
366 // even some post-openat2(2) systems would be insecure if we just left it at
367 // that.
368 //
369 // As a fallback, we can use the "mnt_id" field from /proc/self/fdinfo/<fd>
370 // to get the mount ID -- unlike statx(2), this functionality has existed on
371 // Linux since time immemorial and thus we can error out if this operation
372 // fails. This does require us to operate on procfs in a less-safe way
373 // (unlike the alternative approaches), however note that:
374 //
375 // * For openat2(2) systems, this is completely safe (fdinfo files are regular
376 // files, and thus -- unlike magic-links -- RESOLVE_NO_XDEV can be used to
377 // safely protect against bind-mounts).
378 //
379 // * For non-openat2(2) systems, an attacker can theoretically attack this by
380 // overmounting fdinfo with something like /proc/self/environ and fill it
381 // with a fake fdinfo file.
382 //
383 // However, get_fdinfo_field and fd_get_verify_fdinfo have enough extra
384 // protections that would probably make it infeasible for an attacker to
385 // easily bypass it in practice. You can see the comments there for more
386 // details, but in short an attacker would probably need to be able to
387 // predict the file descriptor numbers for several transient files as
388 // well as the inode number of the target file, and be able to create
389 // overmounts while racing against libpathrs -- it seems unlikely that
390 // this would be trivial to do (especially compared to how trivial
391 // attacks are without these protections).
392 //
393 // NOTE: A very old trick for getting mount IDs in a race-free way was to
394 // (ab)use name_to_handle_at(2) -- if you request a file handle with
395 // too small a buffer, name_to_handle_at(2) will return -EOVERFLOW but
396 // will still give you the mount ID. Sadly, name_to_handle_at(2) did
397 // not work on procfs (or any other pseudofilesystem) until
398 // AT_HANDLE_FID supported was added in Linux 6.7 (at which point
399 // there's no real benefit to using it).
400 //
401 // Maybe we could use this for RESOLVE_NO_XDEV emulation in the
402 // EmulatedOpath resolver, but for procfs this approach is not useful.
403 //
404 // NOTE: Obvious alternatives like parsing /proc/self/mountinfo can be
405 // dismissed out-of-hand as not being useful (mountinfo is trivially
406 // bypassable by an attacker with mount privileges, is generally awful
407 // to parse, and doesn't work with open_tree(2)-style detached
408 // mounts).
409
410 const STATX_MNT_ID_UNIQUE: StatxFlags = StatxFlags::from_bits_retain(0x4000);
411 let want_mask = StatxFlags::MNT_ID | STATX_MNT_ID_UNIQUE;
412
413 let mnt_id = match syscalls::statx(dirfd, path, want_mask) {
414 Ok(stx) => {
415 let got_mask = StatxFlags::from_bits_retain(stx.stx_mask);
416 if got_mask.intersects(want_mask) {
417 Some(stx.stx_mnt_id)
418 } else {
419 None
420 }
421 }
422 Err(err) => match err.root_cause().raw_os_error() {
423 // We have to handle STATX_MNT_ID not being supported on pre-5.8
424 // kernels, so treat an ENOSYS or EINVAL the same so that we can
425 // work on pre-4.11 (pre-statx) kernels as well.
426 Some(libc::ENOSYS) | Some(libc::EINVAL) => None,
427 _ => Err(ErrorImpl::RawOsError {
428 operation: "check mnt_id of filesystem".into(),
429 source: err,
430 })?,
431 },
432 }
433 // Kind of silly intermediate Result<_, Error> type so that we can use
434 // Result::or_else.
435 // TODO: In principle we could remove this once result_flattening is
436 // stabilised...
437 .ok_or_else(|| {
438 ErrorImpl::NotSupported {
439 feature: "STATX_MNT_ID".into(),
440 }
441 .into()
442 })
443 .or_else(|_: Error| -> Result<_, Error> {
444 // openat doesn't support O_EMPTYPATH, so if we are operating on "" we
445 // should reuse the dirfd directly.
446 let file = if path.as_os_str().is_empty() {
447 MaybeOwnedFd::BorrowedFd(dirfd)
448 } else {
449 MaybeOwnedFd::OwnedFd(syscalls::openat(dirfd, path, OpenFlags::O_PATH, 0).map_err(
450 |err| ErrorImpl::RawOsError {
451 operation: "open target file for mnt_id check".into(),
452 source: err,
453 },
454 )?)
455 };
456 let file = file.as_fd();
457
458 match file
459 .get_fdinfo_field(proc_rootfd, "mnt_id")
460 .map_err(|err| (err.kind(), err))
461 {
462 Ok(Some(mnt_id)) => Ok(mnt_id),
463 // "mnt_id" *must* exist as a field -- make sure we return a
464 // SafetyViolation here if it is missing or an invalid value
465 // (InternalError), otherwise an attacker could silence this check
466 // by creating a "mnt_id"-less fdinfo.
467 // TODO: Should we actually match for ErrorImpl::ParseIntError here?
468 Ok(None) | Err((ErrorKind::InternalError, _)) => Err(ErrorImpl::SafetyViolation {
469 description: format!(
470 r#"fd {:?} has a fake fdinfo: invalid or missing "mnt_id" field"#,
471 file.as_raw_fd(),
472 )
473 .into(),
474 }
475 .into()),
476 // Pass through any other errors.
477 Err((_, err)) => Err(err),
478 }
479 })?;
480
481 Ok(mnt_id)
482}
483
484#[cfg(test)]
485mod tests {
486 use crate::{
487 flags::OpenFlags,
488 procfs::ProcfsHandle,
489 syscalls,
490 utils::{FdExt, RawProcfsRoot},
491 };
492
493 use std::{
494 fs::File,
495 os::unix::{fs::MetadataExt, io::AsFd},
496 path::Path,
497 };
498
499 use anyhow::{Context, Error};
500 use pretty_assertions::assert_eq;
501 use tempfile::TempDir;
502
503 fn check_as_unsafe_path(fd: impl AsFd, want_path: impl AsRef<Path>) -> Result<(), Error> {
504 let want_path = want_path.as_ref();
505
506 // Plain /proc/... lookup.
507 let got_path = fd.as_unsafe_path_unchecked()?;
508 assert_eq!(
509 got_path, want_path,
510 "expected as_unsafe_path_unchecked to give the correct path"
511 );
512 // ProcfsHandle-based lookup.
513 let got_path = fd.as_unsafe_path(&ProcfsHandle::new()?)?;
514 assert_eq!(
515 got_path, want_path,
516 "expected as_unsafe_path to give the correct path"
517 );
518 Ok(())
519 }
520
521 #[test]
522 fn as_unsafe_path_cwd() -> Result<(), Error> {
523 let real_cwd = syscalls::getcwd()?;
524 check_as_unsafe_path(syscalls::AT_FDCWD, real_cwd)
525 }
526
527 #[test]
528 fn as_unsafe_path_fd() -> Result<(), Error> {
529 let real_tmpdir = TempDir::new()?;
530 let file = File::open(&real_tmpdir)?;
531 check_as_unsafe_path(&file, real_tmpdir)
532 }
533
534 #[test]
535 fn as_unsafe_path_badfd() -> Result<(), Error> {
536 assert!(
537 syscalls::BADFD.as_unsafe_path_unchecked().is_err(),
538 "as_unsafe_path_unchecked should fail for bad file descriptor"
539 );
540 assert!(
541 syscalls::BADFD
542 .as_unsafe_path(&ProcfsHandle::new()?)
543 .is_err(),
544 "as_unsafe_path should fail for bad file descriptor"
545 );
546 Ok(())
547 }
548
549 #[test]
550 fn reopen_badfd() -> Result<(), Error> {
551 assert!(
552 syscalls::BADFD
553 .reopen(&ProcfsHandle::new()?, OpenFlags::O_PATH)
554 .is_err(),
555 "reopen should fail for bad file descriptor"
556 );
557 Ok(())
558 }
559
560 #[test]
561 fn is_magiclink_filesystem() {
562 assert!(
563 !File::open("/")
564 .expect("should be able to open handle to /")
565 .is_magiclink_filesystem()
566 .expect("is_magiclink_filesystem should work on regular file"),
567 "/ is not a magic-link filesystem"
568 );
569 }
570
571 #[test]
572 fn is_magiclink_filesystem_badfd() {
573 assert!(
574 syscalls::BADFD.is_magiclink_filesystem().is_err(),
575 "is_magiclink_filesystem should fail for bad file descriptor"
576 );
577 }
578
579 #[test]
580 fn metadata_badfd() {
581 assert!(
582 syscalls::BADFD.metadata().is_err(),
583 "metadata should fail for bad file descriptor"
584 );
585 }
586
587 #[test]
588 fn metadata() -> Result<(), Error> {
589 let file = File::open("/").context("open dummy file")?;
590
591 let file_meta = file.metadata().context("fstat file")?;
592 let fd_meta = file.as_fd().metadata().context("fstat fd")?;
593
594 assert_eq!(file_meta.dev(), fd_meta.dev(), "dev must match");
595 assert_eq!(file_meta.ino(), fd_meta.ino(), "ino must match");
596 assert_eq!(file_meta.mode(), fd_meta.mode(), "mode must match");
597 assert_eq!(file_meta.nlink(), fd_meta.nlink(), "nlink must match");
598 assert_eq!(file_meta.uid(), fd_meta.uid(), "uid must match");
599 assert_eq!(file_meta.gid(), fd_meta.gid(), "gid must match");
600 assert_eq!(file_meta.rdev(), fd_meta.rdev(), "rdev must match");
601 assert_eq!(file_meta.size(), fd_meta.size(), "size must match");
602 assert_eq!(file_meta.atime(), fd_meta.atime(), "atime must match");
603 assert_eq!(
604 file_meta.atime_nsec(),
605 fd_meta.atime_nsec(),
606 "atime_nsec must match"
607 );
608 assert_eq!(file_meta.mtime(), fd_meta.mtime(), "mtime must match");
609 assert_eq!(
610 file_meta.mtime_nsec(),
611 fd_meta.mtime_nsec(),
612 "mtime_nsec must match"
613 );
614 assert_eq!(file_meta.ctime(), fd_meta.ctime(), "ctime must match");
615 assert_eq!(
616 file_meta.ctime_nsec(),
617 fd_meta.ctime_nsec(),
618 "ctime_nsec must match"
619 );
620 assert_eq!(file_meta.blksize(), fd_meta.blksize(), "blksize must match");
621 assert_eq!(file_meta.blocks(), fd_meta.blocks(), "blocks must match");
622
623 Ok(())
624 }
625
626 // O_LARGEFILE has different values on different architectures.
627 #[cfg(any(target_arch = "arm", target_arch = "aarch64"))]
628 const DEFAULT_FDINFO_FLAGS: &str = "02400000";
629 #[cfg(any(target_arch = "powerpc", target_arch = "powerpc64"))]
630 const DEFAULT_FDINFO_FLAGS: &str = "02200000";
631 #[cfg(not(any(
632 target_arch = "arm",
633 target_arch = "aarch64",
634 target_arch = "powerpc",
635 target_arch = "powerpc64"
636 )))]
637 const DEFAULT_FDINFO_FLAGS: &str = "02100000";
638
639 #[test]
640 fn get_fdinfo_field() -> Result<(), Error> {
641 let file = File::open("/").context("open dummy file")?;
642
643 assert_eq!(
644 file.get_fdinfo_field::<u64>(RawProcfsRoot::UnsafeGlobal, "pos")?,
645 Some(0),
646 "pos should be parsed and zero for new file"
647 );
648
649 assert_eq!(
650 file.get_fdinfo_field::<String>(RawProcfsRoot::UnsafeGlobal, "flags")?,
651 Some(DEFAULT_FDINFO_FLAGS.to_string()),
652 "flags should be parsed for new file"
653 );
654
655 assert_ne!(
656 file.get_fdinfo_field::<u64>(RawProcfsRoot::UnsafeGlobal, "mnt_id")?
657 .expect("should find mnt_id in fdinfo"),
658 0,
659 "mnt_id should be parsed and non-nil for any real file"
660 );
661
662 assert_eq!(
663 file.get_fdinfo_field::<u64>(RawProcfsRoot::UnsafeGlobal, "non_exist")?,
664 None,
665 "non_exist should not be present in fdinfo"
666 );
667
668 Ok(())
669 }
670
671 #[test]
672 fn get_fdinfo_field_proc_rootfd() -> Result<(), Error> {
673 let procfs = ProcfsHandle::new().context("open procfs handle")?;
674 let file = File::open("/").context("open dummy file")?;
675
676 assert_eq!(
677 file.get_fdinfo_field::<u64>(procfs.as_raw_procfs(), "pos")?,
678 Some(0),
679 "pos should be parsed and zero for new file"
680 );
681
682 assert_eq!(
683 file.get_fdinfo_field::<String>(procfs.as_raw_procfs(), "flags")?,
684 Some(DEFAULT_FDINFO_FLAGS.to_string()),
685 "flags should be parsed for new file"
686 );
687
688 assert_ne!(
689 file.get_fdinfo_field::<u64>(procfs.as_raw_procfs(), "mnt_id")?
690 .expect("should find mnt_id in fdinfo"),
691 0,
692 "mnt_id should be parsed and non-nil for any real file"
693 );
694
695 assert_eq!(
696 file.get_fdinfo_field::<u64>(procfs.as_raw_procfs(), "non_exist")?,
697 None,
698 "non_exist should not be present in fdinfo"
699 );
700
701 Ok(())
702 }
703}