From c8a63353eff2cbcb98872b69b80a2e9abcb6fb29 Mon Sep 17 00:00:00 2001 From: wan9chi Date: Thu, 13 Aug 2026 11:57:55 +0800 Subject: [PATCH 1/5] refactor(fspy-shm): adopt fspy_nostd on Unix Co-authored-by: GPT-5 Codex --- Cargo.lock | 1 + crates/fspy_nostd/src/fs/linux.rs | 55 ++++++++++- crates/fspy_nostd/src/fs/mac.rs | 23 ++++- crates/fspy_nostd/src/fs/mod.rs | 27 +++++- crates/fspy_nostd/src/lib.rs | 2 +- crates/fspy_nostd/src/mm.rs | 2 +- crates/fspy_shm/Cargo.toml | 7 +- crates/fspy_shm/src/unix.rs | 149 +++++++++++++++++++++++------- 8 files changed, 223 insertions(+), 43 deletions(-) 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..a1a68e758 100644 --- a/crates/fspy_nostd/src/fs/linux.rs +++ b/crates/fspy_nostd/src/fs/linux.rs @@ -1,10 +1,61 @@ 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; +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) }) +} + +#[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::syscall3( + syscalls::Sysno::unlinkat, + syscall_fd(dirfd)?, + path.as_ptr().addr(), + usize::try_from(flags.bits()).map_err(|_| Error::INVAL)?, + ) + } + .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 @@ -27,7 +78,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..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..4090ae69b 100644 --- a/crates/fspy_shm/src/unix.rs +++ b/crates/fspy_shm/src/unix.rs @@ -4,13 +4,13 @@ use std::{ env::temp_dir, ffi::OsStr, - fs::{self, File, OpenOptions}, 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,66 @@ 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 mut buf = [0_u8; fspy_nostd::fs::PATH_MAX]; + let path = copy_path(path, &mut buf)?; + fspy_nostd::fs::openat(fspy_nostd::CWD, path, flags, mode).map_err(error_to_io) +} + +fn remove_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::unlinkat(fspy_nostd::CWD, path, fspy_nostd::fs::AtFlags::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()) +} + impl Drop for ShmKeeper { fn drop(&mut self) { - let _ = fs::remove_file(&self.path); + let _ = remove_file(self.path.as_os_str()); } } @@ -131,7 +181,38 @@ 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 { + // Rust references cannot represent a mapping at address zero. + // SAFETY: release the successful mapping before rejecting it. + 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 +220,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 +237,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()) } From b16a42ae9e6285c4a7c1049c3e82229f004dab35 Mon Sep 17 00:00:00 2001 From: wan9chi Date: Thu, 13 Aug 2026 12:10:25 +0800 Subject: [PATCH 2/5] refactor(fspy-nostd): use syscall ABI conversions Co-authored-by: GPT-5 Codex --- crates/fspy_nostd/src/fs/linux.rs | 43 ++++++++++++++++--------------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/crates/fspy_nostd/src/fs/linux.rs b/crates/fspy_nostd/src/fs/linux.rs index a1a68e758..a8ab3c1b0 100644 --- a/crates/fspy_nostd/src/fs/linux.rs +++ b/crates/fspy_nostd/src/fs/linux.rs @@ -10,11 +10,6 @@ use crate::{ // 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<'_>, @@ -25,16 +20,22 @@ pub(super) fn openat( // SAFETY: `dirfd` remains borrowed and `path` is NUL-terminated. The // kernel receives all four syscall arguments explicitly. let fd = unsafe { - syscalls::syscall4( + syscalls::syscall!( 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)?, + dirfd.as_raw_fd(), + path.as_ptr(), + flags.bits(), + mode.bits() ) } .map_err(|errno| Error::from_raw_os_error(errno.into_raw()))?; - let fd = i32::try_from(fd).map_err(|_| Error::OVERFLOW)?; + + #[expect( + clippy::cast_possible_truncation, + clippy::cast_possible_wrap, + reason = "a successful openat returns a nonnegative c_int file descriptor" + )] + let fd = fd as i32; // SAFETY: a successful `openat` returns a new owned descriptor. Ok(unsafe { OwnedFd::from_raw_fd(fd) }) @@ -45,11 +46,11 @@ pub(super) fn unlinkat(dirfd: BorrowedFd<'_>, path: CStr<'_, R>, flags: AtFla // SAFETY: `dirfd` remains borrowed and `path` is NUL-terminated for the // syscall. unsafe { - syscalls::syscall3( + syscalls::syscall!( syscalls::Sysno::unlinkat, - syscall_fd(dirfd)?, - path.as_ptr().addr(), - usize::try_from(flags.bits()).map_err(|_| Error::INVAL)?, + dirfd.as_raw_fd(), + path.as_ptr(), + flags.bits() ) } .map_err(|errno| Error::from_raw_os_error(errno.into_raw()))?; @@ -76,12 +77,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, - syscall_fd(dirfd)?, - 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()))?; @@ -97,7 +98,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. From a1288f760b74901e30f92d0d7c3d28435e41ac8d Mon Sep 17 00:00:00 2001 From: wan9chi Date: Thu, 13 Aug 2026 12:42:00 +0800 Subject: [PATCH 3/5] fix(fspy-nostd): validate returned file descriptors Co-authored-by: GPT-5 Codex --- crates/fspy_nostd/src/fs/linux.rs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/crates/fspy_nostd/src/fs/linux.rs b/crates/fspy_nostd/src/fs/linux.rs index a8ab3c1b0..a2ac39a42 100644 --- a/crates/fspy_nostd/src/fs/linux.rs +++ b/crates/fspy_nostd/src/fs/linux.rs @@ -30,12 +30,9 @@ pub(super) fn openat( } .map_err(|errno| Error::from_raw_os_error(errno.into_raw()))?; - #[expect( - clippy::cast_possible_truncation, - clippy::cast_possible_wrap, - reason = "a successful openat returns a nonnegative c_int file descriptor" - )] - let fd = fd as i32; + // 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) }) From 0cc0b2687dc82e4d1dcb24a30e6dcfd2d3d32518 Mon Sep 17 00:00:00 2001 From: wan9chi Date: Thu, 13 Aug 2026 12:48:20 +0800 Subject: [PATCH 4/5] refactor(fspy-shm): allocate temporary C paths Co-authored-by: GPT-5 Codex --- crates/fspy_shm/src/unix.rs | 41 +++++++++++++------------------------ 1 file changed, 14 insertions(+), 27 deletions(-) diff --git a/crates/fspy_shm/src/unix.rs b/crates/fspy_shm/src/unix.rs index 4090ae69b..cfa7762e0 100644 --- a/crates/fspy_shm/src/unix.rs +++ b/crates/fspy_shm/src/unix.rs @@ -3,7 +3,7 @@ use std::{ env::temp_dir, - ffi::OsStr, + ffi::{CString, OsStr}, io, num::NonZeroUsize, os::unix::ffi::OsStrExt as _, @@ -122,37 +122,24 @@ fn open_file( flags: fspy_nostd::fs::OFlags, mode: fspy_nostd::fs::Mode, ) -> 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, flags, mode).map_err(error_to_io) + 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 mut buf = [0_u8; fspy_nostd::fs::PATH_MAX]; - let path = copy_path(path, &mut buf)?; - fspy_nostd::fs::unlinkat(fspy_nostd::CWD, path, fspy_nostd::fs::AtFlags::empty()) - .map_err(error_to_io) + 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 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 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 { From 352c510e4546467a263e37bc5b46c578822530f8 Mon Sep 17 00:00:00 2001 From: wan9chi Date: Thu, 13 Aug 2026 12:51:03 +0800 Subject: [PATCH 5/5] docs(fspy-shm): clarify zero-address cleanup Co-authored-by: GPT-5 Codex --- crates/fspy_shm/src/unix.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/fspy_shm/src/unix.rs b/crates/fspy_shm/src/unix.rs index cfa7762e0..38c56cbb8 100644 --- a/crates/fspy_shm/src/unix.rs +++ b/crates/fspy_shm/src/unix.rs @@ -186,8 +186,10 @@ impl ShmHandle { } .map_err(error_to_io)?; let Some(ptr) = NonNull::new(mapped.cast()) else { - // Rust references cannot represent a mapping at address zero. - // SAFETY: release the successful mapping before rejecting it. + // `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")); };