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, ) }?;