Skip to main content

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}