Skip to content

Commit

Permalink
Add support for openat2
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.
  • Loading branch information
blinsay committed Mar 23, 2024
1 parent c6a7d40 commit d394b5d
Show file tree
Hide file tree
Showing 2 changed files with 178 additions and 0 deletions.
119 changes: 119 additions & 0 deletions src/fcntl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,125 @@ pub fn openat<P: ?Sized + NixPath>(
Errno::result(fd)
}

#[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>
#[cfg(target_os = "linux")]
#[repr(transparent)]
#[derive(Clone, Copy, Debug)]
pub struct OpenHow(libc::open_how);

#[cfg(target_os = "linux")]
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
#[cfg(target_os = "linux")]
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)
#[cfg(target_os = "linux")]
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>(),
)
})?;

// convert the result of syscall into the right size for an fd in a sort of
// portable way. if the conversion fails somehow, EBADF is reasonable - the
// return value of syscall can't be converted into an fd.
let fd: libc::c_int = fd.try_into().map_err(|_| Errno::EBADF)?;
Errno::result(fd)
}

/// Change the name of a file.
///
/// The `renameat` function is equivalent to `rename` except in the case where either `old_path`
Expand Down
59 changes: 59 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,61 @@ 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")]
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 d394b5d

Please sign in to comment.