diff --git a/Cargo.lock b/Cargo.lock index c78ff18c8..22ed69117 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1457,6 +1457,7 @@ name = "fspy_shm" version = "0.0.0" dependencies = [ "ctor", + "fspy_nostd", "memmap2", "subprocess_test", "uuid", diff --git a/crates/fspy_nostd/src/fs/linux.rs b/crates/fspy_nostd/src/fs/linux.rs index d905c9495..fb94da7ba 100644 --- a/crates/fspy_nostd/src/fs/linux.rs +++ b/crates/fspy_nostd/src/fs/linux.rs @@ -1,10 +1,45 @@ use core::{mem::MaybeUninit, slice}; -use crate::{AsRawFd as _, BorrowedFd, CStr, Error, Fat, Result, Thin}; +use rustix::fd::FromRawFd as _; + +use crate::{ + AsRawFd as _, BorrowedFd, CStr, Error, Fat, OwnedFd, Result, Thin, + fs::{Mode, OFlags}, +}; // Linux UAPI `PATH_MAX`. pub(super) const PATH_MAX: usize = 4096; +fn syscall_fd(fd: BorrowedFd<'_>) -> Result { + let fd = isize::try_from(fd.as_raw_fd()).map_err(|_| Error::OVERFLOW)?; + Ok(fd.cast_unsigned()) +} + +#[expect(clippy::needless_pass_by_value, reason = "CStr is a borrowed value type")] +pub(super) fn openat( + dirfd: BorrowedFd<'_>, + path: CStr<'_, R>, + flags: OFlags, + mode: Mode, +) -> Result { + // SAFETY: `dirfd` remains borrowed and `path` is NUL-terminated. The + // kernel receives all four syscall arguments explicitly. + let fd = unsafe { + syscalls::syscall4( + syscalls::Sysno::openat, + syscall_fd(dirfd)?, + path.as_ptr().addr(), + usize::try_from(flags.bits()).map_err(|_| Error::INVAL)?, + usize::try_from(mode.bits()).map_err(|_| Error::INVAL)?, + ) + } + .map_err(|errno| Error::from_raw_os_error(errno.into_raw()))?; + let fd = i32::try_from(fd).map_err(|_| Error::OVERFLOW)?; + + // SAFETY: a successful `openat` returns a new owned descriptor. + Ok(unsafe { OwnedFd::from_raw_fd(fd) }) +} + /// Reads the target of `path` relative to `dirfd` into `buf`. /// /// The returned bytes borrow the initialized prefix of `buf`. As with the @@ -27,7 +62,7 @@ pub fn readlinkat<'buf>( let initialized = unsafe { syscalls::syscall4( syscalls::Sysno::readlinkat, - (dirfd.as_raw_fd() as isize).cast_unsigned(), + syscall_fd(dirfd)?, path.as_ptr().addr(), buf.as_mut_ptr().addr(), buf.len(), diff --git a/crates/fspy_nostd/src/fs/mac.rs b/crates/fspy_nostd/src/fs/mac.rs index cea3d2283..e35b5fba8 100644 --- a/crates/fspy_nostd/src/fs/mac.rs +++ b/crates/fspy_nostd/src/fs/mac.rs @@ -7,10 +7,12 @@ use rustix::{ use crate::{BorrowedFd, CStr, CWD, Error, Fat, Result, Thin}; -pub(super) const PATH_MAX: usize = libc::PATH_MAX as usize; +// Darwin UAPI `MAXPATHLEN`. +pub(super) const PATH_MAX: usize = 1024; +const _: () = assert!(libc::PATH_MAX == 1024); #[expect(clippy::needless_pass_by_value, reason = "CStr is a borrowed value type")] -fn openat( +pub(super) fn openat( dirfd: BorrowedFd<'_>, path: CStr<'_, R>, flags: OFlags, diff --git a/crates/fspy_nostd/src/fs/mod.rs b/crates/fspy_nostd/src/fs/mod.rs index ea4e50840..84c1211b4 100644 --- a/crates/fspy_nostd/src/fs/mod.rs +++ b/crates/fspy_nostd/src/fs/mod.rs @@ -2,7 +2,9 @@ use core::mem::MaybeUninit; -use crate::{CStr, Fat, Result}; +pub use rustix::fs::{Mode, OFlags, fstat}; + +use crate::{BorrowedFd, CStr, Fat, OwnedFd, Result}; #[cfg(target_os = "linux")] mod linux; @@ -21,6 +23,20 @@ pub use mac::fcntl_getpath; /// The platform's maximum pathname size, including the terminating NUL. pub const PATH_MAX: usize = imp::PATH_MAX; +/// Opens `path` relative to `dirfd` and returns its owned descriptor. +/// +/// # Errors +/// +/// Returns the error reported by `openat`. +pub fn openat( + dirfd: BorrowedFd<'_>, + path: CStr<'_, R>, + flags: OFlags, + mode: Mode, +) -> Result { + imp::openat(dirfd, path, flags, mode) +} + /// Writes the absolute pathname of the current working directory into `buf`. /// /// The returned C string borrows `buf`, starts at the same address as `buf`, diff --git a/crates/fspy_nostd/src/lib.rs b/crates/fspy_nostd/src/lib.rs index 6cfba529a..545ad8fc4 100644 --- a/crates/fspy_nostd/src/lib.rs +++ b/crates/fspy_nostd/src/lib.rs @@ -65,7 +65,7 @@ macro_rules! wide_cstr { #[cfg(unix)] pub use rustix::{ - fd::{AsRawFd, BorrowedFd}, + fd::{AsRawFd, BorrowedFd, FromRawFd, OwnedFd}, fs::CWD, io::Errno as Error, }; diff --git a/crates/fspy_shm/Cargo.toml b/crates/fspy_shm/Cargo.toml index 512cd57ee..603347290 100644 --- a/crates/fspy_shm/Cargo.toml +++ b/crates/fspy_shm/Cargo.toml @@ -11,6 +11,9 @@ rust-version.workspace = true memmap2 = { workspace = true } uuid = { workspace = true, features = ["v4"] } +[target.'cfg(unix)'.dependencies] +fspy_nostd = { workspace = true } + [target.'cfg(target_os = "windows")'.dependencies] windows-sys = { workspace = true, features = [ "Win32_Foundation", diff --git a/crates/fspy_shm/src/unix.rs b/crates/fspy_shm/src/unix.rs index 6924b25ad..c42ee868f 100644 --- a/crates/fspy_shm/src/unix.rs +++ b/crates/fspy_shm/src/unix.rs @@ -6,7 +6,8 @@ use std::{ ffi::OsStr, fs::{self, File, OpenOptions}, io, - os::unix::fs::OpenOptionsExt as _, + num::NonZeroUsize, + os::unix::{ffi::OsStrExt as _, fs::OpenOptionsExt as _, io::IntoRawFd as _}, path::PathBuf, }; @@ -29,8 +30,8 @@ pub struct ShmKeeper { /// [`map`](Self::map) can be called more than once; every call returns another /// view of the same bytes. Drop the handle once the mappings exist. pub struct ShmHandle { - file: File, - size: usize, + file: fspy_nostd::OwnedFd, + size: NonZeroUsize, } /// The mapped shared bytes. @@ -53,13 +54,10 @@ pub struct Mapping { /// /// Returns an error if the shared memory cannot be created or sized. pub fn create(size: usize) -> io::Result<(ShmKeeper, ShmHandle)> { - if size == 0 { - return Err(io::Error::new( - io::ErrorKind::InvalidInput, - "shared-memory size must be nonzero", - )); - } - let size_u64 = u64::try_from(size).map_err(|_| { + let size = NonZeroUsize::new(size).ok_or_else(|| { + io::Error::new(io::ErrorKind::InvalidInput, "shared-memory size must be nonzero") + })?; + let size_u64 = u64::try_from(size.get()).map_err(|_| { io::Error::new(io::ErrorKind::InvalidInput, "shared-memory size exceeds u64") })?; @@ -81,6 +79,7 @@ pub fn create(size: usize) -> io::Result<(ShmKeeper, ShmHandle)> { // Every byte reads as zero because the file is all holes. file.set_len(size_u64)?; + let file = into_nostd_fd(file); Ok((keeper, ShmHandle { file, size })) } @@ -95,20 +94,61 @@ pub fn create(size: usize) -> io::Result<(ShmKeeper, ShmHandle)> { /// Returns an error if the shared memory is unavailable, which is the common /// case once its keeper has been dropped. pub fn open(id: &OsStr) -> io::Result { - // Rust opens are `O_CLOEXEC`, so a traced process never leaks this - // descriptor. - let file = OpenOptions::new().read(true).write(true).open(id)?; + let file = open_file(id)?; // If another process shrinks the file before `map`, mapping fails. If it // resizes afterwards, nothing here touches the mapped pages. A concurrent // resize cannot make a mapping access invalid memory. - let size = usize::try_from(file.metadata()?.len()) + let size = usize::try_from(fspy_nostd::fs::fstat(&file).map_err(error_to_io)?.st_size) .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "invalid shared-memory size"))?; - if size == 0 { - return Err(io::Error::new(io::ErrorKind::InvalidData, "shared-memory size is zero")); - } + let size = NonZeroUsize::new(size) + .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "shared-memory size is zero"))?; Ok(ShmHandle { file, size }) } +fn open_file(path: &OsStr) -> io::Result { + let mut buf = [0_u8; fspy_nostd::fs::PATH_MAX]; + let path = copy_path(path, &mut buf)?; + fspy_nostd::fs::openat( + fspy_nostd::CWD, + path, + fspy_nostd::fs::OFlags::RDWR | fspy_nostd::fs::OFlags::CLOEXEC, + fspy_nostd::fs::Mode::empty(), + ) + .map_err(error_to_io) +} + +fn copy_path<'buf>( + path: &OsStr, + buf: &'buf mut [u8; fspy_nostd::fs::PATH_MAX], +) -> io::Result> { + let bytes = path.as_bytes(); + if bytes.contains(&0) { + return Err(io::Error::new(io::ErrorKind::InvalidInput, "path contains NUL")); + } + + let len_with_nul = bytes.len().checked_add(1).ok_or(io::ErrorKind::InvalidInput)?; + let initialized = buf.get_mut(..len_with_nul).ok_or_else(|| { + io::Error::from_raw_os_error(fspy_nostd::Error::NAMETOOLONG.raw_os_error()) + })?; + initialized[..bytes.len()].copy_from_slice(bytes); + initialized[bytes.len()] = 0; + + // SAFETY: the copied path contains no NUL, followed by the terminator set + // above, and the returned view borrows the initialized buffer prefix. + Ok(unsafe { fspy_nostd::CStr::from_units_with_nul_unchecked(initialized) }) +} + +fn error_to_io(error: fspy_nostd::Error) -> io::Error { + io::Error::from_raw_os_error(error.raw_os_error()) +} + +fn into_nostd_fd(file: File) -> fspy_nostd::OwnedFd { + let fd = file.into_raw_fd(); + // SAFETY: ownership of `file`'s descriptor transfers without closing or + // duplicating it. + unsafe { fspy_nostd::FromRawFd::from_raw_fd(fd) } +} + impl Drop for ShmKeeper { fn drop(&mut self) { let _ = fs::remove_file(&self.path); @@ -131,7 +171,8 @@ impl ShmHandle { /// /// Returns an error if the mapping cannot be established. pub fn map(&self) -> io::Result { - Ok(Mapping { raw: MmapOptions::new().len(self.size).map_raw(&self.file)? }) + let file = fspy_nostd::AsRawFd::as_raw_fd(&self.file); + Ok(Mapping { raw: MmapOptions::new().len(self.size.get()).map_raw(file)? }) } }