Skip to content

Commit

Permalink
Add support for openat2 (#2339)
Browse files Browse the repository at this point in the history
Adds an openat2 function with an OpenHow and ResolveFlag options for
configuring path resolution.

Fixes #1400.
  • Loading branch information
blinsay authored Mar 25, 2024
1 parent 5b1b695 commit 4ab23c3
Show file tree
Hide file tree
Showing 3 changed files with 176 additions and 0 deletions.
1 change: 1 addition & 0 deletions changelog/2339.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added support for openat2 on linux.
113 changes: 113 additions & 0 deletions src/fcntl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,119 @@ pub fn openat<P: ?Sized + NixPath>(
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 <https://man7.org/linux/man-pages/man2/open_how.2type.html>
#[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<P: ?Sized + NixPath>(
dirfd: RawFd,
path: &P,
mut how: OpenHow,
) -> Result<RawFd> {
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::<libc::open_how>(),
)
})?;

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`
Expand Down
62 changes: 62 additions & 0 deletions test/test_fcntl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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() {
Expand Down

0 comments on commit 4ab23c3

Please sign in to comment.