diff --git a/src/backend/libc/fs/syscalls.rs b/src/backend/libc/fs/syscalls.rs index 8cbd3a2c1..25d7accdd 100644 --- a/src/backend/libc/fs/syscalls.rs +++ b/src/backend/libc/fs/syscalls.rs @@ -31,6 +31,8 @@ use crate::fs::FallocateFlags; target_os = "wasi" )))] use crate::fs::FlockOperation; +#[cfg(target_os = "linux")] +use crate::fs::HandleFlags; #[cfg(any(linux_kernel, target_os = "freebsd"))] use crate::fs::MemfdFlags; #[cfg(any(linux_kernel, apple))] @@ -1880,6 +1882,58 @@ const SYS_OPENAT2: i32 = 437; #[cfg(all(linux_kernel, target_pointer_width = "64"))] const SYS_OPENAT2: i64 = 437; +#[cfg(target_os = "linux")] +pub(crate) fn name_to_handle_at( + dirfd: BorrowedFd<'_>, + path: &CStr, + handle: *mut ffi::c_void, + mount_id: *mut ffi::c_void, + flags: HandleFlags, +) -> io::Result<()> { + syscall! { + fn name_to_handle_at( + dir_fd: c::c_int, + path: *const ffi::c_char, + handle: *mut ffi::c_void, + mount_id: *mut ffi::c_void, + flags: u32 + ) via SYS_name_to_handle_at -> c::c_int + } + + unsafe { + ret(name_to_handle_at( + borrowed_fd(dirfd), + c_str(path), + handle, + mount_id, + flags.bits(), + )) + } +} + +#[cfg(target_os = "linux")] +pub(crate) fn open_by_handle_at( + mount_fd: BorrowedFd<'_>, + handle: *const core::ffi::c_void, + flags: OFlags, +) -> io::Result { + syscall! { + fn open_by_handle_at( + mount_fd: c::c_int, + handle: *const ffi::c_void, + flags: u32 + ) via SYS_open_by_handle_at -> c::c_int + } + + unsafe { + ret_owned_fd(open_by_handle_at( + borrowed_fd(mount_fd), + handle, + flags.bits(), + )) + } +} + #[cfg(target_os = "linux")] pub(crate) fn sendfile( out_fd: BorrowedFd<'_>, diff --git a/src/backend/libc/fs/types.rs b/src/backend/libc/fs/types.rs index d570c5b28..3ae98be96 100644 --- a/src/backend/libc/fs/types.rs +++ b/src/backend/libc/fs/types.rs @@ -527,6 +527,34 @@ bitflags! { } } +#[cfg(target_os = "linux")] +bitflags! { + /// `AT_*` constants for use with [`name_to_handle_at`] + /// + /// [`name_to_handle_at`]: crate::fs::name_to_handle_at + #[repr(transparent)] + #[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)] + pub struct HandleFlags: u32 { + /// `AT_HANDLE_FID` + const FID = linux_raw_sys::general::AT_HANDLE_FID; + + /// `AT_HANDLE_MNT_ID_UNIQUE` + const MNT_ID_UNIQUE = linux_raw_sys::general::AT_HANDLE_MNT_ID_UNIQUE; + + /// `AT_HANDLE_CONNECTABLE` + const CONNECTABLE = linux_raw_sys::general::AT_HANDLE_CONNECTABLE; + + /// `AT_SYMLINK_FOLLOW` + const SYMLINK_FOLLOW = linux_raw_sys::general::AT_SYMLINK_FOLLOW; + + /// `AT_EMPTY_PATH` + const EMPTY_PATH = linux_raw_sys::general::AT_EMPTY_PATH; + + /// + const _ = !0; + } +} + /// `S_IF*` constants for use with [`mknodat`] and [`Stat`]'s `st_mode` field. /// /// [`mknodat`]: crate::fs::mknodat diff --git a/src/backend/linux_raw/conv.rs b/src/backend/linux_raw/conv.rs index 3d4693fe1..45a943e8b 100644 --- a/src/backend/linux_raw/conv.rs +++ b/src/backend/linux_raw/conv.rs @@ -333,6 +333,13 @@ pub(crate) mod fs { } } + impl<'a, Num: ArgNumber> From for ArgReg<'a, Num> { + #[inline] + fn from(flags: crate::fs::HandleFlags) -> Self { + c_uint(flags.bits()) + } + } + impl<'a, Num: ArgNumber> From for ArgReg<'a, Num> { #[inline] fn from(flags: crate::fs::XattrFlags) -> Self { diff --git a/src/backend/linux_raw/fs/syscalls.rs b/src/backend/linux_raw/fs/syscalls.rs index 872dd8e35..a29ba15df 100644 --- a/src/backend/linux_raw/fs/syscalls.rs +++ b/src/backend/linux_raw/fs/syscalls.rs @@ -28,8 +28,8 @@ use crate::ffi::CStr; use crate::fs::CWD; use crate::fs::{ inotify, Access, Advice, AtFlags, FallocateFlags, FileType, FlockOperation, Fsid, Gid, - MemfdFlags, Mode, OFlags, RenameFlags, ResolveFlags, SealFlags, SeekFrom, Stat, StatFs, - StatVfs, StatVfsMountFlags, Statx, StatxFlags, Timestamps, Uid, XattrFlags, + HandleFlags, MemfdFlags, Mode, OFlags, RenameFlags, ResolveFlags, SealFlags, SeekFrom, Stat, + StatFs, StatVfs, StatVfsMountFlags, Statx, StatxFlags, Timestamps, Uid, XattrFlags, }; use crate::io; use core::mem::MaybeUninit; @@ -1657,6 +1657,35 @@ pub(crate) fn fremovexattr(fd: BorrowedFd<'_>, name: &CStr) -> io::Result<()> { unsafe { ret(syscall_readonly!(__NR_fremovexattr, fd, name)) } } +#[inline] +pub(crate) fn name_to_handle_at( + dirfd: BorrowedFd<'_>, + path: &CStr, + file_handle: *mut core::ffi::c_void, + mount_id: *mut core::ffi::c_void, + flags: HandleFlags, +) -> io::Result<()> { + unsafe { + ret(syscall!( + __NR_name_to_handle_at, + dirfd, + path, + file_handle, + mount_id, + flags + )) + } +} + +#[inline] +pub(crate) fn open_by_handle_at( + mount_fd: BorrowedFd<'_>, + handle: *const core::ffi::c_void, + flags: OFlags, +) -> io::Result { + unsafe { ret_owned_fd(syscall!(__NR_open_by_handle_at, mount_fd, handle, flags)) } +} + // Some linux_raw_sys structs have unsigned types for values which are // interpreted as signed. This defines a utility or casting to the // same-sized signed type. diff --git a/src/backend/linux_raw/fs/types.rs b/src/backend/linux_raw/fs/types.rs index fc19a3f47..dcb3a3158 100644 --- a/src/backend/linux_raw/fs/types.rs +++ b/src/backend/linux_raw/fs/types.rs @@ -320,6 +320,34 @@ bitflags! { } } +#[cfg(target_os = "linux")] +bitflags! { + /// `AT_*` constants for use with [`name_to_handle_at`] + /// + /// [`name_to_handle_at`]: crate::fs::name_to_handle_at + #[repr(transparent)] + #[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)] + pub struct HandleFlags: ffi::c_uint { + /// `AT_HANDLE_FID` + const FID = linux_raw_sys::general::AT_HANDLE_FID; + + /// `AT_HANDLE_MNT_ID_UNIQUE` + const MNT_ID_UNIQUE = linux_raw_sys::general::AT_HANDLE_MNT_ID_UNIQUE; + + /// `AT_HANDLE_CONNECTABLE` + const CONNECTABLE = linux_raw_sys::general::AT_HANDLE_CONNECTABLE; + + /// `AT_SYMLINK_FOLLOW` + const SYMLINK_FOLLOW = linux_raw_sys::general::AT_SYMLINK_FOLLOW; + + /// `AT_EMPTY_PATH` + const EMPTY_PATH = linux_raw_sys::general::AT_EMPTY_PATH; + + /// + const _ = !0; + } +} + /// `S_IF*` constants for use with [`mknodat`] and [`Stat`]'s `st_mode` field. /// /// [`mknodat`]: crate::fs::mknodat diff --git a/src/fs/filehandle.rs b/src/fs/filehandle.rs new file mode 100644 index 000000000..e84d99460 --- /dev/null +++ b/src/fs/filehandle.rs @@ -0,0 +1,194 @@ +use core::mem::size_of; + +use crate::{backend, ffi, io, path}; +use backend::fd::{AsFd, OwnedFd}; +use backend::fs::types::{HandleFlags, OFlags}; + +/// This maximum is more of a "guideline"; the man page for name_to_handle_at(2) indicates it could +/// increase in the future. +const MAX_HANDLE_SIZE: usize = 128; + +/// The minimum size of a `struct file_handle` is the size of an int and an unsigned int, for the +/// length and type fields. +const HANDLE_STRUCT_SIZE: usize = size_of::() + size_of::(); + +/// An opaque identifier for a file. +/// +/// While the C struct definition in `fcntl.h` exposes fields like length and type, in reality, +/// user applications cannot usefully interpret (or modify) the separate fields of a file handle, so +/// this implementation treats the file handle as an entirely opaque sequence of bytes. +#[derive(Debug)] +pub struct FileHandle { + raw: Box<[u8]>, +} + +impl FileHandle { + fn new(size: usize) -> Self { + let handle_allocation_size: usize = HANDLE_STRUCT_SIZE + size; + let bytes = vec![0; handle_allocation_size]; + + let mut handle = Self { + raw: Box::from(bytes), + }; + handle.set_handle_bytes(size); + + handle + } + + /// Create a file handle from a sequence of bytes. + /// + /// # Panics + /// + /// Panics if the given handle is malformed, suggesting that it did not originate from a + /// previous call to name_to_handle_at(). + pub fn from_raw(raw: Box<[u8]>) -> Self { + assert!(raw.len() >= HANDLE_STRUCT_SIZE); + + let handle = Self { raw }; + + assert!(handle.raw.len() >= handle.get_handle_bytes() + HANDLE_STRUCT_SIZE); + + handle + } + + /// Get the raw bytes of a file handle. + pub fn into_raw(self) -> Box<[u8]> { + self.raw + } + + /// We allocate the "maximum" size for a file handle straight away in order to avoid needing + /// multiple syscalls / reallocations whenever possible. However, that leaves raw.len() + /// excessively high when the filehandle will usually be much smaller than MAX_HANDLE_SIZE. + /// This function "trims" the filehandle so that the slice is only as large as it needs to be. + fn trim(&mut self) { + let len = self.get_handle_bytes() + HANDLE_STRUCT_SIZE; + + self.raw = Box::from(&self.raw[0..len]); + } + + /// Set the `handle_bytes` field (first 4 bytes of the struct) to the given length. + fn set_handle_bytes(&mut self, size: usize) { + self.raw[0..size_of::()].copy_from_slice(&(size as ffi::c_uint).to_ne_bytes()); + } + + /// Get the length of the file handle data by reading the `handle_bytes` field + fn get_handle_bytes(&self) -> usize { + ffi::c_uint::from_ne_bytes( + self.raw[0..size_of::()] + .try_into() + .expect("Vector should be long enough"), + ) as usize + } + + fn as_mut_ptr(&mut self) -> *mut ffi::c_void { + self.raw.as_mut_ptr() as *mut _ + } + + fn as_ptr(&self) -> *const ffi::c_void { + self.raw.as_ptr() as *const _ + } +} + +/// An identifier for a mount that is returned by [`name_to_handle_at`]. +/// +/// [`name_to_handle_at`]: crate::fs::name_to_handle_at +#[derive(Debug)] +pub enum MountId { + /// By default a MountId is a C int. + Regular(ffi::c_int), + /// When `AT_HANDLE_MNT_ID_UNIQUE` is passed in `HandleFlags`, MountId is a u64. + Unique(u64), +} + +/// `name_to_handle_at(dirfd, path, flags)` - Gets a filehandle given a path. +/// +/// # References +/// - [Linux] +/// +/// [Linux]: https://man7.org/linux/man-pages/man2/open_by_handle_at.2.html +pub fn name_to_handle_at( + dirfd: Fd, + path: P, + flags: HandleFlags, +) -> io::Result<(FileHandle, MountId)> { + // name_to_handle_at(2) takes the mount_id parameter as either a 32-bit or 64-bit int pointer + // depending on the flag AT_HANDLE_MNT_ID_UNIQUE + let mount_id_unique: bool = flags.contains(HandleFlags::MNT_ID_UNIQUE); + let mut mount_id_int: ffi::c_int = 0; + let mut mount_id_64: u64 = 0; + let mount_id_ptr: *mut ffi::c_void = if mount_id_unique { + &mut mount_id_64 as *mut u64 as *mut _ + } else { + &mut mount_id_int as *mut ffi::c_int as *mut _ + }; + + // The MAX_HANDLE_SZ constant is not a fixed upper bound, because the kernel is permitted to + // increase it in the future. So, the loop is needed in the rare case that MAX_HANDLE_SZ was + // insufficient. + let mut handle_size: usize = MAX_HANDLE_SIZE; + path.into_with_c_str(|path| loop { + let mut file_handle = FileHandle::new(handle_size); + + let ret = backend::fs::syscalls::name_to_handle_at( + dirfd.as_fd(), + path, + file_handle.as_mut_ptr(), + mount_id_ptr, + flags, + ); + + // If EOVERFLOW was returned, and the handle size was increased, we need to try again with + // a larger handle. If the handle size was not increased, EOVERFLOW was due to some other + // cause, and should be returned to the user. + if let Err(e) = ret { + if e == io::Errno::OVERFLOW && file_handle.get_handle_bytes() > handle_size { + handle_size = file_handle.get_handle_bytes(); + continue; + } + } + + let mount_id = if mount_id_unique { + MountId::Unique(mount_id_64) + } else { + MountId::Regular(mount_id_int) + }; + + // Ensure the slice is only as large as it needs to be before returning it to the user. + file_handle.trim(); + + return ret.map(|_| (file_handle, mount_id)); + }) +} + +/// `open_by_handle_at(mount_fd, handle, flags)` - Open a file by filehandle. +/// +/// # References +/// - [Linux] +/// +/// [Linux]: https://man7.org/linux/man-pages/man2/open_by_handle_at.2.html +pub fn open_by_handle_at( + mount_fd: Fd, + handle: &FileHandle, + flags: OFlags, +) -> io::Result { + backend::fs::syscalls::open_by_handle_at(mount_fd.as_fd(), handle.as_ptr(), flags) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_name_to_handle() { + let (_, mount_id) = + name_to_handle_at(crate::fs::CWD, "Cargo.toml", HandleFlags::empty()).unwrap(); + assert!(matches!(mount_id, MountId::Regular(_))); + + match name_to_handle_at(crate::fs::CWD, "Cargo.toml", HandleFlags::MNT_ID_UNIQUE) { + // On a new enough kernel, AT_HANDLE_MNT_ID_UNIQUE should succeed: + Ok((_, mount_id)) => assert!(matches!(mount_id, MountId::Unique(_))), + // But it should be rejected with -EINVAL on an older kernel: + Err(e) => assert!(e == io::Errno::INVAL), + } + } +} diff --git a/src/fs/mod.rs b/src/fs/mod.rs index 1807ada6a..895b5fb04 100644 --- a/src/fs/mod.rs +++ b/src/fs/mod.rs @@ -26,6 +26,8 @@ mod fcntl_apple; #[cfg(apple)] mod fcopyfile; pub(crate) mod fd; +#[cfg(target_os = "linux")] +mod filehandle; #[cfg(all(apple, feature = "alloc"))] mod getpath; #[cfg(not(target_os = "wasi"))] // WASI doesn't have get[gpu]id. @@ -93,6 +95,8 @@ pub use fcntl_apple::*; #[cfg(apple)] pub use fcopyfile::*; pub use fd::*; +#[cfg(target_os = "linux")] +pub use filehandle::*; #[cfg(all(apple, feature = "alloc"))] pub use getpath::getpath; #[cfg(not(target_os = "wasi"))]