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..a2ac39a42 100644 --- a/crates/fspy_nostd/src/fs/linux.rs +++ b/crates/fspy_nostd/src/fs/linux.rs @@ -1,10 +1,59 @@ 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::{AtFlags, Mode, OFlags}, +}; // Linux UAPI `PATH_MAX`. pub(super) const PATH_MAX: usize = 4096; +#[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::syscall!( + syscalls::Sysno::openat, + dirfd.as_raw_fd(), + path.as_ptr(), + flags.bits(), + mode.bits() + ) + } + .map_err(|errno| Error::from_raw_os_error(errno.into_raw()))?; + + // This should not fail with a well-behaved kernel: `openat` returns a + // nonnegative `c_int` file descriptor. + 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) }) +} + +#[expect(clippy::needless_pass_by_value, reason = "CStr is a borrowed value type")] +pub(super) fn unlinkat(dirfd: BorrowedFd<'_>, path: CStr<'_, R>, flags: AtFlags) -> Result<()> { + // SAFETY: `dirfd` remains borrowed and `path` is NUL-terminated for the + // syscall. + unsafe { + syscalls::syscall!( + syscalls::Sysno::unlinkat, + dirfd.as_raw_fd(), + path.as_ptr(), + flags.bits() + ) + } + .map_err(|errno| Error::from_raw_os_error(errno.into_raw()))?; + Ok(()) +} + /// Reads the target of `path` relative to `dirfd` into `buf`. /// /// The returned bytes borrow the initialized prefix of `buf`. As with the @@ -25,12 +74,12 @@ pub fn readlinkat<'buf>( // is writable for `buf.len()` bytes. `readlinkat` returns the initialized // byte count. let initialized = unsafe { - syscalls::syscall4( + syscalls::syscall!( syscalls::Sysno::readlinkat, - (dirfd.as_raw_fd() as isize).cast_unsigned(), - path.as_ptr().addr(), - buf.as_mut_ptr().addr(), - buf.len(), + dirfd.as_raw_fd(), + path.as_ptr(), + buf.as_mut_ptr(), + buf.len() ) } .map_err(|errno| Error::from_raw_os_error(errno.into_raw()))?; @@ -46,7 +95,7 @@ pub(super) fn getcwd(buf: &mut [MaybeUninit]) -> Result> { // writes no more than that and returns the initialized length including // its terminating NUL. let initialized = - unsafe { syscalls::syscall2(syscalls::Sysno::getcwd, buf.as_mut_ptr().addr(), buf.len()) } + unsafe { syscalls::syscall!(syscalls::Sysno::getcwd, buf.as_mut_ptr(), buf.len()) } .map_err(|errno| Error::from_raw_os_error(errno.into_raw()))?; // SAFETY: the syscall initialized this prefix through its terminating NUL. diff --git a/crates/fspy_nostd/src/fs/mac.rs b/crates/fspy_nostd/src/fs/mac.rs index cea3d2283..bd31513fa 100644 --- a/crates/fspy_nostd/src/fs/mac.rs +++ b/crates/fspy_nostd/src/fs/mac.rs @@ -2,15 +2,17 @@ use core::{mem::MaybeUninit, slice}; use rustix::{ fd::{AsFd as _, AsRawFd as _, FromRawFd as _, OwnedFd}, - fs::{Mode, OFlags}, + fs::{AtFlags, Mode, OFlags}, }; 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, @@ -35,6 +37,21 @@ fn openat( Ok(unsafe { OwnedFd::from_raw_fd(fd) }) } +#[expect(clippy::needless_pass_by_value, reason = "CStr is a borrowed value type")] +pub(super) fn unlinkat(dirfd: BorrowedFd<'_>, path: CStr<'_, R>, flags: AtFlags) -> Result<()> { + // SAFETY: `dirfd` remains borrowed and `path` is NUL-terminated for the + // call. + let result = unsafe { + libc::unlinkat(dirfd.as_raw_fd(), path.as_ptr().cast(), flags.bits().cast_signed()) + }; + if result == -1 { + // SAFETY: libSystem stored this call's error before returning -1. + Err(Error::from_raw_os_error(unsafe { *libc::__error() })) + } else { + Ok(()) + } +} + /// Gets the path associated with `fd`. /// /// `F_GETPATH` writes a NUL-terminated path into its `MAXPATHLEN` buffer but diff --git a/crates/fspy_nostd/src/fs/mod.rs b/crates/fspy_nostd/src/fs/mod.rs index ea4e50840..3720e13bd 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::{AtFlags, Mode, OFlags, fstat, ftruncate}; + +use crate::{BorrowedFd, CStr, Fat, OwnedFd, Result}; #[cfg(target_os = "linux")] mod linux; @@ -21,6 +23,29 @@ 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) +} + +/// Removes `path` relative to `dirfd`. +/// +/// # Errors +/// +/// Returns the error reported by `unlinkat`. +pub fn unlinkat(dirfd: BorrowedFd<'_>, path: CStr<'_, R>, flags: AtFlags) -> Result<()> { + imp::unlinkat(dirfd, path, flags) +} + /// 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..779889d51 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, OwnedFd}, fs::CWD, io::Errno as Error, }; diff --git a/crates/fspy_nostd/src/mm.rs b/crates/fspy_nostd/src/mm.rs index c7cdecd05..7de3763f5 100644 --- a/crates/fspy_nostd/src/mm.rs +++ b/crates/fspy_nostd/src/mm.rs @@ -8,4 +8,4 @@ //! pre-libc startup) and the crate-level backend check, which guarantees //! they cannot silently turn into libc calls on Linux. -pub use rustix::mm::{MapFlags, MprotectFlags, ProtFlags, mmap_anonymous, mprotect, munmap}; +pub use rustix::mm::{MapFlags, MprotectFlags, ProtFlags, mmap, mmap_anonymous, mprotect, munmap}; diff --git a/crates/fspy_shm/Cargo.toml b/crates/fspy_shm/Cargo.toml index 512cd57ee..70115da56 100644 --- a/crates/fspy_shm/Cargo.toml +++ b/crates/fspy_shm/Cargo.toml @@ -8,9 +8,14 @@ publish = false rust-version.workspace = true [dependencies] -memmap2 = { workspace = true } uuid = { workspace = true, features = ["v4"] } +[target.'cfg(unix)'.dependencies] +fspy_nostd = { workspace = true } + +[target.'cfg(windows)'.dependencies] +memmap2 = { 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..38c56cbb8 100644 --- a/crates/fspy_shm/src/unix.rs +++ b/crates/fspy_shm/src/unix.rs @@ -3,14 +3,14 @@ use std::{ env::temp_dir, - ffi::OsStr, - fs::{self, File, OpenOptions}, + ffi::{CString, OsStr}, io, - os::unix::fs::OpenOptionsExt as _, + num::NonZeroUsize, + os::unix::ffi::OsStrExt as _, path::PathBuf, + ptr::{self, NonNull}, }; -use memmap2::{MmapOptions, MmapRaw}; use uuid::Uuid; use crate::BACKING_PREFIX; @@ -29,8 +29,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. @@ -38,9 +38,17 @@ pub struct ShmHandle { /// A `Mapping` keeps the bytes alive until it is dropped and cannot affect the /// shared memory's identifier. pub struct Mapping { - raw: MmapRaw, + ptr: NonNull, + len: NonZeroUsize, } +// SAFETY: a mapping owns no thread-affine state; access synchronization is +// supplied by the fspy channel built on top of it. +unsafe impl Send for Mapping {} +// SAFETY: sharing a `Mapping` does not itself access its bytes, and all actual +// concurrent access is synchronized by the fspy channel. +unsafe impl Sync for Mapping {} + /// Creates `size` bytes of zero-initialized shared memory. /// /// Returns its [`ShmKeeper`] and an already opened [`ShmHandle`], so the @@ -53,13 +61,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") })?; @@ -69,18 +74,20 @@ pub fn create(size: usize) -> io::Result<(ShmKeeper, ShmHandle)> { let path = std::path::absolute(temp_dir())? .join(format!("{BACKING_PREFIX}{}.shm", Uuid::new_v4().simple())); - let file = OpenOptions::new() - .read(true) - .write(true) - .create_new(true) + let file = open_file( + path.as_os_str(), + fspy_nostd::fs::OFlags::RDWR + | fspy_nostd::fs::OFlags::CREATE + | fspy_nostd::fs::OFlags::EXCL + | fspy_nostd::fs::OFlags::CLOEXEC, // Only the creating user may open the mapping. - .mode(0o600) - .open(&path)?; + fspy_nostd::fs::Mode::RUSR | fspy_nostd::fs::Mode::WUSR, + )?; // The keeper exists from here on, so every error path below cleans up. let keeper = ShmKeeper { path }; // Every byte reads as zero because the file is all holes. - file.set_len(size_u64)?; + fspy_nostd::fs::ftruncate(&file, size_u64).map_err(error_to_io)?; Ok((keeper, ShmHandle { file, size })) } @@ -95,23 +102,53 @@ 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, + fspy_nostd::fs::OFlags::RDWR | fspy_nostd::fs::OFlags::CLOEXEC, + fspy_nostd::fs::Mode::empty(), + )?; // 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, + flags: fspy_nostd::fs::OFlags, + mode: fspy_nostd::fs::Mode, +) -> io::Result { + let path = CString::new(path.as_bytes())?; + fspy_nostd::fs::openat(fspy_nostd::CWD, as_nostd_path(&path), flags, mode).map_err(error_to_io) +} + +fn remove_file(path: &OsStr) -> io::Result<()> { + let path = CString::new(path.as_bytes())?; + fspy_nostd::fs::unlinkat( + fspy_nostd::CWD, + as_nostd_path(&path), + fspy_nostd::fs::AtFlags::empty(), + ) + .map_err(error_to_io) +} + +fn as_nostd_path(path: &CString) -> fspy_nostd::CStr<'_, fspy_nostd::Fat> { + // SAFETY: `CString` contains no interior NUL and includes one terminating + // NUL; the returned view borrows it. + unsafe { fspy_nostd::CStr::from_units_with_nul_unchecked(path.as_bytes_with_nul()) } +} + +fn error_to_io(error: fspy_nostd::Error) -> io::Error { + io::Error::from_raw_os_error(error.raw_os_error()) +} + impl Drop for ShmKeeper { fn drop(&mut self) { - let _ = fs::remove_file(&self.path); + let _ = remove_file(self.path.as_os_str()); } } @@ -131,7 +168,40 @@ 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 _slice_len = isize::try_from(self.size.get()).map_err(|_| { + io::Error::new(io::ErrorKind::InvalidData, "shared-memory size exceeds isize") + })?; + // SAFETY: the address is only a hint, the validated nonzero length is + // representable as a Rust slice, the descriptor remains borrowed, and + // the resulting shared mapping is owned by `Mapping`. + let mapped = unsafe { + fspy_nostd::mm::mmap( + ptr::null_mut(), + self.size.get(), + fspy_nostd::mm::ProtFlags::READ | fspy_nostd::mm::ProtFlags::WRITE, + fspy_nostd::mm::MapFlags::SHARED, + &self.file, + 0, + ) + } + .map_err(error_to_io)?; + let Some(ptr) = NonNull::new(mapped.cast()) else { + // `mmap` reports failure with `MAP_FAILED`, not null, so this is a + // successful mapping at address zero. Rust references cannot + // represent it. + // SAFETY: release that complete mapping before returning an error. + let _ = unsafe { fspy_nostd::mm::munmap(mapped, self.size.get()) }; + return Err(io::Error::other("mmap returned address zero")); + }; + Ok(Mapping { ptr, len: self.size }) + } +} + +impl Drop for Mapping { + fn drop(&mut self) { + // SAFETY: this is the complete mapping owned by `self`, and dropping + // it proves that no safe borrow through `self` remains. + let _ = unsafe { fspy_nostd::mm::munmap(self.ptr.as_ptr().cast(), self.len.get()) }; } } @@ -139,14 +209,14 @@ impl ShmHandle { impl Mapping { /// Returns the mapped length in bytes. #[must_use] - pub fn len(&self) -> usize { - self.raw.len() + pub const fn len(&self) -> usize { + self.len.get() } /// Returns a raw pointer to the first mapped byte. #[must_use] - pub fn as_ptr(&self) -> *mut u8 { - self.raw.as_mut_ptr() + pub const fn as_ptr(&self) -> *mut u8 { + self.ptr.as_ptr() } /// Returns the mapped bytes as a shared slice. @@ -156,7 +226,7 @@ impl Mapping { /// The caller must ensure that no process or thread mutates the mapping for /// the lifetime of the returned slice. #[must_use] - pub unsafe fn as_slice(&self) -> &[u8] { + pub const unsafe fn as_slice(&self) -> &[u8] { // SAFETY: The mapping is valid for its full length, and the caller // guarantees that it is not mutated while the slice is borrowed. unsafe { std::slice::from_raw_parts(self.as_ptr().cast_const(), self.len()) }