From 4d53c40f77422f9fe48d6e794aa6be07786545a2 Mon Sep 17 00:00:00 2001 From: wan9chi Date: Wed, 19 Aug 2026 23:59:49 +0800 Subject: [PATCH] refactor(fspy-nostd): own platform system interfaces Replace rustix with crate-owned descriptor, flag, mapping, and error types. Linux and freestanding none targets issue syscalls directly, while macOS uses libc and Windows keeps its Win32 backend. This lets injected Linux code share the same runtime-independent APIs without acquiring a libc dependency. Co-authored-by: GPT-5 Codex --- Cargo.lock | 10 +- Cargo.toml | 2 +- crates/fspy_client_unix/src/convert.rs | 2 +- crates/fspy_nostd/Cargo.toml | 19 +--- crates/fspy_nostd/README.md | 22 +++- crates/fspy_nostd/src/c_str.rs | 2 +- crates/fspy_nostd/src/env/linux.rs | 17 ++-- crates/fspy_nostd/src/env/mod.rs | 4 +- crates/fspy_nostd/src/error.rs | 104 +++++++++++++++++++ crates/fspy_nostd/src/fd.rs | 95 ++++++++++++++++++ crates/fspy_nostd/src/fs/linux.rs | 44 ++++++-- crates/fspy_nostd/src/fs/mac.rs | 45 ++++++--- crates/fspy_nostd/src/fs/mod.rs | 8 +- crates/fspy_nostd/src/fs/tests.rs | 14 +-- crates/fspy_nostd/src/fs/unix.rs | 65 +++++++++++- crates/fspy_nostd/src/fs/windows.rs | 2 +- crates/fspy_nostd/src/io.rs | 41 ++++++++ crates/fspy_nostd/src/lib.rs | 66 +++--------- crates/fspy_nostd/src/mm.rs | 134 ++++++++++++++++++++++++- crates/fspy_nostd/src/mm/linux.rs | 76 ++++++++++++++ crates/fspy_nostd/src/mm/mac.rs | 70 +++++++++++++ crates/fspy_nostd/src/mm/windows.rs | 6 +- crates/fspy_nostd/src/param.rs | 52 +++++----- crates/fspy_nostd/src/windows.rs | 26 +---- crates/fspy_shm/src/unix.rs | 7 +- 25 files changed, 752 insertions(+), 181 deletions(-) create mode 100644 crates/fspy_nostd/src/error.rs create mode 100644 crates/fspy_nostd/src/fd.rs create mode 100644 crates/fspy_nostd/src/io.rs create mode 100644 crates/fspy_nostd/src/mm/linux.rs create mode 100644 crates/fspy_nostd/src/mm/mac.rs diff --git a/Cargo.lock b/Cargo.lock index 594f1ca07..14cfa6dfe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1354,7 +1354,7 @@ dependencies = [ "bitflags 2.10.0", "bstr", "libc", - "rustix", + "linux-raw-sys 0.12.1", "syscalls", "windows-sys 0.61.2", ] @@ -2008,6 +2008,12 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + [[package]] name = "litrs" version = "1.0.0" @@ -3261,7 +3267,7 @@ dependencies = [ "bitflags 2.10.0", "errno", "libc", - "linux-raw-sys", + "linux-raw-sys 0.11.0", "windows-sys 0.61.2", ] diff --git a/Cargo.toml b/Cargo.toml index eab1b1b7a..fee91cf29 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -92,6 +92,7 @@ getrandom = "0.4.2" itoa = "1.0.17" jsonc-parser = "0.32.0" libc = "0.2.185" +linux-raw-sys = { version = "0.12", default-features = false } libtest-mimic = "0.8.2" memmap2 = "0.9.11" monostate = "1.0.2" @@ -126,7 +127,6 @@ ref-cast = "1.0.24" regex = "1.11.3" rusqlite = "0.39.0" rustc-hash = "2.1.1" -rustix = { version = "1", default-features = false } # SeccompAction::UserNotif (SECCOMP_RET_USER_NOTIF) was added after the latest published release (v0.5.0) seccompiler = { git = "https://github.com/rust-vmm/seccompiler", rev = "08587106340b8e3cb361c7561411510039436857" } serde = "1.0.219" diff --git a/crates/fspy_client_unix/src/convert.rs b/crates/fspy_client_unix/src/convert.rs index f286b7e0c..2466e20b1 100644 --- a/crates/fspy_client_unix/src/convert.rs +++ b/crates/fspy_client_unix/src/convert.rs @@ -2,7 +2,7 @@ use std::ffi::CStr; use allocator_api2::{alloc::Allocator, vec::Vec}; use bstr::ByteSlice; -use fspy_nostd::{AsRawFd as _, BorrowedFd, CWD}; +use fspy_nostd::{BorrowedFd, CWD}; use fspy_shared::ipc::AccessMode; use libc::{c_char, c_int}; diff --git a/crates/fspy_nostd/Cargo.toml b/crates/fspy_nostd/Cargo.toml index 8c1f935f3..bddf0046d 100644 --- a/crates/fspy_nostd/Cargo.toml +++ b/crates/fspy_nostd/Cargo.toml @@ -11,24 +11,15 @@ doctest = false # Only the borrowed `BStr` type is used. Keeping every feature disabled makes # the environment iterator usable without `alloc` or `std`. bstr = { workspace = true } - -[target.'cfg(unix)'.dependencies] -rustix = { workspace = true, features = ["fs", "mm"] } +bitflags = { workspace = true } [target.'cfg(target_os = "macos")'.dependencies] libc = { workspace = true } -# On Linux the page size is probed from the kernel directly (see param.rs); -# rustix's `param` is only needed where sysconf is the platform interface. -[target.'cfg(all(unix, not(target_os = "linux")))'.dependencies] -rustix = { workspace = true, features = ["param"] } - -# The compile-time backend check in lib.rs needs a `linux_raw`-gated rustix -# item to reference; `runtime` is the module that has one. -[target.'cfg(target_os = "linux")'.dependencies] +[target.'cfg(any(target_os = "linux", target_os = "none"))'.dependencies] # Parsing remains allocation-free and no-std; `atoi` enables `std` by default. atoi = { version = "3.1.0", default-features = false } -rustix = { workspace = true, features = ["runtime"] } +linux-raw-sys = { workspace = true, features = ["errno", "general", "no_std"] } syscalls = { workspace = true } [target.'cfg(windows)'.dependencies] @@ -41,9 +32,5 @@ windows-sys = { workspace = true, features = [ "Win32_System_Memory", ] } -# Cross-validates the page-size probe against rustix's auxv-based answer. -[target.'cfg(target_os = "linux")'.dev-dependencies] -rustix = { workspace = true, features = ["param"] } - [lints] workspace = true diff --git a/crates/fspy_nostd/README.md b/crates/fspy_nostd/README.md index 9e918ca5e..c9a499264 100644 --- a/crates/fspy_nostd/README.md +++ b/crates/fspy_nostd/README.md @@ -2,7 +2,8 @@ Low-level operations for fspy code that runs before a process runtime is ready or in a context where normal runtime code can deadlock. -The current implementation supports Linux, macOS, and Windows. +The current implementation supports Linux, `target_os = "none"` code injected +into Linux, macOS, and Windows. ## Execution contexts @@ -34,7 +35,12 @@ Preload code can also run before `main`, while libc and the target runtime are s The injected runtime cannot assume that the target process has libc. Linux kernel operations in `fspy_nostd` use raw syscalls, so injected code can reuse them without linking or calling libc. -The final injected artifact must also reject libc references that dependencies or compiler-generated memory operations introduce. The raw-syscall check covers the `rustix` backend, not the complete artifact link. +For this crate, `target_os = "none"` means code that runs inside a Linux +process. It uses the same Linux kernel ABI as the normal Linux build. + +The final injected artifact must also reject libc references introduced by +other dependencies or compiler-generated memory operations. This crate's +Linux operations alone cannot enforce the complete artifact link. ## API rules @@ -46,11 +52,17 @@ Every exported operation follows these rules: Code that needs allocation uses an explicit allocator. [`fspy_nostd_alloc`](../fspy_nostd_alloc) provides one based on memory mappings. -## Linux raw-syscall enforcement +## Platform boundaries -`rustix` can use libc instead of raw syscalls. A dependency can select that backend through feature unification or `RUSTFLAGS`. +- Linux and `none` issue syscalls directly and obtain ABI constants and + structures from `linux-raw-sys`. +- macOS calls libSystem through `libc`, as required by the platform. +- Windows calls Win32 directly. -[`lib.rs`](src/lib.rs) references `rustix::runtime`, which exists only with the raw Linux backend. Selecting the libc backend makes `fspy_nostd` fail to compile. +`Error` owns the raw platform error code. On macOS and Windows it additionally +provides `Error::last_os_error()` because their APIs report failure through a +sentinel and put the reason in thread-local state. Linux and `none` syscalls +return their error directly, so that method deliberately does not exist there. ## Modules diff --git a/crates/fspy_nostd/src/c_str.rs b/crates/fspy_nostd/src/c_str.rs index 9fda1971d..bf36cdf86 100644 --- a/crates/fspy_nostd/src/c_str.rs +++ b/crates/fspy_nostd/src/c_str.rs @@ -48,7 +48,7 @@ pub type WideCStr<'a, R> = CStr<'a, R, u16>; /// A borrowed NUL-terminated string of the platform's native path code /// units: bytes on Unix and wide (`u16`) code units on Windows. -#[cfg(unix)] +#[cfg(any(target_os = "linux", target_os = "none", target_os = "macos"))] pub type OsCStr<'a, R> = CStr<'a, R>; /// A borrowed NUL-terminated string of the platform's native path code /// units: bytes on Unix and wide (`u16`) code units on Windows. diff --git a/crates/fspy_nostd/src/env/linux.rs b/crates/fspy_nostd/src/env/linux.rs index 827b9da9e..d8da3a095 100644 --- a/crates/fspy_nostd/src/env/linux.rs +++ b/crates/fspy_nostd/src/env/linux.rs @@ -2,10 +2,12 @@ use core::{ffi::CStr as CoreCStr, num::NonZeroUsize, ptr, slice}; use atoi::FromRadix10Checked as _; use bstr::{BStr, ByteSlice as _}; -use rustix::fs::{Mode, OFlags}; use super::Entry; -use crate::{CStr, CWD, Error, Fat, Result}; +use crate::{ + CStr, CWD, Error, Fat, Result, Thin, + fs::{Mode, OFlags}, +}; #[derive(Clone, Copy)] struct Bounds { @@ -147,9 +149,8 @@ impl Iterator for FatEnvs { /// /// This opens and reads `/proc/self/stat` once into fixed stack storage and /// parses `arg_start`, `arg_end`, `env_start`, and `env_end` together. It does -/// not allocate, and rustix's raw Linux backend makes the file operations -/// direct syscalls. Iterators subsequently created from the snapshot perform -/// no syscalls. +/// not allocate, and the file operations are direct syscalls. Iterators +/// subsequently created from the snapshot perform no syscalls. /// /// # Errors /// @@ -174,7 +175,9 @@ fn read_bounds() -> Result { const STAT_PATH: &CoreCStr = c"/proc/self/stat"; const STAT_CAPACITY: usize = 4096; - let fd = rustix::fs::openat(CWD, STAT_PATH, OFlags::RDONLY | OFlags::CLOEXEC, Mode::empty())?; + // SAFETY: `STAT_PATH` is a static NUL-terminated byte string. + let path = unsafe { CStr::::from_ptr(STAT_PATH.as_ptr().cast()) }; + let fd = crate::fs::openat(CWD, path, OFlags::RDONLY | OFlags::CLOEXEC, Mode::empty())?; let mut stat = [0; STAT_CAPACITY]; let mut initialized = 0; @@ -186,7 +189,7 @@ fn read_bounds() -> Result { return Err(Error::OVERFLOW); } - let Some(read) = NonZeroUsize::new(rustix::io::read(&fd, remaining)?) else { + let Some(read) = NonZeroUsize::new(crate::io::read(fd.as_fd(), remaining)?) else { break; }; initialized = initialized.checked_add(read.get()).ok_or(Error::OVERFLOW)?; diff --git a/crates/fspy_nostd/src/env/mod.rs b/crates/fspy_nostd/src/env/mod.rs index bd82e19ca..ff7ffd2b3 100644 --- a/crates/fspy_nostd/src/env/mod.rs +++ b/crates/fspy_nostd/src/env/mod.rs @@ -13,12 +13,12 @@ use bstr::BStr; use crate::{CStr, Fat}; -#[cfg(target_os = "linux")] +#[cfg(any(target_os = "linux", target_os = "none"))] mod linux; #[cfg(target_os = "macos")] mod mac; -#[cfg(target_os = "linux")] +#[cfg(any(target_os = "linux", target_os = "none"))] pub use linux::{Current, FatArgs, FatEnvs, current}; #[cfg(target_os = "macos")] pub use mac::{Current, FatArgs, FatEnvs, ThinArgs, ThinEnvs, args, current, envs}; diff --git a/crates/fspy_nostd/src/error.rs b/crates/fspy_nostd/src/error.rs new file mode 100644 index 000000000..e6d3d9af4 --- /dev/null +++ b/crates/fspy_nostd/src/error.rs @@ -0,0 +1,104 @@ +#[cfg(target_os = "macos")] +use libc::__error; +#[cfg(windows)] +use windows_sys::Win32::Foundation::GetLastError; + +/// An operating-system error code. +#[cfg(any(target_os = "linux", target_os = "none", target_os = "macos"))] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[repr(transparent)] +pub struct Error(i32); + +/// An operating-system error code. +#[cfg(windows)] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[repr(transparent)] +pub struct Error(u32); + +#[cfg(any(target_os = "linux", target_os = "none", target_os = "macos"))] +impl Error { + pub const BADF: Self = Self(errno::BADF); + pub const INVAL: Self = Self(errno::INVAL); + pub const NOENT: Self = Self(errno::NOENT); + pub const NOMEM: Self = Self(errno::NOMEM); + pub const OVERFLOW: Self = Self(errno::OVERFLOW); + pub const RANGE: Self = Self(errno::RANGE); + + /// Creates an error from a raw errno value. + #[must_use] + pub const fn from_raw_os_error(code: i32) -> Self { + Self(code) + } + + /// Returns the raw errno value. + #[must_use] + pub const fn raw_os_error(self) -> i32 { + self.0 + } + + /// Returns the calling thread's last operating-system error. + /// + /// Call this immediately after the failing libc call: anything in between, + /// including drops, can overwrite the thread-local error code. + #[cfg(target_os = "macos")] + #[must_use] + pub fn last_os_error() -> Self { + // SAFETY: libSystem exposes the calling thread's errno through this + // non-null pointer. + Self::from_raw_os_error(unsafe { __error().read() }) + } +} + +#[cfg(windows)] +impl Error { + /// Creates an error from a raw Windows error code. + #[must_use] + pub const fn from_raw_os_error(code: u32) -> Self { + Self(code) + } + + /// Returns the raw Windows error code. + #[must_use] + pub const fn raw_os_error(self) -> u32 { + self.0 + } + + /// Returns the calling thread's last operating-system error. + /// + /// Call this immediately after the failing Win32 call: anything in + /// between, including drops, can overwrite the thread-local error code. + #[must_use] + pub fn last_os_error() -> Self { + // SAFETY: `GetLastError` reads thread-local error state. + Self::from_raw_os_error(unsafe { GetLastError() }) + } +} + +#[cfg(any(target_os = "linux", target_os = "none"))] +impl From for Error { + fn from(error: syscalls::Errno) -> Self { + Self::from_raw_os_error(error.into_raw()) + } +} + +#[cfg(any(target_os = "linux", target_os = "none"))] +mod errno { + pub const BADF: i32 = linux_raw_sys::errno::EBADF.cast_signed(); + pub const INVAL: i32 = linux_raw_sys::errno::EINVAL.cast_signed(); + pub const NOMEM: i32 = linux_raw_sys::errno::ENOMEM.cast_signed(); + pub const NOENT: i32 = linux_raw_sys::errno::ENOENT.cast_signed(); + pub const OVERFLOW: i32 = linux_raw_sys::errno::EOVERFLOW.cast_signed(); + pub const RANGE: i32 = linux_raw_sys::errno::ERANGE.cast_signed(); +} + +#[cfg(target_os = "macos")] +mod errno { + pub const BADF: i32 = libc::EBADF; + pub const INVAL: i32 = libc::EINVAL; + pub const NOMEM: i32 = libc::ENOMEM; + pub const NOENT: i32 = libc::ENOENT; + pub const OVERFLOW: i32 = libc::EOVERFLOW; + pub const RANGE: i32 = libc::ERANGE; +} + +pub type Result = core::result::Result; diff --git a/crates/fspy_nostd/src/fd.rs b/crates/fspy_nostd/src/fd.rs new file mode 100644 index 000000000..202411033 --- /dev/null +++ b/crates/fspy_nostd/src/fd.rs @@ -0,0 +1,95 @@ +use core::marker::PhantomData; + +/// A raw Unix file descriptor. +pub type RawFd = i32; + +/// A borrowed file descriptor or a reserved descriptor value accepted by the +/// receiving system call, such as [`CWD`]. +#[derive(Clone, Copy)] +#[repr(transparent)] +pub struct BorrowedFd<'fd> { + fd: RawFd, + lifetime: PhantomData<&'fd OwnedFd>, +} + +/// An owned file descriptor, closed on drop. +#[repr(transparent)] +pub struct OwnedFd { + fd: RawFd, +} + +impl BorrowedFd<'_> { + /// Borrows a raw descriptor or reserved descriptor value. + /// + /// # Safety + /// + /// A real descriptor must remain open for the returned lifetime. A + /// reserved value must be valid for every operation receiving the borrow. + /// The value `-1` is never permitted. + /// + /// # Panics + /// + /// Panics when `fd` is `-1`. + #[must_use] + pub const unsafe fn borrow_raw(fd: RawFd) -> Self { + assert!(fd != -1, "-1 is not a borrowed file descriptor"); + Self { fd, lifetime: PhantomData } + } + + /// Returns the raw descriptor without transferring ownership. + #[must_use] + pub const fn as_raw_fd(self) -> RawFd { + self.fd + } +} + +impl OwnedFd { + /// Takes ownership of a raw descriptor returned by the operating system. + /// + /// # Safety + /// + /// `fd` must be a valid, uniquely owned descriptor that may be closed. + pub(crate) const unsafe fn from_raw_fd(fd: RawFd) -> Self { + debug_assert!(fd >= 0); + Self { fd } + } + + /// Borrows this descriptor. + #[must_use] + pub const fn as_fd(&self) -> BorrowedFd<'_> { + // SAFETY: `self` keeps the descriptor open for the returned lifetime. + unsafe { BorrowedFd::borrow_raw(self.fd) } + } + + /// Returns the raw descriptor without transferring ownership. + #[must_use] + pub const fn as_raw_fd(&self) -> RawFd { + self.fd + } +} + +impl Drop for OwnedFd { + fn drop(&mut self) { + #[cfg(any(target_os = "linux", target_os = "none"))] + { + // SAFETY: this type owns the descriptor and closes it exactly + // once. Close errors cannot be acted on during drop. + let _ = unsafe { syscalls::syscall!(syscalls::Sysno::close, self.fd) }; + } + #[cfg(target_os = "macos")] + { + // SAFETY: this type owns the descriptor and closes it exactly + // once. Close errors cannot be acted on during drop. + let _ = unsafe { libc::close(self.fd) }; + } + } +} + +#[cfg(any(target_os = "linux", target_os = "none"))] +const CWD_RAW: RawFd = linux_raw_sys::general::AT_FDCWD; +#[cfg(target_os = "macos")] +const CWD_RAW: RawFd = libc::AT_FDCWD; + +/// The reserved directory descriptor representing the current directory. +// SAFETY: `AT_FDCWD` is a permanent reserved value accepted by `*at` calls. +pub const CWD: BorrowedFd<'static> = unsafe { BorrowedFd::borrow_raw(CWD_RAW) }; diff --git a/crates/fspy_nostd/src/fs/linux.rs b/crates/fspy_nostd/src/fs/linux.rs index a2ac39a42..259133da2 100644 --- a/crates/fspy_nostd/src/fs/linux.rs +++ b/crates/fspy_nostd/src/fs/linux.rs @@ -1,14 +1,21 @@ use core::{mem::MaybeUninit, slice}; -use rustix::fd::FromRawFd as _; - use crate::{ - AsRawFd as _, BorrowedFd, CStr, Error, Fat, OwnedFd, Result, Thin, - fs::{AtFlags, Mode, OFlags}, + BorrowedFd, CStr, Error, Fat, OwnedFd, Result, Thin, + fs::{AtFlags, Mode, OFlags, Stat}, }; // Linux UAPI `PATH_MAX`. pub(super) const PATH_MAX: usize = 4096; +pub(super) const O_RDONLY: u32 = linux_raw_sys::general::O_RDONLY; +pub(super) const O_RDWR: u32 = linux_raw_sys::general::O_RDWR; +pub(super) const O_CREAT: u32 = linux_raw_sys::general::O_CREAT; +pub(super) const O_EXCL: u32 = linux_raw_sys::general::O_EXCL; +pub(super) const O_CLOEXEC: u32 = linux_raw_sys::general::O_CLOEXEC; +pub(super) const AT_REMOVEDIR: u32 = linux_raw_sys::general::AT_REMOVEDIR; +pub(super) type ModeBits = u32; +pub(super) const S_IRUSR: ModeBits = linux_raw_sys::general::S_IRUSR; +pub(super) const S_IWUSR: ModeBits = linux_raw_sys::general::S_IWUSR; #[expect(clippy::needless_pass_by_value, reason = "CStr is a borrowed value type")] pub(super) fn openat( @@ -28,7 +35,7 @@ pub(super) fn openat( mode.bits() ) } - .map_err(|errno| Error::from_raw_os_error(errno.into_raw()))?; + .map_err(Error::from)?; // This should not fail with a well-behaved kernel: `openat` returns a // nonnegative `c_int` file descriptor. @@ -50,7 +57,26 @@ pub(super) fn unlinkat(dirfd: BorrowedFd<'_>, path: CStr<'_, R>, flags: AtFla flags.bits() ) } - .map_err(|errno| Error::from_raw_os_error(errno.into_raw()))?; + .map_err(Error::from)?; + Ok(()) +} + +pub(super) fn fstat(fd: BorrowedFd<'_>) -> Result { + let mut raw = core::mem::MaybeUninit::::zeroed(); + // SAFETY: `fd` remains borrowed and `raw` points to writable storage for + // the kernel's architecture-specific `stat` structure. + unsafe { syscalls::syscall!(syscalls::Sysno::fstat, fd.as_raw_fd(), raw.as_mut_ptr()) } + .map_err(Error::from)?; + // SAFETY: a successful `fstat` initialized the complete structure. + let raw = unsafe { raw.assume_init() }; + Ok(Stat { st_size: raw.st_size }) +} + +pub(super) fn ftruncate(fd: BorrowedFd<'_>, len: u64) -> Result<()> { + // SAFETY: `fd` remains borrowed; the kernel receives the desired length + // by value and validates whether it is representable for the file. + unsafe { syscalls::syscall!(syscalls::Sysno::ftruncate, fd.as_raw_fd(), len) } + .map_err(Error::from)?; Ok(()) } @@ -82,21 +108,19 @@ pub fn readlinkat<'buf>( buf.len() ) } - .map_err(|errno| Error::from_raw_os_error(errno.into_raw()))?; + .map_err(Error::from)?; // SAFETY: the syscall initialized exactly this prefix. Ok(unsafe { slice::from_raw_parts(buf.as_ptr().cast(), initialized) }) } pub(super) fn getcwd(buf: &mut [MaybeUninit]) -> Result> { - // rustix exposes only an allocating `getcwd`, so use the raw syscall for - // caller-owned storage. // SAFETY: `buf` is writable for exactly `buf.len()` bytes. The syscall // writes no more than that and returns the initialized length including // its terminating NUL. let initialized = unsafe { syscalls::syscall!(syscalls::Sysno::getcwd, buf.as_mut_ptr(), buf.len()) } - .map_err(|errno| Error::from_raw_os_error(errno.into_raw()))?; + .map_err(Error::from)?; // SAFETY: the syscall initialized this prefix through its terminating NUL. let bytes = unsafe { slice::from_raw_parts(buf.as_ptr().cast(), initialized) }; diff --git a/crates/fspy_nostd/src/fs/mac.rs b/crates/fspy_nostd/src/fs/mac.rs index bd31513fa..943dfd417 100644 --- a/crates/fspy_nostd/src/fs/mac.rs +++ b/crates/fspy_nostd/src/fs/mac.rs @@ -1,15 +1,22 @@ use core::{mem::MaybeUninit, slice}; -use rustix::{ - fd::{AsFd as _, AsRawFd as _, FromRawFd as _, OwnedFd}, - fs::{AtFlags, Mode, OFlags}, +use crate::{ + BorrowedFd, CStr, CWD, Error, Fat, OwnedFd, Result, Thin, + fs::{AtFlags, Mode, OFlags, Stat}, }; -use crate::{BorrowedFd, CStr, CWD, Error, Fat, Result, Thin}; - // Darwin UAPI `MAXPATHLEN`. pub(super) const PATH_MAX: usize = 1024; const _: () = assert!(libc::PATH_MAX == 1024); +pub(super) const O_RDONLY: u32 = libc::O_RDONLY.cast_unsigned(); +pub(super) const O_RDWR: u32 = libc::O_RDWR.cast_unsigned(); +pub(super) const O_CREAT: u32 = libc::O_CREAT.cast_unsigned(); +pub(super) const O_EXCL: u32 = libc::O_EXCL.cast_unsigned(); +pub(super) const O_CLOEXEC: u32 = libc::O_CLOEXEC.cast_unsigned(); +pub(super) const AT_REMOVEDIR: u32 = libc::AT_REMOVEDIR.cast_unsigned(); +pub(super) type ModeBits = libc::mode_t; +pub(super) const S_IRUSR: ModeBits = libc::S_IRUSR; +pub(super) const S_IWUSR: ModeBits = libc::S_IWUSR; #[expect(clippy::needless_pass_by_value, reason = "CStr is a borrowed value type")] pub(super) fn openat( @@ -29,8 +36,7 @@ pub(super) fn openat( ) }; if fd == -1 { - // SAFETY: libSystem stored this call's error before returning -1. - return Err(Error::from_raw_os_error(unsafe { *libc::__error() })); + return Err(Error::last_os_error()); } // SAFETY: ownership of the newly opened descriptor transfers here. @@ -44,9 +50,25 @@ pub(super) fn unlinkat(dirfd: BorrowedFd<'_>, path: CStr<'_, R>, flags: AtFla 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() })) + if result == -1 { Err(Error::last_os_error()) } else { Ok(()) } +} + +pub(super) fn fstat(fd: BorrowedFd<'_>) -> Result { + let mut raw = core::mem::MaybeUninit::::zeroed(); + // SAFETY: `fd` remains borrowed and `raw` is writable for the call. + if unsafe { libc::fstat(fd.as_raw_fd(), raw.as_mut_ptr()) } == -1 { + return Err(Error::last_os_error()); + } + // SAFETY: a successful `fstat` initialized the complete structure. + let raw = unsafe { raw.assume_init() }; + Ok(Stat { st_size: raw.st_size }) +} + +pub(super) fn ftruncate(fd: BorrowedFd<'_>, len: u64) -> Result<()> { + let len = i64::try_from(len).map_err(|_| Error::OVERFLOW)?; + // SAFETY: `fd` remains borrowed and the length is passed by value. + if unsafe { libc::ftruncate(fd.as_raw_fd(), len) } == -1 { + Err(Error::last_os_error()) } else { Ok(()) } @@ -75,8 +97,7 @@ pub fn fcntl_getpath<'buf>( libc::fcntl(fd.as_raw_fd(), libc::F_GETPATH, buf.as_mut_ptr().cast::()) }; if result == -1 { - // SAFETY: libSystem stored this call's error before returning -1. - return Err(Error::from_raw_os_error(unsafe { *libc::__error() })); + return Err(Error::last_os_error()); } // SAFETY: `F_GETPATH` wrote a NUL-terminated pathname into `buf`. diff --git a/crates/fspy_nostd/src/fs/mod.rs b/crates/fspy_nostd/src/fs/mod.rs index 29197830c..bb661566a 100644 --- a/crates/fspy_nostd/src/fs/mod.rs +++ b/crates/fspy_nostd/src/fs/mod.rs @@ -1,18 +1,18 @@ //! Filesystem calls with caller-owned storage. -#[cfg(target_os = "linux")] +#[cfg(any(target_os = "linux", target_os = "none"))] mod linux; #[cfg(target_os = "macos")] mod mac; -#[cfg(unix)] +#[cfg(any(target_os = "linux", target_os = "none", target_os = "macos"))] mod unix; #[cfg(windows)] mod windows; -#[cfg(unix)] +#[cfg(any(target_os = "linux", target_os = "none", target_os = "macos"))] pub use unix::*; #[cfg(windows)] pub use windows::*; -#[cfg(all(test, unix))] +#[cfg(all(test, any(target_os = "linux", target_os = "macos")))] mod tests; diff --git a/crates/fspy_nostd/src/fs/tests.rs b/crates/fspy_nostd/src/fs/tests.rs index 55de5aeda..87e99d9d3 100644 --- a/crates/fspy_nostd/src/fs/tests.rs +++ b/crates/fspy_nostd/src/fs/tests.rs @@ -31,12 +31,14 @@ fn getcwd_rejects_an_empty_buffer() { #[cfg(target_os = "macos")] #[test] fn fcntl_getpath_returns_descriptor_path() { - use rustix::{ - fd::AsFd as _, - fs::{Mode, OFlags, open}, - }; - - let root = open(c"/", OFlags::RDONLY, Mode::empty()).unwrap(); + let root = super::openat( + crate::CWD, + // SAFETY: the literal is NUL-terminated and static. + unsafe { crate::CStr::::from_ptr(c"/".as_ptr().cast()) }, + super::OFlags::RDONLY, + super::Mode::empty(), + ) + .unwrap(); let mut buf = [MaybeUninit::uninit(); super::PATH_MAX]; let buf_ptr = buf.as_ptr().cast::(); diff --git a/crates/fspy_nostd/src/fs/unix.rs b/crates/fspy_nostd/src/fs/unix.rs index 05279a393..36c7ceeec 100644 --- a/crates/fspy_nostd/src/fs/unix.rs +++ b/crates/fspy_nostd/src/fs/unix.rs @@ -1,10 +1,10 @@ use core::mem::MaybeUninit; -pub use rustix::fs::{AtFlags, Mode, OFlags, fstat, ftruncate}; +use bitflags::bitflags; -#[cfg(target_os = "linux")] +#[cfg(any(target_os = "linux", target_os = "none"))] use super::linux as imp; -#[cfg(target_os = "linux")] +#[cfg(any(target_os = "linux", target_os = "none"))] pub use super::linux::readlinkat; #[cfg(target_os = "macos")] use super::mac as imp; @@ -12,6 +12,47 @@ use super::mac as imp; pub use super::mac::fcntl_getpath; use crate::{BorrowedFd, CStr, Fat, OwnedFd, Result}; +type ModeBits = imp::ModeBits; + +bitflags! { + /// Options accepted by file-opening operations. + #[derive(Clone, Copy, Debug, Eq, PartialEq)] + pub struct OFlags: u32 { + /// Open for reading. + const RDONLY = imp::O_RDONLY; + /// Open for reading and writing. + const RDWR = imp::O_RDWR; + /// Create the file when it does not exist. + const CREATE = imp::O_CREAT; + /// Fail when creating a file that already exists. + const EXCL = imp::O_EXCL; + /// Close the descriptor across `exec`. + const CLOEXEC = imp::O_CLOEXEC; + } + + /// Permission bits used when creating a file. + #[derive(Clone, Copy, Debug, Eq, PartialEq)] + pub struct Mode: ModeBits { + /// Owner read permission. + const RUSR = imp::S_IRUSR; + /// Owner write permission. + const WUSR = imp::S_IWUSR; + } + + /// Options accepted by `*at` filesystem operations. + #[derive(Clone, Copy, Debug, Eq, PartialEq)] + pub struct AtFlags: u32 { + /// Remove a directory rather than a non-directory entry. + const REMOVEDIR = imp::AT_REMOVEDIR; + } +} + +/// Metadata used by fspy's shared-memory backing files. +#[derive(Clone, Copy, Debug)] +pub struct Stat { + pub st_size: i64, +} + /// The platform's maximum pathname size, including the terminating NUL. pub const PATH_MAX: usize = imp::PATH_MAX; @@ -38,6 +79,24 @@ pub fn unlinkat(dirfd: BorrowedFd<'_>, path: CStr<'_, R>, flags: AtFlags) -> imp::unlinkat(dirfd, path, flags) } +/// Returns metadata for an open descriptor. +/// +/// # Errors +/// +/// Returns the error reported by `fstat`. +pub fn fstat(fd: BorrowedFd<'_>) -> Result { + imp::fstat(fd) +} + +/// Sets the length of an open file. +/// +/// # Errors +/// +/// Returns the error reported by `ftruncate`. +pub fn ftruncate(fd: BorrowedFd<'_>, len: u64) -> Result<()> { + imp::ftruncate(fd, len) +} + /// 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/fs/windows.rs b/crates/fspy_nostd/src/fs/windows.rs index 6b29b1d2f..82a4997fe 100644 --- a/crates/fspy_nostd/src/fs/windows.rs +++ b/crates/fspy_nostd/src/fs/windows.rs @@ -94,7 +94,7 @@ pub fn create_file( ) }; if handle == INVALID_HANDLE_VALUE { - Err(crate::windows::last_error()) + Err(crate::Error::last_os_error()) } else { // SAFETY: `CreateFileW` returned a valid, newly owned handle. Ok(unsafe { OwnedHandle::from_raw_handle(handle) }) diff --git a/crates/fspy_nostd/src/io.rs b/crates/fspy_nostd/src/io.rs new file mode 100644 index 000000000..abad39b91 --- /dev/null +++ b/crates/fspy_nostd/src/io.rs @@ -0,0 +1,41 @@ +//! I/O calls with caller-owned buffers. +//! +//! Linux only: these go straight to the kernel with no libc wrapper, so they +//! are usable from a signal handler, a post-`fork()` child, or freestanding +//! injected code. + +use crate::{BorrowedFd, Error, Result}; + +/// Reads into `buf` with a single `read(2)` and returns the initialized byte +/// count. +/// +/// # Errors +/// +/// Returns the error reported by `read`. +pub fn read(fd: BorrowedFd<'_>, buf: &mut [u8]) -> Result { + // SAFETY: `fd` stays borrowed and `buf` is writable for its whole length. + unsafe { + syscalls::syscall!(syscalls::Sysno::read, fd.as_raw_fd(), buf.as_mut_ptr(), buf.len()) + } + .map_err(Error::from) +} + +/// Writes `buf` to `fd` with a single `write(2)` and returns the number of +/// bytes the kernel accepted. +/// +/// A short write is reported as-is, exactly like the syscall; the caller +/// decides whether to write the remainder. +/// +/// # Errors +/// +/// Returns the error reported by `write`. +pub fn write(fd: BorrowedFd<'_>, buf: &[u8]) -> Result { + // SAFETY: `fd` stays borrowed for the call and `buf` is readable for its + // whole length. The kernel receives the descriptor, buffer pointer, and + // length explicitly and returns the accepted byte count. + let written = unsafe { + syscalls::syscall!(syscalls::Sysno::write, fd.as_raw_fd(), buf.as_ptr(), buf.len()) + } + .map_err(Error::from)?; + Ok(written) +} diff --git a/crates/fspy_nostd/src/lib.rs b/crates/fspy_nostd/src/lib.rs index 1541e11c2..8a0ec38cb 100644 --- a/crates/fspy_nostd/src/lib.rs +++ b/crates/fspy_nostd/src/lib.rs @@ -8,47 +8,31 @@ #![cfg_attr(not(test), no_std)] mod c_str; +mod error; +#[cfg(any(target_os = "linux", target_os = "none", target_os = "macos"))] +mod fd; #[cfg(windows)] mod windows; -#[cfg(unix)] +#[cfg(any(target_os = "linux", target_os = "none", target_os = "macos"))] pub mod env; -#[cfg(any(unix, windows))] +#[cfg(any(target_os = "linux", target_os = "none", target_os = "macos", windows))] pub mod fs; -#[cfg(any(unix, windows))] +#[cfg(any(target_os = "linux", target_os = "none"))] +pub mod io; +#[cfg(any(target_os = "linux", target_os = "none", target_os = "macos", windows))] pub mod mm; -#[cfg(unix)] +#[cfg(any(target_os = "linux", target_os = "none", target_os = "macos"))] pub mod param; pub use c_str::{CStr, CStrUnit, Fat, OsCStr, Thin, Units, WideCStr}; +pub use error::{Error, Result}; +#[cfg(any(target_os = "linux", target_os = "none", target_os = "macos"))] +pub use fd::{BorrowedFd, CWD, OwnedFd, RawFd}; #[cfg(windows)] pub use windows::{ BorrowedHandle, OwnedHandle, RawHandle, SecurityAttributes, bool_result, get_module_handle, - last_error, }; - -#[cfg(windows)] -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -#[repr(transparent)] -pub struct Error(u32); - -#[cfg(windows)] -impl Error { - /// Creates an error from a raw Windows error code. - #[must_use] - pub const fn from_raw_os_error(code: u32) -> Self { - Self(code) - } - - /// Returns the raw Windows error code. - #[must_use] - pub const fn raw_os_error(self) -> u32 { - self.0 - } -} - -pub type Result = core::result::Result; - #[cfg(windows)] #[doc(hidden)] pub use windows_sys::w as __wide_cstr_literal; @@ -65,29 +49,3 @@ macro_rules! wide_cstr { } }}; } - -#[cfg(unix)] -pub use rustix::{ - fd::{AsRawFd, BorrowedFd, OwnedFd}, - fs::CWD, - io::Errno as Error, -}; - -// Compile-time proof that rustix uses its raw-syscall backend (`linux_raw`) -// on Linux — and with it, that rustix calls do not go through libc there. -// `rustix::runtime` is gated on that backend (`#[cfg(linux_raw)]`), so this -// reference fails to resolve, failing the whole build, whenever anything -// selects the libc backend instead: the `rustix/use-libc` feature (which any -// crate in the dependency graph can enable through feature unification, where -// no build script could ever see it), `RUSTFLAGS=--cfg=rustix_use_libc`, or a -// target rustix has no raw backend for. Miri also forces the libc backend, so -// it is exempted: it type-checks rather than ships code. -// -// The module's contents are not covered by rustix's stability promise, so a -// rustix upgrade may break this line. If that happens, re-point it at any -// other `rustix::runtime` item — do not delete it: it is the only enforcement -// of the raw-rustix rule above. -#[cfg(all(target_os = "linux", not(miri)))] -const _: () = { - let _ = rustix::runtime::exit_group; -}; diff --git a/crates/fspy_nostd/src/mm.rs b/crates/fspy_nostd/src/mm.rs index ce98132ea..1547a60c3 100644 --- a/crates/fspy_nostd/src/mm.rs +++ b/crates/fspy_nostd/src/mm.rs @@ -1,9 +1,137 @@ //! Memory mappings with no process-runtime dependency. -#[cfg(unix)] -pub use rustix::mm::{MapFlags, MprotectFlags, ProtFlags, mmap, mmap_anonymous, mprotect, munmap}; - +#[cfg(any(target_os = "linux", target_os = "none"))] +mod linux; +#[cfg(target_os = "macos")] +mod mac; #[cfg(windows)] mod windows; + +#[cfg(any(target_os = "linux", target_os = "none"))] +use linux as imp; +#[cfg(target_os = "macos")] +use mac as imp; #[cfg(windows)] pub use windows::*; + +#[cfg(any(target_os = "linux", target_os = "none", target_os = "macos"))] +mod unix { + use core::ffi::c_void; + + use bitflags::bitflags; + + use super::imp; + use crate::{BorrowedFd, Result}; + + bitflags! { + /// Access permitted for mapped pages. + #[derive(Clone, Copy, Debug, Eq, PartialEq)] + pub struct ProtFlags: u32 { + /// Pages may be read. + const READ = imp::PROT_READ; + /// Pages may be written. + const WRITE = imp::PROT_WRITE; + /// Pages may be executed. + const EXEC = imp::PROT_EXEC; + } + + /// Access permitted after changing a mapping's protection. + #[derive(Clone, Copy, Debug, Eq, PartialEq)] + pub struct MprotectFlags: u32 { + /// Pages may be read. + const READ = imp::PROT_READ; + /// Pages may be written. + const WRITE = imp::PROT_WRITE; + /// Pages may be executed. + const EXEC = imp::PROT_EXEC; + } + + /// Sharing behavior for a mapping. + #[derive(Clone, Copy, Debug, Eq, PartialEq)] + pub struct MapFlags: u32 { + /// Changes are shared with mappings of the same object. + const SHARED = imp::MAP_SHARED; + /// Changes are private to this mapping. + const PRIVATE = imp::MAP_PRIVATE; + } + } + + /// Maps bytes from `fd`. + /// + /// # Errors + /// + /// Returns the error reported by the operating system. + /// + /// # Safety + /// + /// The caller must uphold the kernel's address, length, offset, and + /// aliasing requirements for the requested mapping. + pub unsafe fn mmap( + address: *mut c_void, + length: usize, + protection: ProtFlags, + flags: MapFlags, + fd: BorrowedFd<'_>, + offset: u64, + ) -> Result<*mut c_void> { + // SAFETY: forwarded from this function's contract. + unsafe { imp::mmap(address, length, protection, flags, fd, offset) } + } + + /// Creates an anonymous mapping. + /// + /// # Errors + /// + /// Returns the error reported by the operating system. + /// + /// # Safety + /// + /// The caller must uphold the kernel's address, length, and aliasing + /// requirements for the requested mapping. + pub unsafe fn mmap_anonymous( + address: *mut c_void, + length: usize, + protection: ProtFlags, + flags: MapFlags, + ) -> Result<*mut c_void> { + // SAFETY: forwarded from this function's contract. + unsafe { imp::mmap_anonymous(address, length, protection, flags) } + } + + /// Changes protection on mapped pages. + /// + /// # Errors + /// + /// Returns the error reported by the operating system. + /// + /// # Safety + /// + /// `address..address + length` must be a mapped region on which changing + /// protection does not violate live-reference requirements. + pub unsafe fn mprotect( + address: *mut c_void, + length: usize, + protection: MprotectFlags, + ) -> Result<()> { + // SAFETY: forwarded from this function's contract. + unsafe { imp::mprotect(address, length, protection) } + } + + /// Releases a mapping. + /// + /// # Errors + /// + /// Returns the error reported by the operating system. + /// + /// # Safety + /// + /// `address..address + length` must be a mapping owned by the caller with + /// no remaining live references. + pub unsafe fn munmap(address: *mut c_void, length: usize) -> Result<()> { + // SAFETY: forwarded from this function's contract. + unsafe { imp::munmap(address, length) } + } +} + +#[cfg(any(target_os = "linux", target_os = "none", target_os = "macos"))] +pub use unix::*; diff --git a/crates/fspy_nostd/src/mm/linux.rs b/crates/fspy_nostd/src/mm/linux.rs new file mode 100644 index 000000000..a6d2dd6b4 --- /dev/null +++ b/crates/fspy_nostd/src/mm/linux.rs @@ -0,0 +1,76 @@ +use core::{ffi::c_void, ptr}; + +use super::{MapFlags, MprotectFlags, ProtFlags}; +use crate::{BorrowedFd, Error, Result}; + +pub(super) const PROT_READ: u32 = linux_raw_sys::general::PROT_READ; +pub(super) const PROT_WRITE: u32 = linux_raw_sys::general::PROT_WRITE; +pub(super) const PROT_EXEC: u32 = linux_raw_sys::general::PROT_EXEC; +pub(super) const MAP_SHARED: u32 = linux_raw_sys::general::MAP_SHARED; +pub(super) const MAP_PRIVATE: u32 = linux_raw_sys::general::MAP_PRIVATE; + +pub(super) unsafe fn mmap( + address: *mut c_void, + length: usize, + protection: ProtFlags, + flags: MapFlags, + fd: BorrowedFd<'_>, + offset: u64, +) -> Result<*mut c_void> { + // SAFETY: the caller upholds the mapping contract and every argument is + // passed directly to the kernel. + let mapped = unsafe { + syscalls::syscall!( + syscalls::Sysno::mmap, + address, + length, + protection.bits(), + flags.bits(), + fd.as_raw_fd(), + offset + ) + } + .map_err(Error::from)?; + Ok(ptr::with_exposed_provenance_mut(mapped)) +} + +pub(super) unsafe fn mmap_anonymous( + address: *mut c_void, + length: usize, + protection: ProtFlags, + flags: MapFlags, +) -> Result<*mut c_void> { + let flags = flags.bits() | linux_raw_sys::general::MAP_ANONYMOUS; + // SAFETY: the caller upholds the mapping contract and every argument is + // passed directly to the kernel. Anonymous mappings require fd -1. + let mapped = unsafe { + syscalls::syscall!( + syscalls::Sysno::mmap, + address, + length, + protection.bits(), + flags, + -1_i32, + 0_usize + ) + } + .map_err(Error::from)?; + Ok(ptr::with_exposed_provenance_mut(mapped)) +} + +pub(super) unsafe fn mprotect( + address: *mut c_void, + length: usize, + protection: MprotectFlags, +) -> Result<()> { + // SAFETY: the caller upholds the mapped-region contract. + unsafe { syscalls::syscall!(syscalls::Sysno::mprotect, address, length, protection.bits()) } + .map_err(Error::from)?; + Ok(()) +} + +pub(super) unsafe fn munmap(address: *mut c_void, length: usize) -> Result<()> { + // SAFETY: the caller upholds the mapping-ownership contract. + unsafe { syscalls::syscall!(syscalls::Sysno::munmap, address, length) }.map_err(Error::from)?; + Ok(()) +} diff --git a/crates/fspy_nostd/src/mm/mac.rs b/crates/fspy_nostd/src/mm/mac.rs new file mode 100644 index 000000000..90acf731e --- /dev/null +++ b/crates/fspy_nostd/src/mm/mac.rs @@ -0,0 +1,70 @@ +use core::ffi::c_void; + +use super::{MapFlags, MprotectFlags, ProtFlags}; +use crate::{BorrowedFd, Error, Result}; + +pub(super) const PROT_READ: u32 = libc::PROT_READ.cast_unsigned(); +pub(super) const PROT_WRITE: u32 = libc::PROT_WRITE.cast_unsigned(); +pub(super) const PROT_EXEC: u32 = libc::PROT_EXEC.cast_unsigned(); +pub(super) const MAP_SHARED: u32 = libc::MAP_SHARED.cast_unsigned(); +pub(super) const MAP_PRIVATE: u32 = libc::MAP_PRIVATE.cast_unsigned(); + +pub(super) unsafe fn mmap( + address: *mut c_void, + length: usize, + protection: ProtFlags, + flags: MapFlags, + fd: BorrowedFd<'_>, + offset: u64, +) -> Result<*mut c_void> { + let offset = i64::try_from(offset).map_err(|_| Error::OVERFLOW)?; + // SAFETY: the caller upholds the mapping contract; libSystem validates + // every scalar argument. + let mapped = unsafe { + libc::mmap( + address, + length, + protection.bits().cast_signed(), + flags.bits().cast_signed(), + fd.as_raw_fd(), + offset, + ) + }; + if mapped == libc::MAP_FAILED { Err(Error::last_os_error()) } else { Ok(mapped) } +} + +pub(super) unsafe fn mmap_anonymous( + address: *mut c_void, + length: usize, + protection: ProtFlags, + flags: MapFlags, +) -> Result<*mut c_void> { + let flags = flags.bits().cast_signed() | libc::MAP_ANON; + // SAFETY: the caller upholds the mapping contract; anonymous mappings do + // not consume a file descriptor or offset. + let mapped = + unsafe { libc::mmap(address, length, protection.bits().cast_signed(), flags, -1, 0) }; + if mapped == libc::MAP_FAILED { Err(Error::last_os_error()) } else { Ok(mapped) } +} + +pub(super) unsafe fn mprotect( + address: *mut c_void, + length: usize, + protection: MprotectFlags, +) -> Result<()> { + // SAFETY: the caller upholds the mapped-region contract. + if unsafe { libc::mprotect(address, length, protection.bits().cast_signed()) } == -1 { + Err(Error::last_os_error()) + } else { + Ok(()) + } +} + +pub(super) unsafe fn munmap(address: *mut c_void, length: usize) -> Result<()> { + // SAFETY: the caller upholds the mapping-ownership contract. + if unsafe { libc::munmap(address, length) } == -1 { + Err(Error::last_os_error()) + } else { + Ok(()) + } +} diff --git a/crates/fspy_nostd/src/mm/windows.rs b/crates/fspy_nostd/src/mm/windows.rs index df2ce79a1..f2a48eb57 100644 --- a/crates/fspy_nostd/src/mm/windows.rs +++ b/crates/fspy_nostd/src/mm/windows.rs @@ -60,7 +60,7 @@ pub fn virtual_alloc( // and Windows validates the size, allocation type, and protection. let address = unsafe { VirtualAlloc(ptr::null(), size, allocation_type.bits(), protection as u32) }; - NonNull::new(address).ok_or_else(crate::windows::last_error) + NonNull::new(address).ok_or_else(crate::Error::last_os_error) } /// Calls `VirtualFree` with `MEM_RELEASE`, releasing the whole region. @@ -143,7 +143,7 @@ pub fn create_file_mapping( ) }; if mapping.is_null() { - return Err(crate::windows::last_error()); + return Err(crate::Error::last_os_error()); } // SAFETY: `CreateFileMappingW` returned a valid, newly owned handle. Ok(unsafe { OwnedHandle::from_raw_handle(mapping) }) @@ -174,7 +174,7 @@ pub fn map_view_of_file( ) }; let Some(ptr) = core::ptr::NonNull::new(view.Value.cast::()) else { - return Err(crate::windows::last_error()); + return Err(crate::Error::last_os_error()); }; Ok(MappingView { ptr }) } diff --git a/crates/fspy_nostd/src/param.rs b/crates/fspy_nostd/src/param.rs index 0e907689a..08140a890 100644 --- a/crates/fspy_nostd/src/param.rs +++ b/crates/fspy_nostd/src/param.rs @@ -9,16 +9,15 @@ //! vector, the value is learned from syscall behavior that any kernel //! version guarantees: see [`linux::page_size`]. //! -//! On other unix platforms (macOS, where every syscall goes through -//! libSystem by platform contract anyway) it is rustix's `sysconf`, a -//! lock-free read of startup data. +//! On macOS, where every syscall goes through libSystem by platform contract +//! anyway, it calls `sysconf` directly. -#[cfg(target_os = "linux")] +#[cfg(any(target_os = "linux", target_os = "none"))] pub use linux::page_size; -#[cfg(not(target_os = "linux"))] -pub use rustix::param::page_size; +#[cfg(target_os = "macos")] +pub use mac::page_size; -#[cfg(target_os = "linux")] +#[cfg(any(target_os = "linux", target_os = "none"))] mod linux { use core::{ ptr, @@ -70,16 +69,12 @@ mod linux { /// as the first offset the kernel accepts: for a power-of-two page /// size P, the first power of two P divides is P itself. /// - /// Why not `rustix::param::page_size()` (fine on macOS, used there)? - /// Its Linux sources are exactly the ones this crate must not assume: - /// with `use-libc-auxv` it is libc's `sysconf`; without, its lazy init - /// needs `prctl(PR_GET_AUXV)` (kernel 6.4+) or `/proc/self/auxv`, it - /// panics when both are unavailable, and with rustix's `alloc` feature - /// enabled — which Cargo feature unification lets any other rustix - /// user in the build graph turn on for our copy — the `/proc` path - /// heap-allocates, which is forbidden in the contexts this crate - /// serves. The probe has none of those modes: no allocation, no panic, - /// no minimum kernel, and its worst case is 0. + /// Why not an auxiliary-vector helper? Its Linux sources are exactly the + /// ones this crate must not assume: libc startup state, + /// `prctl(PR_GET_AUXV)` (kernel 6.4+), `/proc/self/auxv`, or an initial + /// stack pointer retained from process entry. The probe has none of those + /// requirements: no allocation, no panic, no minimum kernel, and its worst + /// case is 0. #[cold] fn probe_page_size() -> usize { // Large enough that every probe below stays inside the mapping @@ -129,19 +124,24 @@ mod linux { mod tests { use super::*; - /// Cross-validates the probe against rustix's auxv-based answer - /// (a dev-dependency here; CI Linux runners always have `/proc`). - #[test] - fn probe_matches_rustix() { - assert_eq!(page_size(), rustix::param::page_size()); - #[cfg(target_arch = "x86_64")] - assert_eq!(page_size(), 4096); - } - #[test] fn probe_is_stable_and_cached() { assert_eq!(probe_page_size(), page_size()); assert_eq!(page_size(), page_size()); + #[cfg(target_arch = "x86_64")] + assert_eq!(page_size(), 4096); } } } + +#[cfg(target_os = "macos")] +mod mac { + /// Returns the process page size, or zero if libSystem cannot report it. + #[must_use] + pub fn page_size() -> usize { + // SAFETY: `sysconf` accepts this constant by value and accesses no + // caller-owned memory. + let page = unsafe { libc::sysconf(libc::_SC_PAGESIZE) }; + usize::try_from(page).unwrap_or(0) + } +} diff --git a/crates/fspy_nostd/src/windows.rs b/crates/fspy_nostd/src/windows.rs index 1a70e5d4a..29d6781fb 100644 --- a/crates/fspy_nostd/src/windows.rs +++ b/crates/fspy_nostd/src/windows.rs @@ -1,9 +1,6 @@ use core::{ffi::c_void, marker::PhantomData, ptr::NonNull}; -use windows_sys::Win32::{ - Foundation::GetLastError, Security::SECURITY_ATTRIBUTES, - System::LibraryLoader::GetModuleHandleW, -}; +use windows_sys::Win32::{Security::SECURITY_ATTRIBUTES, System::LibraryLoader::GetModuleHandleW}; use crate::{Result, WideCStr}; @@ -103,26 +100,16 @@ impl Drop for OwnedHandle { } } -/// Returns the calling thread's last Win32 error. -/// -/// Call this immediately after the failing Win32 call: anything in between, -/// including drops, can overwrite the thread-local error code. -#[must_use] -pub fn last_error() -> crate::Error { - // SAFETY: `GetLastError` reads thread-local error state. - crate::Error::from_raw_os_error(unsafe { GetLastError() }) -} - /// Converts a Win32 `BOOL` result into a [`Result`]. /// -/// As with [`last_error`], call this immediately after the Win32 call whose -/// result it receives. +/// As with [`crate::Error::last_os_error`], call this immediately after the +/// Win32 call whose result it receives. /// /// # Errors /// /// Returns the calling thread's last Win32 error when `result` is zero. pub fn bool_result(result: i32) -> Result<()> { - if result == 0 { Err(last_error()) } else { Ok(()) } + if result == 0 { Err(crate::Error::last_os_error()) } else { Ok(()) } } /// Returns a handle to the loaded module named by `name`. @@ -137,10 +124,7 @@ pub fn bool_result(result: i32) -> Result<()> { pub fn get_module_handle(name: WideCStr<'_, R>) -> Result> { // SAFETY: `name` remains a valid NUL-terminated wide string for the call. let module = unsafe { GetModuleHandleW(name.as_ptr()) }; - NonNull::new(module).ok_or_else(|| { - // SAFETY: `GetModuleHandleW` just failed on this thread. - crate::Error::from_raw_os_error(unsafe { GetLastError() }) - }) + NonNull::new(module).ok_or_else(crate::Error::last_os_error) } #[cfg(test)] diff --git a/crates/fspy_shm/src/unix.rs b/crates/fspy_shm/src/unix.rs index f2a1075ed..6ae695085 100644 --- a/crates/fspy_shm/src/unix.rs +++ b/crates/fspy_shm/src/unix.rs @@ -63,7 +63,7 @@ pub fn create(path: OsCStr<'_, Thin>, size: usize) -> Result { )?; // Every byte reads as zero because the file is all holes. - if let Err(error) = fspy_nostd::fs::ftruncate(&file, size as u64) { + if let Err(error) = fspy_nostd::fs::ftruncate(file.as_fd(), size as u64) { // Do not hand the caller an unusable partial file to clean up. let _ = remove(path); return Err(error); @@ -93,7 +93,8 @@ pub fn open(path: OsCStr<'_, Thin>) -> Result { // resize cannot make a mapping access invalid memory. // // A regular file's size is never negative. - let size = crate::file_size_to_len(fspy_nostd::fs::fstat(&file)?.st_size.cast_unsigned()); + let size = + crate::file_size_to_len(fspy_nostd::fs::fstat(file.as_fd())?.st_size.cast_unsigned()); Ok(ShmHandle { file, size }) } @@ -127,7 +128,7 @@ impl ShmHandle { len, fspy_nostd::mm::ProtFlags::READ | fspy_nostd::mm::ProtFlags::WRITE, fspy_nostd::mm::MapFlags::SHARED, - &self.file, + self.file.as_fd(), 0, ) }?;