From 5e4acde0f2664478368a3ceb541fb3048e6e8839 Mon Sep 17 00:00:00 2001 From: Thomas Bertschinger Date: Tue, 9 Sep 2025 09:53:16 -0600 Subject: [PATCH 1/4] fs: introduce AT_ filehandle constants for name_to_handle_at(2) --- src/backend/libc/fs/types.rs | 28 ++++++++++++++++++++++++++++ src/backend/linux_raw/conv.rs | 7 +++++++ src/backend/linux_raw/fs/types.rs | 28 ++++++++++++++++++++++++++++ 3 files changed, 63 insertions(+) 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/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 From bdb8a1b34e315cd9d8134dc90dbd77d4af0e6b91 Mon Sep 17 00:00:00 2001 From: Thomas Bertschinger Date: Tue, 9 Sep 2025 09:56:22 -0600 Subject: [PATCH 2/4] fs: introduce name_to_handle_at() --- src/backend/libc/fs/syscalls.rs | 31 +++++ src/backend/linux_raw/fs/syscalls.rs | 24 +++- src/fs/filehandle.rs | 163 +++++++++++++++++++++++++++ src/fs/mod.rs | 4 + 4 files changed, 220 insertions(+), 2 deletions(-) create mode 100644 src/fs/filehandle.rs diff --git a/src/backend/libc/fs/syscalls.rs b/src/backend/libc/fs/syscalls.rs index 8cbd3a2c1..871a38adb 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,35 @@ 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 sendfile( out_fd: BorrowedFd<'_>, diff --git a/src/backend/linux_raw/fs/syscalls.rs b/src/backend/linux_raw/fs/syscalls.rs index 872dd8e35..9e3d7f2ea 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,26 @@ 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 + )) + } +} + // 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/fs/filehandle.rs b/src/fs/filehandle.rs new file mode 100644 index 000000000..08cb3b88b --- /dev/null +++ b/src/fs/filehandle.rs @@ -0,0 +1,163 @@ +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 + } + + /// 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 _ + } +} + +/// 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) + }; + + return ret.map(|_| (file_handle, mount_id)); + }) +} + +#[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"))] From 35587dbcfddaba48f06650c90b6960dba9b8b44c Mon Sep 17 00:00:00 2001 From: Thomas Bertschinger Date: Tue, 9 Sep 2025 09:56:51 -0600 Subject: [PATCH 3/4] fs: introduce open_by_handle_at() --- src/backend/libc/fs/syscalls.rs | 23 +++++++++++++++++++++++ src/backend/linux_raw/fs/syscalls.rs | 9 +++++++++ src/fs/filehandle.rs | 18 ++++++++++++++++++ 3 files changed, 50 insertions(+) diff --git a/src/backend/libc/fs/syscalls.rs b/src/backend/libc/fs/syscalls.rs index 871a38adb..25d7accdd 100644 --- a/src/backend/libc/fs/syscalls.rs +++ b/src/backend/libc/fs/syscalls.rs @@ -1911,6 +1911,29 @@ pub(crate) fn name_to_handle_at( } } +#[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/linux_raw/fs/syscalls.rs b/src/backend/linux_raw/fs/syscalls.rs index 9e3d7f2ea..a29ba15df 100644 --- a/src/backend/linux_raw/fs/syscalls.rs +++ b/src/backend/linux_raw/fs/syscalls.rs @@ -1677,6 +1677,15 @@ pub(crate) fn name_to_handle_at( } } +#[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/fs/filehandle.rs b/src/fs/filehandle.rs index 08cb3b88b..69140c8e7 100644 --- a/src/fs/filehandle.rs +++ b/src/fs/filehandle.rs @@ -73,6 +73,10 @@ impl FileHandle { 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`]. @@ -143,6 +147,20 @@ pub fn name_to_handle_at( }) } +/// `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::*; From 60ea286f1e5851a339df0b1bb3ace74eba5292c4 Mon Sep 17 00:00:00 2001 From: Thomas Bertschinger Date: Wed, 10 Sep 2025 08:31:04 -0600 Subject: [PATCH 4/4] filehandle: trim the filehandle slice before returning it to user This way, once the user gets it, the filehandle slice is only as large as it needs to be. Then if the user copies it, excess unused bytes are not copied. --- src/fs/filehandle.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/fs/filehandle.rs b/src/fs/filehandle.rs index 69140c8e7..e84d99460 100644 --- a/src/fs/filehandle.rs +++ b/src/fs/filehandle.rs @@ -56,6 +56,16 @@ impl FileHandle { 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()); @@ -143,6 +153,9 @@ pub fn name_to_handle_at( 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)); }) }