From 4ab23c327889ffb4f57dfec883a9c3c596953949 Mon Sep 17 00:00:00 2001 From: Ben Linsay Date: Mon, 25 Mar 2024 19:14:05 -0400 Subject: [PATCH] Add support for openat2 (#2339) Adds an openat2 function with an OpenHow and ResolveFlag options for configuring path resolution. Fixes #1400. --- changelog/2339.added.md | 1 + src/fcntl.rs | 113 ++++++++++++++++++++++++++++++++++++++++ test/test_fcntl.rs | 62 ++++++++++++++++++++++ 3 files changed, 176 insertions(+) create mode 100644 changelog/2339.added.md diff --git a/changelog/2339.added.md b/changelog/2339.added.md new file mode 100644 index 0000000000..0f80767d84 --- /dev/null +++ b/changelog/2339.added.md @@ -0,0 +1 @@ +Added support for openat2 on linux. diff --git a/src/fcntl.rs b/src/fcntl.rs index ccefe955de..16af6e6d86 100644 --- a/src/fcntl.rs +++ b/src/fcntl.rs @@ -242,6 +242,119 @@ pub fn openat( Errno::result(fd) } +cfg_if::cfg_if! { + if #[cfg(target_os = "linux")] { + libc_bitflags! { + /// Path resolution flags. + /// + /// See [path resolution(7)](https://man7.org/linux/man-pages/man7/path_resolution.7.html) + /// for details of the resolution process. + pub struct ResolveFlag: libc::c_ulonglong { + /// Do not permit the path resolution to succeed if any component of + /// the resolution is not a descendant of the directory indicated by + /// dirfd. This causes absolute symbolic links (and absolute values of + /// pathname) to be rejected. + RESOLVE_BENEATH; + + /// Treat the directory referred to by dirfd as the root directory + /// while resolving pathname. + RESOLVE_IN_ROOT; + + /// Disallow all magic-link resolution during path resolution. Magic + /// links are symbolic link-like objects that are most notably found + /// in proc(5); examples include `/proc/[pid]/exe` and `/proc/[pid]/fd/*`. + /// + /// See symlink(7) for more details. + RESOLVE_NO_MAGICLINKS; + + /// Disallow resolution of symbolic links during path resolution. This + /// option implies RESOLVE_NO_MAGICLINKS. + RESOLVE_NO_SYMLINKS; + + /// Disallow traversal of mount points during path resolution (including + /// all bind mounts). + RESOLVE_NO_XDEV; + } + } + + /// Specifies how [openat2] should open a pathname. + /// + /// See + #[repr(transparent)] + #[derive(Clone, Copy, Debug)] + pub struct OpenHow(libc::open_how); + + impl OpenHow { + /// Create a new zero-filled `open_how`. + pub fn new() -> Self { + // safety: according to the man page, open_how MUST be zero-initialized + // on init so that unknown fields are also zeroed. + Self(unsafe { + std::mem::MaybeUninit::zeroed().assume_init() + }) + } + + /// Set the open flags used to open a file, completely overwriting any + /// existing flags. + pub fn flags(mut self, flags: OFlag) -> Self { + let flags = flags.bits() as libc::c_ulonglong; + self.0.flags = flags; + self + } + + /// Set the file mode new files will be created with, overwriting any + /// existing flags. + pub fn mode(mut self, mode: Mode) -> Self { + let mode = mode.bits() as libc::c_ulonglong; + self.0.mode = mode; + self + } + + /// Set resolve flags, completely overwriting any existing flags. + /// + /// See [ResolveFlag] for more detail. + pub fn resolve(mut self, resolve: ResolveFlag) -> Self { + let resolve = resolve.bits(); + self.0.resolve = resolve; + self + } + } + + // safety: default isn't derivable because libc::open_how must be zeroed + impl Default for OpenHow { + fn default() -> Self { + Self::new() + } + } + + /// Open or create a file for reading, writing or executing. + /// + /// `openat2` is an extension of the [`openat`] function that allows the caller + /// to control how path resolution happens. + /// + /// # See also + /// + /// [openat2](https://man7.org/linux/man-pages/man2/openat2.2.html) + pub fn openat2( + dirfd: RawFd, + path: &P, + mut how: OpenHow, + ) -> Result { + let fd = path.with_nix_path(|cstr| unsafe { + libc::syscall( + libc::SYS_openat2, + dirfd, + cstr.as_ptr(), + &mut how as *mut OpenHow, + std::mem::size_of::(), + ) + })?; + + Errno::result(fd as RawFd) + } + } +} + /// Change the name of a file. /// /// The `renameat` function is equivalent to `rename` except in the case where either `old_path` diff --git a/test/test_fcntl.rs b/test/test_fcntl.rs index 6572e8af8d..e4ace020f7 100644 --- a/test/test_fcntl.rs +++ b/test/test_fcntl.rs @@ -4,6 +4,10 @@ use nix::errno::*; use nix::fcntl::{open, readlink, OFlag}; #[cfg(not(target_os = "redox"))] use nix::fcntl::{openat, readlinkat, renameat}; + +#[cfg(target_os = "linux")] +use nix::fcntl::{openat2, OpenHow, ResolveFlag}; + #[cfg(all( target_os = "linux", target_env = "gnu", @@ -57,6 +61,64 @@ fn test_openat() { close(dirfd).unwrap(); } +#[test] +#[cfg(target_os = "linux")] +// QEMU does not handle openat well enough to satisfy this test +// https://gitlab.com/qemu-project/qemu/-/issues/829 +#[cfg_attr(qemu, ignore)] +fn test_openat2() { + const CONTENTS: &[u8] = b"abcd"; + let mut tmp = NamedTempFile::new().unwrap(); + tmp.write_all(CONTENTS).unwrap(); + + let dirfd = + open(tmp.path().parent().unwrap(), OFlag::empty(), Mode::empty()) + .unwrap(); + + let fd = openat2( + dirfd, + tmp.path().file_name().unwrap(), + OpenHow::new() + .flags(OFlag::O_RDONLY) + .mode(Mode::empty()) + .resolve(ResolveFlag::RESOLVE_BENEATH), + ) + .unwrap(); + + let mut buf = [0u8; 1024]; + assert_eq!(4, read(fd, &mut buf).unwrap()); + assert_eq!(CONTENTS, &buf[0..4]); + + close(fd).unwrap(); + close(dirfd).unwrap(); +} + +#[test] +#[cfg(target_os = "linux")] +// QEMU does not handle openat well enough to satisfy this test +// https://gitlab.com/qemu-project/qemu/-/issues/829 +#[cfg_attr(qemu, ignore)] +fn test_openat2_forbidden() { + let mut tmp = NamedTempFile::new().unwrap(); + tmp.write_all(b"let me out").unwrap(); + + let dirfd = + open(tmp.path().parent().unwrap(), OFlag::empty(), Mode::empty()) + .unwrap(); + + let escape_attempt = + tmp.path().parent().unwrap().join("../../../hello.txt"); + + let res = openat2( + dirfd, + &escape_attempt, + OpenHow::new() + .flags(OFlag::O_RDONLY) + .resolve(ResolveFlag::RESOLVE_BENEATH), + ); + assert_eq!(Err(Errno::EXDEV), res); +} + #[test] #[cfg(not(target_os = "redox"))] fn test_renameat() {