diff --git a/Cargo.lock b/Cargo.lock index ec8baa74e..b18dca386 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1340,6 +1340,7 @@ dependencies = [ "libc", "rustix", "syscalls", + "windows-sys 0.61.2", ] [[package]] @@ -1371,6 +1372,7 @@ version = "0.1.0" dependencies = [ "constcat", "fspy_detours_sys", + "fspy_nostd", "fspy_shared", "ntapi", "smallvec 2.0.0-alpha.12", diff --git a/crates/fspy_client_unix/src/convert.rs b/crates/fspy_client_unix/src/convert.rs index 3a7afba18..89045e814 100644 --- a/crates/fspy_client_unix/src/convert.rs +++ b/crates/fspy_client_unix/src/convert.rs @@ -18,7 +18,7 @@ fn get_fd_path(allocator: A, fd: BorrowedFd<'_>) -> nix::Result Ok(Some(path)), - Err(fspy_nostd::Errno::BADF | fspy_nostd::Errno::NOENT) => Ok(None), + Err(fspy_nostd::Error::BADF | fspy_nostd::Error::NOENT) => Ok(None), Err(errno) => Err(nix::errno::Errno::from_raw(errno.raw_os_error())), } } @@ -61,7 +61,7 @@ fn get_fd_path(allocator: A, fd: BorrowedFd<'_>) -> nix::Result Ok(None), + Err(fspy_nostd::Error::BADF | fspy_nostd::Error::NOENT) => Ok(None), Err(errno) => Err(nix::errno::Errno::from_raw(errno.raw_os_error())), } } @@ -72,9 +72,9 @@ pub trait ToAbsolutePath { /// /// The result is a C string so that callers forwarding it to an exec — /// which needs a terminator — cannot be handed unterminated bytes; - /// [`as_bytes`] gives the path without the NUL. + /// [`as_units`] gives the path without the NUL. /// - /// [`as_bytes`]: fspy_nostd::CStr::as_bytes + /// [`as_units`]: fspy_nostd::CStr::as_units /// /// # Errors /// @@ -103,7 +103,7 @@ impl ToAbsolutePath for BorrowedFd<'_> { // SAFETY: a resolved descriptor path carries no interior NUL, and // exactly one was appended above. The storage stays in `allocator` // until it is dropped, which for a per-call arena ends the call. - Ok(Some(unsafe { fspy_nostd::CStr::from_bytes_with_nul_unchecked(path.leak()) })) + Ok(Some(unsafe { fspy_nostd::CStr::from_units_with_nul_unchecked(path.leak()) })) } } @@ -119,7 +119,9 @@ impl PathAt<'_, '_> { #[must_use] pub const unsafe fn borrow_raw(fd: c_int, path: *const c_char) -> Self { // SAFETY: both invariants are upheld by the caller. - Self(unsafe { BorrowedFd::borrow_raw(fd) }, unsafe { fspy_nostd::CStr::from_ptr(path) }) + Self(unsafe { BorrowedFd::borrow_raw(fd) }, unsafe { + fspy_nostd::CStr::from_ptr(path.cast()) + }) } } @@ -132,7 +134,7 @@ impl ToAbsolutePath for PathAt<'_, '_> { Self: 'a, { let counted = self.1.count(); - let pathname = counted.as_bytes(); + let pathname = counted.as_units(); if pathname.starts_with(b"/") { // Already absolute, and already NUL-terminated by the caller. @@ -152,7 +154,7 @@ impl ToAbsolutePath for PathAt<'_, '_> { // interior NUL — both come from C strings or the kernel — and // exactly one was appended above. The storage stays in // `allocator` until it is dropped. - Ok(Some(unsafe { fspy_nostd::CStr::from_bytes_with_nul_unchecked(base.leak()) })) + Ok(Some(unsafe { fspy_nostd::CStr::from_units_with_nul_unchecked(base.leak()) })) } } } diff --git a/crates/fspy_client_unix/src/lib.rs b/crates/fspy_client_unix/src/lib.rs index 934f32c54..d697257e2 100644 --- a/crates/fspy_client_unix/src/lib.rs +++ b/crates/fspy_client_unix/src/lib.rs @@ -147,6 +147,6 @@ impl Client { let Some(abs_path) = path.to_absolute_path(&arena)? else { return Ok(()); }; - self.send(mode, Path::new(OsStr::from_bytes(abs_path.as_bytes()))) + self.send(mode, Path::new(OsStr::from_bytes(abs_path.as_units()))) } } diff --git a/crates/fspy_nostd/Cargo.toml b/crates/fspy_nostd/Cargo.toml index 1d371f732..02cce3f40 100644 --- a/crates/fspy_nostd/Cargo.toml +++ b/crates/fspy_nostd/Cargo.toml @@ -31,6 +31,9 @@ atoi = { version = "3.1.0", default-features = false } rustix = { workspace = true, features = ["runtime"] } syscalls = { workspace = true } +[target.'cfg(windows)'.dependencies] +windows-sys = { workspace = true, features = ["Win32_Foundation", "Win32_System_LibraryLoader"] } + # Cross-validates the page-size probe against rustix's auxv-based answer. [target.'cfg(target_os = "linux")'.dev-dependencies] rustix = { workspace = true, features = ["param"] } diff --git a/crates/fspy_nostd/README.md b/crates/fspy_nostd/README.md index db5dc07b5..3404b5c86 100644 --- a/crates/fspy_nostd/README.md +++ b/crates/fspy_nostd/README.md @@ -2,7 +2,7 @@ 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 and macOS. The crate has no Windows backend yet. +The current implementation supports Linux, macOS, and Windows. ## Execution contexts @@ -58,3 +58,4 @@ Code that needs allocation uses an explicit allocator. [`fspy_nostd_alloc`](../f - `env`: allocation-free process argument and environment iteration. - `fs`: filesystem operations with caller-owned buffers. - `param`: page-size access. +- `get_module_handle`: allocation-free lookup of an already-loaded Windows module. diff --git a/crates/fspy_nostd/src/c_str.rs b/crates/fspy_nostd/src/c_str.rs index c246a0474..265250199 100644 --- a/crates/fspy_nostd/src/c_str.rs +++ b/crates/fspy_nostd/src/c_str.rs @@ -1,6 +1,24 @@ -use core::{ - ffi::c_char, iter::FusedIterator, marker::PhantomData, num::NonZeroUsize, ptr::NonNull, slice, -}; +use core::{iter::FusedIterator, marker::PhantomData, num::NonZeroUsize, ptr::NonNull, slice}; + +mod private { + pub trait Sealed { + const NUL: Self; + } + + impl Sealed for u8 { + const NUL: Self = 0; + } + + impl Sealed for u16 { + const NUL: Self = 0; + } +} + +/// A code unit supported by [`CStr`]. +pub trait CStrUnit: private::Sealed + Copy + Eq {} + +impl CStrUnit for u8 {} +impl CStrUnit for u16 {} /// Marks a [`CStr`] whose length is not known. #[derive(Clone, Copy)] @@ -14,50 +32,53 @@ pub struct Fat { len_with_nul: NonZeroUsize, } -/// A borrowed NUL-terminated string. +/// A borrowed NUL-terminated string of code units. /// /// [`CStr<'_, Thin>`] stores only the string pointer, while /// [`CStr<'_, Fat>`] also stores the length including the terminating NUL. #[derive(Clone, Copy)] -pub struct CStr<'a, R> { - ptr: NonNull, +pub struct CStr<'a, R, U: CStrUnit = u8> { + ptr: NonNull, repr: R, - lifetime: PhantomData<&'a c_char>, + lifetime: PhantomData<&'a U>, } -/// An iterator over the non-NUL bytes of a thin C string. +/// A borrowed NUL-terminated string of `u16` code units. +pub type WideCStr<'a, R> = CStr<'a, R, u16>; + +/// An iterator over the non-NUL code units of a thin C string. #[derive(Clone)] -pub struct Bytes<'a> { - ptr: NonNull, - lifetime: PhantomData<&'a u8>, +pub struct Units<'a, U: CStrUnit> { + ptr: NonNull, + lifetime: PhantomData<&'a U>, } -impl Iterator for Bytes<'_> { - type Item = u8; +impl Iterator for Units<'_, U> { + type Item = U; #[inline] fn next(&mut self) -> Option { // SAFETY: `ptr` starts within a valid C string and is advanced only - // after reading a non-NUL byte, so it remains readable and never moves - // beyond the terminating NUL. + // after reading a non-NUL code unit, so it remains readable and never + // moves beyond the terminating NUL. unsafe { - let byte = self.ptr.read(); - if byte == 0 { + let unit = self.ptr.read(); + if unit == U::NUL { None } else { self.ptr = self.ptr.add(1); - Some(byte) + Some(unit) } } } } -impl FusedIterator for Bytes<'_> {} +impl FusedIterator for Units<'_, U> {} -impl CStr<'_, R> { - /// Returns a pointer to the first byte of this C string. +impl CStr<'_, R, U> { + /// Returns a pointer to the first code unit of this C string. #[must_use] - pub const fn as_ptr(&self) -> *const c_char { + pub const fn as_ptr(&self) -> *const U { self.ptr.as_ptr() } @@ -76,52 +97,53 @@ impl Fat { } } -impl<'a> CStr<'a, Thin> { - /// Creates a thin C string view from a non-null pointer without finding - /// its length. +impl<'a, U: CStrUnit> CStr<'a, Thin, U> { + /// Creates a thin C string view from a non-null code-unit pointer without + /// finding its length. /// /// # Safety /// /// `ptr` must point to an immutable NUL-terminated string that remains /// valid for the lifetime of the returned view. #[must_use] - pub const unsafe fn from_non_null(ptr: NonNull) -> Self { + pub const unsafe fn from_non_null(ptr: NonNull) -> Self { Self { ptr, repr: Thin { _private: () }, lifetime: PhantomData } } - /// Creates a thin C string view without finding its length. + /// Creates a thin C string view from a code-unit pointer without finding + /// its length. /// /// # Safety /// /// `ptr` must be non-null and point to an immutable NUL-terminated string /// that remains valid for the lifetime of the returned view. #[must_use] - pub const unsafe fn from_ptr(ptr: *const c_char) -> Self { + pub const unsafe fn from_ptr(ptr: *const U) -> Self { // SAFETY: the caller guarantees that `ptr` is non-null. let ptr = unsafe { NonNull::new_unchecked(ptr.cast_mut()) }; // SAFETY: the caller guarantees the remaining C string invariants. unsafe { Self::from_non_null(ptr) } } - /// Returns an iterator over the bytes before the terminating NUL. + /// Returns an iterator over the code units before the terminating NUL. #[inline] #[must_use] - pub const fn bytes(self) -> Bytes<'a> { - Bytes { ptr: self.ptr.cast(), lifetime: PhantomData } + pub const fn units(self) -> Units<'a, U> { + Units { ptr: self.ptr, lifetime: PhantomData } } /// Counts through the terminating NUL and returns a length-retaining view. #[inline] #[must_use] - pub fn count(self) -> CStr<'a, Fat> { - let count = self.bytes().count(); + pub fn count(self) -> CStr<'a, Fat, U> { + let count = self.units().count(); CStr { ptr: self.ptr, repr: Fat { // SAFETY: adding the terminator makes the represented length // nonzero, and a valid allocation cannot contain `usize::MAX` - // non-NUL bytes. + // non-NUL code units. len_with_nul: unsafe { NonZeroUsize::new_unchecked(count + 1) }, }, lifetime: PhantomData, @@ -129,41 +151,41 @@ impl<'a> CStr<'a, Thin> { } } -impl<'a> CStr<'a, Fat> { - /// Creates a length-retaining C string from bytes without validation. +impl<'a, U: CStrUnit> CStr<'a, Fat, U> { + /// Creates a length-retaining C string from code units without validation. /// /// # Safety /// - /// `bytes` must end with exactly one NUL byte and contain no other NUL - /// bytes. + /// `units` must end with exactly one NUL code unit and contain no other + /// NUL code units. #[must_use] - pub const unsafe fn from_bytes_with_nul_unchecked(bytes: &'a [u8]) -> Self { + pub const unsafe fn from_units_with_nul_unchecked(units: &'a [U]) -> Self { Self { // SAFETY: a valid C string is nonempty, so its pointer is non-null. - ptr: unsafe { NonNull::new_unchecked(bytes.as_ptr().cast::().cast_mut()) }, + ptr: unsafe { NonNull::new_unchecked(units.as_ptr().cast_mut()) }, repr: Fat { // SAFETY: a valid C string contains at least its terminating NUL. - len_with_nul: unsafe { NonZeroUsize::new_unchecked(bytes.len()) }, + len_with_nul: unsafe { NonZeroUsize::new_unchecked(units.len()) }, }, lifetime: PhantomData, } } - /// Returns the string's bytes without the terminating NUL. + /// Returns the string's code units without the terminating NUL. #[must_use] - pub const fn as_bytes(&self) -> &'a [u8] { - let bytes = self.as_bytes_with_nul(); - bytes.split_at(bytes.len() - 1).0 + pub const fn as_units(&self) -> &'a [U] { + let units = self.as_units_with_nul(); + units.split_at(units.len() - 1).0 } - /// Returns the string's bytes, including the terminating NUL. + /// Returns the string's code units, including the terminating NUL. #[must_use] - pub const fn as_bytes_with_nul(&self) -> &'a [u8] { + pub const fn as_units_with_nul(&self) -> &'a [U] { // SAFETY: this view carries the exact initialized C string length. - unsafe { slice::from_raw_parts(self.ptr.as_ptr().cast(), self.len_with_nul()) } + unsafe { slice::from_raw_parts(self.ptr.as_ptr(), self.len_with_nul()) } } - /// Returns the number of bytes including the terminating NUL. + /// Returns the number of code units including the terminating NUL. #[must_use] pub const fn len_with_nul(&self) -> usize { self.repr.len_with_nul() @@ -174,42 +196,58 @@ impl<'a> CStr<'a, Fat> { mod tests { use core::{mem::size_of, ptr::NonNull}; - use super::{CStr, Fat, Thin}; + use super::{CStr, Fat, Thin, WideCStr}; #[test] fn representations_retain_the_expected_metadata() { // SAFETY: the input contains one trailing NUL. - let fat = unsafe { CStr::::from_bytes_with_nul_unchecked(b"abc\0") }; + let fat = unsafe { CStr::::from_units_with_nul_unchecked(b"abc\0") }; // SAFETY: the input contains one trailing NUL. - let counted = unsafe { CStr::::from_ptr(c"abc".as_ptr()) }.count(); + let counted = unsafe { CStr::::from_ptr(c"abc".as_ptr().cast()) }.count(); assert_eq!(size_of::>(), size_of::<*const u8>()); assert_eq!(size_of::>(), size_of::<(*const u8, usize)>()); + assert_eq!(size_of::>(), size_of::<*const u16>()); + assert_eq!(size_of::>(), size_of::<(*const u16, usize)>()); assert_eq!(fat.len_with_nul(), 4); - assert_eq!(fat.as_bytes(), b"abc"); - assert_eq!(fat.as_bytes_with_nul(), b"abc\0"); - assert_eq!(counted.as_bytes_with_nul(), fat.as_bytes_with_nul()); + assert_eq!(fat.as_units(), b"abc"); + assert_eq!(fat.as_units_with_nul(), b"abc\0"); + assert_eq!(counted.as_units_with_nul(), fat.as_units_with_nul()); } #[test] fn thin_view_accepts_a_checked_non_null_pointer() { - let ptr = NonNull::new(c"abc".as_ptr().cast_mut()).unwrap(); + let ptr = NonNull::new(c"abc".as_ptr().cast_mut().cast()).unwrap(); // SAFETY: the literal is an immutable NUL-terminated string. let thin = unsafe { CStr::::from_non_null(ptr) }; - assert!(thin.bytes().eq(b"abc".iter().copied())); + assert!(thin.units().eq(b"abc".iter().copied())); } #[test] - fn thin_bytes_exclude_the_nul_and_remain_fused() { - let mut bytes = { + fn thin_units_exclude_the_nul_and_remain_fused() { + let mut units = { // SAFETY: the literal is NUL-terminated and outlives the iterator. - let thin = unsafe { CStr::::from_ptr(c"abc".as_ptr()) }; - thin.bytes() + let thin = unsafe { CStr::::from_ptr(c"abc".as_ptr().cast()) }; + thin.units() }; - assert_eq!(bytes.by_ref().collect::>(), b"abc"); - assert_eq!(bytes.next(), None); - assert_eq!(bytes.next(), None); + assert_eq!(units.by_ref().collect::>(), b"abc"); + assert_eq!(units.next(), None); + assert_eq!(units.next(), None); + } + + #[test] + fn wide_strings_iterate_and_retain_code_unit_lengths() { + let units = [u16::from(b'a'), u16::from(b'b'), 0]; + // SAFETY: the input contains one trailing NUL. + let fat = unsafe { WideCStr::::from_units_with_nul_unchecked(&units) }; + // SAFETY: the same input is immutable and NUL-terminated. + let thin = unsafe { WideCStr::::from_ptr(units.as_ptr()) }; + + assert!(thin.units().eq([u16::from(b'a'), u16::from(b'b')])); + assert_eq!(thin.count().as_units_with_nul(), units); + assert_eq!(fat.as_units(), [u16::from(b'a'), u16::from(b'b')]); + assert_eq!(fat.len_with_nul(), 3); } } diff --git a/crates/fspy_nostd/src/env/linux.rs b/crates/fspy_nostd/src/env/linux.rs index 47fad46cc..827b9da9e 100644 --- a/crates/fspy_nostd/src/env/linux.rs +++ b/crates/fspy_nostd/src/env/linux.rs @@ -5,7 +5,7 @@ use bstr::{BStr, ByteSlice as _}; use rustix::fs::{Mode, OFlags}; use super::Entry; -use crate::{CStr, CWD, Errno, Fat, Result}; +use crate::{CStr, CWD, Error, Fat, Result}; #[derive(Clone, Copy)] struct Bounds { @@ -39,17 +39,17 @@ impl<'a> Iterator for RangeIter<'a> { // SAFETY: splitting at the first NUL produces a nonempty slice with // exactly one trailing NUL. - Some(unsafe { CStr::from_bytes_with_nul_unchecked(entry) }) + Some(unsafe { CStr::from_units_with_nul_unchecked(entry) }) } } fn split_fat(entry: CStr<'static, Fat>) -> Entry { - let Some((name, value)) = entry.as_bytes_with_nul().split_once_str(b"=") else { - return (BStr::new(entry.as_bytes()), None); + let Some((name, value)) = entry.as_units_with_nul().split_once_str(b"=") else { + return (BStr::new(entry.as_units()), None); }; // SAFETY: splitting preserves the single trailing NUL. - let value = unsafe { CStr::from_bytes_with_nul_unchecked(value) }; + let value = unsafe { CStr::from_units_with_nul_unchecked(value) }; (BStr::new(name), Some(value)) } @@ -102,13 +102,13 @@ impl Current { const unsafe fn slice_from_range<'a>(start: usize, end: usize) -> Result<&'a [u8]> { let len = match end.checked_sub(start) { Some(len) if len <= isize::MAX.cast_unsigned() => len, - _ => return Err(Errno::INVAL), + _ => return Err(Error::INVAL), }; let Some(len) = NonZeroUsize::new(len) else { return Ok(&[]); }; let Some(start) = NonZeroUsize::new(start) else { - return Err(Errno::INVAL); + return Err(Error::INVAL); }; let start = ptr::with_exposed_provenance::(start.get()); @@ -180,16 +180,16 @@ fn read_bounds() -> Result { loop { let Some(remaining) = stat.get_mut(initialized..) else { - return Err(Errno::OVERFLOW); + return Err(Error::OVERFLOW); }; if remaining.is_empty() { - return Err(Errno::OVERFLOW); + return Err(Error::OVERFLOW); } let Some(read) = NonZeroUsize::new(rustix::io::read(&fd, remaining)?) else { break; }; - initialized = initialized.checked_add(read.get()).ok_or(Errno::OVERFLOW)?; + initialized = initialized.checked_add(read.get()).ok_or(Error::OVERFLOW)?; } parse_bounds(&stat[..initialized]) @@ -199,17 +199,17 @@ fn parse_bounds(stat: &[u8]) -> Result { // `comm` (field 2) may itself contain spaces, newlines, and `)`. The // kernel-added delimiter is the last `)` because every later field is // numeric except for the one-byte process state. - let comm_end = stat.iter().rposition(|byte| *byte == b')').ok_or(Errno::INVAL)?; - let (_, comm_and_fields) = stat.split_at_checked(comm_end).ok_or(Errno::INVAL)?; - let (_, fields) = comm_and_fields.split_first().ok_or(Errno::INVAL)?; + let comm_end = stat.iter().rposition(|byte| *byte == b')').ok_or(Error::INVAL)?; + let (_, comm_and_fields) = stat.split_at_checked(comm_end).ok_or(Error::INVAL)?; + let (_, fields) = comm_and_fields.split_first().ok_or(Error::INVAL)?; let mut fields = fields.split(u8::is_ascii_whitespace).filter(|field| !field.is_empty()); // With field 3 at index zero, arg_start (field 48) is index 45, followed // by arg_end, env_start, and env_end. - let arg_start = fields.nth(45).ok_or(Errno::INVAL)?; - let arg_end = fields.next().ok_or(Errno::INVAL)?; - let env_start = fields.next().ok_or(Errno::INVAL)?; - let env_end = fields.next().ok_or(Errno::INVAL)?; + let arg_start = fields.nth(45).ok_or(Error::INVAL)?; + let arg_end = fields.next().ok_or(Error::INVAL)?; + let env_start = fields.next().ok_or(Error::INVAL)?; + let env_end = fields.next().ok_or(Error::INVAL)?; Ok(Bounds { arg_start: parse_usize(arg_start)?, arg_end: parse_usize(arg_end)?, @@ -220,10 +220,10 @@ fn parse_bounds(stat: &[u8]) -> Result { fn parse_usize(bytes: &[u8]) -> Result { let (value, used) = usize::from_radix_10_checked(bytes); - let value = value.ok_or(Errno::OVERFLOW)?; - let used = NonZeroUsize::new(used).ok_or(Errno::INVAL)?; + let value = value.ok_or(Error::OVERFLOW)?; + let used = NonZeroUsize::new(used).ok_or(Error::INVAL)?; if used.get() != bytes.len() { - return Err(Errno::INVAL); + return Err(Error::INVAL); } Ok(value) } @@ -239,14 +239,14 @@ mod tests { let current = Current { args: ARGS, envs: ENVS }; let mut args = current.args(); - assert_eq!(args.next().unwrap().as_bytes(), b"program"); - assert_eq!(args.next().unwrap().as_bytes(), b"--flag"); + assert_eq!(args.next().unwrap().as_units(), b"program"); + assert_eq!(args.next().unwrap().as_units(), b"--flag"); assert!(args.next().is_none()); let mut envs = current.envs(); let (name, value) = envs.next().unwrap(); assert_eq!(name.as_bytes(), b"FIRST"); - assert_eq!(value.unwrap().as_bytes(), b"one"); + assert_eq!(value.unwrap().as_units(), b"one"); let (name, value) = envs.next().unwrap(); assert_eq!(name.as_bytes(), b"INVALID"); @@ -254,41 +254,41 @@ mod tests { let (name, value) = envs.next().unwrap(); assert_eq!(name.as_bytes(), b"EMPTY"); - assert_eq!(value.unwrap().as_bytes(), b""); + assert_eq!(value.unwrap().as_units(), b""); let (name, value) = envs.next().unwrap(); assert_eq!(name.as_bytes(), b"LAST"); - assert_eq!(value.unwrap().as_bytes(), b"a=b"); + assert_eq!(value.unwrap().as_units(), b"a=b"); assert!(envs.next().is_none()); } #[test] fn entry_splits_only_at_the_first_equals() { // SAFETY: this static byte string contains exactly one trailing NUL. - let entry = unsafe { CStr::::from_bytes_with_nul_unchecked(b"NAME=a=b\0") }; + let entry = unsafe { CStr::::from_units_with_nul_unchecked(b"NAME=a=b\0") }; let (name, value) = split_fat(entry); let value = value.unwrap(); assert_eq!(name.as_bytes(), b"NAME"); - assert_eq!(value.as_bytes(), b"a=b"); - assert_eq!(value.as_bytes_with_nul(), b"a=b\0"); + assert_eq!(value.as_units(), b"a=b"); + assert_eq!(value.as_units_with_nul(), b"a=b\0"); } #[test] fn entry_accepts_an_empty_value() { // SAFETY: this static byte string contains exactly one trailing NUL. - let entry = unsafe { CStr::::from_bytes_with_nul_unchecked(b"EMPTY=\0") }; + let entry = unsafe { CStr::::from_units_with_nul_unchecked(b"EMPTY=\0") }; let (name, value) = split_fat(entry); let value = value.unwrap(); assert_eq!(name.as_bytes(), b"EMPTY"); - assert_eq!(value.as_bytes_with_nul(), b"\0"); + assert_eq!(value.as_units_with_nul(), b"\0"); } #[test] fn entry_without_equals_has_no_value() { // SAFETY: this static byte string contains exactly one trailing NUL. - let entry = unsafe { CStr::::from_bytes_with_nul_unchecked(b"INVALID\0") }; + let entry = unsafe { CStr::::from_units_with_nul_unchecked(b"INVALID\0") }; let (name, value) = split_fat(entry); assert_eq!(name.as_bytes(), b"INVALID"); @@ -318,11 +318,11 @@ mod tests { #[test] fn parses_only_complete_in_range_decimal_fields() { assert_eq!(parse_usize(b"42"), Ok(42)); - assert_eq!(parse_usize(b""), Err(Errno::INVAL)); - assert_eq!(parse_usize(b"42x"), Err(Errno::INVAL)); + assert_eq!(parse_usize(b""), Err(Error::INVAL)); + assert_eq!(parse_usize(b"42x"), Err(Error::INVAL)); assert_eq!( parse_usize(b"9999999999999999999999999999999999999999999"), - Err(Errno::OVERFLOW) + Err(Error::OVERFLOW) ); } @@ -333,10 +333,10 @@ mod tests { let current = unsafe { current().unwrap() }; let argv_zero = current.args().next().unwrap(); - assert_eq!(argv_zero.as_bytes(), std::env::args_os().next().unwrap().as_encoded_bytes()); + assert_eq!(argv_zero.as_units(), std::env::args_os().next().unwrap().as_encoded_bytes()); let path = current.envs().find(|(name, _)| name.as_bytes() == b"PATH").unwrap().1.unwrap(); - assert_eq!(path.as_bytes(), std::env::var_os("PATH").unwrap().as_encoded_bytes()); + assert_eq!(path.as_units(), std::env::var_os("PATH").unwrap().as_encoded_bytes()); } #[test] diff --git a/crates/fspy_nostd/src/env/mac/current.rs b/crates/fspy_nostd/src/env/mac/current.rs index 1fd696da2..aac768cde 100644 --- a/crates/fspy_nostd/src/env/mac/current.rs +++ b/crates/fspy_nostd/src/env/mac/current.rs @@ -88,9 +88,9 @@ mod tests { let current = unsafe { current().unwrap() }; let argv_zero = current.args().next().unwrap(); - assert_eq!(argv_zero.as_bytes(), std::env::args_os().next().unwrap().as_encoded_bytes()); + assert_eq!(argv_zero.as_units(), std::env::args_os().next().unwrap().as_encoded_bytes()); let path = current.envs().find(|(name, _)| name.as_bytes() == b"PATH").unwrap().1.unwrap(); - assert_eq!(path.as_bytes(), std::env::var_os("PATH").unwrap().as_encoded_bytes()); + assert_eq!(path.as_units(), std::env::var_os("PATH").unwrap().as_encoded_bytes()); } } diff --git a/crates/fspy_nostd/src/env/mac/thin.rs b/crates/fspy_nostd/src/env/mac/thin.rs index 2b97cf151..e7b056c92 100644 --- a/crates/fspy_nostd/src/env/mac/thin.rs +++ b/crates/fspy_nostd/src/env/mac/thin.rs @@ -22,7 +22,7 @@ impl Iterator for PointerIter { self.current = unsafe { self.current.add(1) }; // SAFETY: every non-null pointer in these arrays names an immutable, // NUL-terminated string under the constructor's caller contract. - Some(unsafe { CStr::::from_non_null(entry) }) + Some(unsafe { CStr::::from_non_null(entry.cast()) }) } } @@ -59,7 +59,7 @@ fn split_thin(entry: CStr<'static, Thin>) -> Entry { let start = entry.as_ptr().cast::(); let mut len = 0usize; - for byte in entry.bytes() { + for byte in entry.units() { match byte { b'=' => { // SAFETY: the scan established the name prefix. @@ -75,7 +75,7 @@ fn split_thin(entry: CStr<'static, Thin>) -> Entry { } } - // SAFETY: `Bytes` stopped at the NUL after this prefix. + // SAFETY: `Units` stopped at the NUL after this prefix. let name: &'static [u8] = unsafe { slice::from_raw_parts(start, len) }; (BStr::new(name), None) } @@ -131,9 +131,9 @@ mod tests { #[test] fn thin_entries_distinguish_missing_and_empty_values() { // SAFETY: both literals are NUL-terminated and live for the views. - let missing = unsafe { CStr::::from_ptr(c"INVALID".as_ptr()) }; + let missing = unsafe { CStr::::from_ptr(c"INVALID".as_ptr().cast()) }; // SAFETY: as above. - let empty = unsafe { CStr::::from_ptr(c"EMPTY=".as_ptr()) }; + let empty = unsafe { CStr::::from_ptr(c"EMPTY=".as_ptr().cast()) }; let (name, value) = split_thin(missing); assert_eq!(name.as_bytes(), b"INVALID"); @@ -141,7 +141,7 @@ mod tests { let (name, value) = split_thin(empty); assert_eq!(name.as_bytes(), b"EMPTY"); - assert_eq!(value.unwrap().count().as_bytes(), b""); + assert_eq!(value.unwrap().count().as_units(), b""); } #[test] @@ -149,7 +149,7 @@ mod tests { // SAFETY: this test does not mutate the argument or environment arrays // while their iterators or borrowed entries are live. let argv_zero = unsafe { args() }.next().unwrap().count(); - assert_eq!(argv_zero.as_bytes(), std::env::args_os().next().unwrap().as_encoded_bytes()); + assert_eq!(argv_zero.as_units(), std::env::args_os().next().unwrap().as_encoded_bytes()); // SAFETY: as above. let path = unsafe { envs() } @@ -158,6 +158,6 @@ mod tests { .1 .unwrap() .count(); - assert_eq!(path.as_bytes(), std::env::var_os("PATH").unwrap().as_encoded_bytes()); + assert_eq!(path.as_units(), std::env::var_os("PATH").unwrap().as_encoded_bytes()); } } diff --git a/crates/fspy_nostd/src/fs/linux.rs b/crates/fspy_nostd/src/fs/linux.rs index c9a4689ca..d905c9495 100644 --- a/crates/fspy_nostd/src/fs/linux.rs +++ b/crates/fspy_nostd/src/fs/linux.rs @@ -1,6 +1,6 @@ use core::{mem::MaybeUninit, slice}; -use crate::{AsRawFd as _, BorrowedFd, CStr, Errno, Fat, Result, Thin}; +use crate::{AsRawFd as _, BorrowedFd, CStr, Error, Fat, Result, Thin}; // Linux UAPI `PATH_MAX`. pub(super) const PATH_MAX: usize = 4096; @@ -33,7 +33,7 @@ pub fn readlinkat<'buf>( buf.len(), ) } - .map_err(|errno| Errno::from_raw_os_error(errno.into_raw()))?; + .map_err(|errno| Error::from_raw_os_error(errno.into_raw()))?; // SAFETY: the syscall initialized exactly this prefix. Ok(unsafe { slice::from_raw_parts(buf.as_ptr().cast(), initialized) }) @@ -47,10 +47,10 @@ pub(super) fn getcwd(buf: &mut [MaybeUninit]) -> Result> { // its terminating NUL. let initialized = unsafe { syscalls::syscall2(syscalls::Sysno::getcwd, buf.as_mut_ptr().addr(), buf.len()) } - .map_err(|errno| Errno::from_raw_os_error(errno.into_raw()))?; + .map_err(|errno| Error::from_raw_os_error(errno.into_raw()))?; // SAFETY: the syscall initialized this prefix through its terminating NUL. let bytes = unsafe { slice::from_raw_parts(buf.as_ptr().cast(), initialized) }; // SAFETY: the syscall returned one NUL-terminated pathname. - Ok(unsafe { CStr::from_bytes_with_nul_unchecked(bytes) }) + Ok(unsafe { CStr::from_units_with_nul_unchecked(bytes) }) } diff --git a/crates/fspy_nostd/src/fs/mac.rs b/crates/fspy_nostd/src/fs/mac.rs index 53283f80f..cea3d2283 100644 --- a/crates/fspy_nostd/src/fs/mac.rs +++ b/crates/fspy_nostd/src/fs/mac.rs @@ -5,7 +5,7 @@ use rustix::{ fs::{Mode, OFlags}, }; -use crate::{BorrowedFd, CStr, CWD, Errno, Fat, Result, Thin}; +use crate::{BorrowedFd, CStr, CWD, Error, Fat, Result, Thin}; pub(super) const PATH_MAX: usize = libc::PATH_MAX as usize; @@ -21,14 +21,14 @@ fn openat( let fd = unsafe { libc::openat( dirfd.as_raw_fd(), - path.as_ptr(), + path.as_ptr().cast(), flags.bits().cast_signed(), libc::c_uint::from(mode.bits()), ) }; if fd == -1 { // SAFETY: libSystem stored this call's error before returning -1. - return Err(Errno::from_raw_os_error(unsafe { *libc::__error() })); + return Err(Error::from_raw_os_error(unsafe { *libc::__error() })); } // SAFETY: ownership of the newly opened descriptor transfers here. @@ -59,7 +59,7 @@ pub fn fcntl_getpath<'buf>( }; if result == -1 { // SAFETY: libSystem stored this call's error before returning -1. - return Err(Errno::from_raw_os_error(unsafe { *libc::__error() })); + return Err(Error::from_raw_os_error(unsafe { *libc::__error() })); } // SAFETY: `F_GETPATH` wrote a NUL-terminated pathname into `buf`. @@ -81,13 +81,13 @@ pub(super) fn getcwd(buf: &mut [MaybeUninit]) -> Result> { fn getcwd_small(buf: &mut [MaybeUninit]) -> Result> { let mut scratch = [MaybeUninit::uninit(); PATH_MAX]; let initialized = getcwd_full(&mut scratch)?.len_with_nul(); - let buf = buf.get_mut(..initialized).ok_or(Errno::RANGE)?; + let buf = buf.get_mut(..initialized).ok_or(Error::RANGE)?; buf.copy_from_slice(&scratch[..initialized]); // SAFETY: `getcwd_full` initialized this copied C string prefix. let bytes = unsafe { slice::from_raw_parts(buf.as_ptr().cast(), buf.len()) }; // SAFETY: upheld by the initialized prefix above. - Ok(unsafe { CStr::from_bytes_with_nul_unchecked(bytes) }) + Ok(unsafe { CStr::from_units_with_nul_unchecked(bytes) }) } /// The allocation-free fast path from Apple's [`getcwd`]: ask an open `.` @@ -106,7 +106,7 @@ fn getcwd_small(buf: &mut [MaybeUninit]) -> Result> { /// [`getcwd`]: https://github.com/apple-oss-distributions/Libc/blob/Libc-1752.120.2/gen/FreeBSD/getcwd.c#L62-L138 fn getcwd_full(buf: &mut [MaybeUninit; PATH_MAX]) -> Result> { // SAFETY: the byte string contains one trailing NUL. - let dot_path = unsafe { CStr::::from_bytes_with_nul_unchecked(b".\0") }; + let dot_path = unsafe { CStr::::from_units_with_nul_unchecked(b".\0") }; let fd = openat(CWD, dot_path, OFlags::RDONLY | OFlags::CLOEXEC, Mode::empty())?; let path = fcntl_getpath(fd.as_fd(), buf)?; diff --git a/crates/fspy_nostd/src/fs/tests.rs b/crates/fspy_nostd/src/fs/tests.rs index 61161e74c..55de5aeda 100644 --- a/crates/fspy_nostd/src/fs/tests.rs +++ b/crates/fspy_nostd/src/fs/tests.rs @@ -2,7 +2,7 @@ use core::mem::MaybeUninit; use std::os::unix::ffi::OsStrExt as _; use super::getcwd; -use crate::Errno; +use crate::Error; #[test] #[expect( @@ -18,14 +18,14 @@ fn getcwd_returns_current_directory() { assert_eq!(actual.as_ptr().cast::(), buf_ptr); assert_eq!( - actual.as_bytes_with_nul()[..actual.len_with_nul() - 1], + actual.as_units_with_nul()[..actual.len_with_nul() - 1], *expected.as_os_str().as_bytes() ); } #[test] fn getcwd_rejects_an_empty_buffer() { - assert!(matches!(getcwd(&mut []), Err(Errno::RANGE))); + assert!(matches!(getcwd(&mut []), Err(Error::RANGE))); } #[cfg(target_os = "macos")] @@ -44,7 +44,7 @@ fn fcntl_getpath_returns_descriptor_path() { assert_eq!(path.as_ptr().cast::(), buf_ptr); let path = path.count(); - assert_eq!(path.as_bytes_with_nul(), b"/\0"); + assert_eq!(path.as_units_with_nul(), b"/\0"); } #[cfg(target_os = "linux")] @@ -52,7 +52,7 @@ fn fcntl_getpath_returns_descriptor_path() { fn readlinkat_returns_the_initialized_target() { let mut buf = [MaybeUninit::uninit(); super::PATH_MAX]; // SAFETY: the literal is NUL-terminated and lives for the call. - let path = unsafe { crate::CStr::::from_ptr(c"/proc/self/exe".as_ptr()) }; + let path = unsafe { crate::CStr::::from_ptr(c"/proc/self/exe".as_ptr().cast()) }; let target = super::readlinkat(crate::CWD, path, &mut buf).unwrap(); diff --git a/crates/fspy_nostd/src/lib.rs b/crates/fspy_nostd/src/lib.rs index b46d2f6c9..6cfba529a 100644 --- a/crates/fspy_nostd/src/lib.rs +++ b/crates/fspy_nostd/src/lib.rs @@ -1,30 +1,73 @@ -//! Unix syscall wrappers that are safe to call where libc is not: in signal -//! handlers, in the child of `fork()` in a multithreaded process, and before -//! libc has finished initializing. +//! Low-level operations for fspy code that cannot use the normal process +//! runtime. //! -//! The fspy preload library interposes libc functions that POSIX declares -//! async-signal-safe (`open`, `stat`, `execve`, ...), so its code runs in all -//! of those places, where libc's own machinery — locks, lazy initialization, -//! malloc — is off limits. Everything this crate exposes follows three rules: -//! kernel calls bypass libc on Linux, operations use no locks or hidden state, -//! and nothing allocates globally. See README.md for the full approach. - -// Compile as an empty crate on non-unix targets: the crate backs the unix -// preload library. -#![cfg(unix)] +//! This includes Unix signal handlers and post-`fork()` children, Windows +//! loader callbacks, process startup, and the injected Linux runtime. See +//! README.md for the platform-specific guarantees. + #![cfg_attr(not(test), no_std)] mod c_str; +#[cfg(windows)] +mod windows; + +#[cfg(unix)] pub mod env; +#[cfg(unix)] pub mod fs; +#[cfg(unix)] pub mod mm; +#[cfg(unix)] pub mod param; -pub use c_str::{Bytes, CStr, Fat, Thin}; +pub use c_str::{CStr, CStrUnit, Fat, Thin, Units, WideCStr}; +#[cfg(windows)] +pub use windows::get_module_handle; + +#[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; + +/// Creates a static [`WideCStr`] from a UTF-8 string literal. +#[cfg(windows)] +#[macro_export] +macro_rules! wide_cstr { + ($literal:literal) => {{ + // SAFETY: `windows-sys` transcodes the literal into static UTF-16 + // storage and appends its NUL terminator. + unsafe { + $crate::WideCStr::<$crate::Thin>::from_ptr($crate::__wide_cstr_literal!($literal)) + } + }}; +} + +#[cfg(unix)] pub use rustix::{ fd::{AsRawFd, BorrowedFd}, fs::CWD, - io::{Errno, Errno as Error, Result}, + io::Errno as Error, }; // Compile-time proof that rustix uses its raw-syscall backend (`linux_raw`) diff --git a/crates/fspy_nostd/src/windows.rs b/crates/fspy_nostd/src/windows.rs new file mode 100644 index 000000000..b2b4d369f --- /dev/null +++ b/crates/fspy_nostd/src/windows.rs @@ -0,0 +1,33 @@ +use core::{ffi::c_void, ptr::NonNull}; + +use windows_sys::Win32::{Foundation::GetLastError, System::LibraryLoader::GetModuleHandleW}; + +use crate::{Result, WideCStr}; + +/// Returns a handle to the loaded module named by `name`. +/// +/// This does not load the module or increment its loader reference count. The +/// returned handle must not be passed to `FreeLibrary`. +/// +/// # Errors +/// +/// Returns the Windows error reported when no matching loaded module exists. +#[expect(clippy::needless_pass_by_value, reason = "CStr is a borrowed value type")] +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() }) + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn finds_a_loaded_system_module() { + let _module = get_module_handle(crate::wide_cstr!("kernel32.dll")).unwrap(); + } +} diff --git a/crates/fspy_nostd_alloc/src/c_string.rs b/crates/fspy_nostd_alloc/src/c_string.rs index 917d23cdf..29b00bf67 100644 --- a/crates/fspy_nostd_alloc/src/c_string.rs +++ b/crates/fspy_nostd_alloc/src/c_string.rs @@ -19,7 +19,7 @@ impl CString { #[must_use] pub unsafe fn from_vec_with_nul_unchecked(bytes: Vec) -> Self { // SAFETY: upheld by the caller. - let repr = unsafe { CStr::::from_bytes_with_nul_unchecked(&bytes) }.into_repr(); + let repr = unsafe { CStr::::from_units_with_nul_unchecked(&bytes) }.into_repr(); let (bytes, _len, capacity, allocator) = bytes.into_raw_parts_with_alloc(); let bytes = ptr::slice_from_raw_parts_mut(bytes.cast::>(), capacity); @@ -70,7 +70,7 @@ impl CString { #[must_use] pub fn as_c_str(&self) -> CStr<'_, Fat> { // SAFETY: the initialized prefix described by `repr` is a C string. - unsafe { CStr::from_bytes_with_nul_unchecked(self.as_bytes_with_nul()) } + unsafe { CStr::from_units_with_nul_unchecked(self.as_bytes_with_nul()) } } /// Returns the contents of this C string without the terminating NUL. diff --git a/crates/fspy_nostd_alloc/src/fs.rs b/crates/fspy_nostd_alloc/src/fs.rs index ab0ecfd9e..54d0cbef7 100644 --- a/crates/fspy_nostd_alloc/src/fs.rs +++ b/crates/fspy_nostd_alloc/src/fs.rs @@ -7,7 +7,7 @@ use allocator_api2::{alloc::Allocator, vec::Vec}; use fspy_nostd::Thin; #[cfg(target_os = "linux")] use fspy_nostd::{BorrowedFd, CStr}; -use fspy_nostd::{Errno, Fat, Result}; +use fspy_nostd::{Error, Fat, Result}; use crate::CString; @@ -16,11 +16,11 @@ use crate::CString; /// /// # Errors /// -/// Returns [`Errno::NOMEM`] if storage cannot be allocated, or the error from +/// Returns [`Error::NOMEM`] if storage cannot be allocated, or the error from /// [`fspy_nostd::fs::getcwd`]. pub fn getcwd(allocator: A) -> Result> { let mut bytes = Vec::new_in(allocator); - bytes.try_reserve_exact(fspy_nostd::fs::PATH_MAX).map_err(|_| Errno::NOMEM)?; + bytes.try_reserve_exact(fspy_nostd::fs::PATH_MAX).map_err(|_| Error::NOMEM)?; let initialized = fspy_nostd::fs::getcwd(&mut bytes.spare_capacity_mut()[..fspy_nostd::fs::PATH_MAX])? .len_with_nul(); @@ -41,7 +41,7 @@ pub fn getcwd(allocator: A) -> Result> { /// /// # Errors /// -/// Returns [`Errno::NOMEM`] if storage cannot be allocated, or the error from +/// Returns [`Error::NOMEM`] if storage cannot be allocated, or the error from /// [`fspy_nostd::fs::readlinkat`]. #[cfg(target_os = "linux")] pub fn readlinkat( @@ -50,7 +50,7 @@ pub fn readlinkat( path: CStr<'_, Thin>, ) -> Result> { let mut bytes = Vec::new_in(allocator); - bytes.try_reserve_exact(fspy_nostd::fs::PATH_MAX).map_err(|_| Errno::NOMEM)?; + bytes.try_reserve_exact(fspy_nostd::fs::PATH_MAX).map_err(|_| Error::NOMEM)?; loop { let capacity = bytes.capacity(); @@ -63,8 +63,8 @@ pub fn readlinkat( } // The length remains zero, so reserve the desired total capacity. - let next_capacity = capacity.checked_mul(2).ok_or(Errno::NOMEM)?; - bytes.try_reserve_exact(next_capacity).map_err(|_| Errno::NOMEM)?; + let next_capacity = capacity.checked_mul(2).ok_or(Error::NOMEM)?; + bytes.try_reserve_exact(next_capacity).map_err(|_| Error::NOMEM)?; } } @@ -74,7 +74,7 @@ pub fn readlinkat( /// /// # Errors /// -/// Returns [`Errno::NOMEM`] if storage cannot be allocated, or the error from +/// Returns [`Error::NOMEM`] if storage cannot be allocated, or the error from /// [`fspy_nostd::fs::fcntl_getpath`]. #[cfg(target_os = "macos")] pub fn fcntl_getpath( @@ -97,7 +97,7 @@ fn path_buffer( Box::<[core::mem::MaybeUninit; fspy_nostd::fs::PATH_MAX], A>::try_new_uninit_in( allocator, ) - .map_err(|_| Errno::NOMEM)?; + .map_err(|_| Error::NOMEM)?; // SAFETY: an array of `MaybeUninit` requires no initialization. Ok(unsafe { bytes.assume_init() }) @@ -112,9 +112,9 @@ mod tests { let path = super::getcwd(Global).unwrap(); let mut buffer = [core::mem::MaybeUninit::uninit(); fspy_nostd::fs::PATH_MAX]; let expected = fspy_nostd::fs::getcwd(&mut buffer).unwrap(); - let expected = expected.as_bytes_with_nul(); + let expected = expected.as_units_with_nul(); - assert_eq!(path.as_c_str().as_bytes_with_nul(), expected); + assert_eq!(path.as_c_str().as_units_with_nul(), expected); assert_eq!(path.as_bytes(), &expected[..expected.len() - 1]); assert_eq!(path.as_bytes_with_nul(), expected); assert_eq!(path.into_bytes().as_slice(), &expected[..expected.len() - 1]); @@ -133,15 +133,16 @@ mod tests { let root = unsafe { fspy_nostd::BorrowedFd::borrow_raw(root.as_raw_fd()) }; let path = super::fcntl_getpath(Global, root).unwrap(); - assert_eq!(path.as_c_str().count().as_bytes_with_nul(), b"/\0"); + assert_eq!(path.as_c_str().count().as_units_with_nul(), b"/\0"); } #[cfg(target_os = "linux")] #[test] fn readlinkat_allocates_the_complete_target() { // SAFETY: the literal is NUL-terminated and lives for the call. - let path = - unsafe { fspy_nostd::CStr::::from_ptr(c"/proc/self/exe".as_ptr()) }; + let path = unsafe { + fspy_nostd::CStr::::from_ptr(c"/proc/self/exe".as_ptr().cast()) + }; let target = super::readlinkat(Global, fspy_nostd::CWD, path).unwrap(); assert_eq!( diff --git a/crates/fspy_preload_unix/src/interceptions/access.rs b/crates/fspy_preload_unix/src/interceptions/access.rs index f6b008282..32f743e81 100644 --- a/crates/fspy_preload_unix/src/interceptions/access.rs +++ b/crates/fspy_preload_unix/src/interceptions/access.rs @@ -10,7 +10,7 @@ intercept!(access(64): unsafe extern "C" fn(pathname: *const c_char, mode: c_int unsafe extern "C" fn access(pathname: *const c_char, mode: c_int) -> c_int { // SAFETY: pathname is a valid C string pointer provided by the caller of the interposed function unsafe { - handle_open(fspy_nostd::CStr::from_ptr(pathname), AccessMode::READ); + handle_open(fspy_nostd::CStr::from_ptr(pathname.cast()), AccessMode::READ); } // SAFETY: calling the original libc access() with the same arguments forwarded from the interposed function unsafe { access::original()(pathname, mode) } diff --git a/crates/fspy_preload_unix/src/interceptions/dirent.rs b/crates/fspy_preload_unix/src/interceptions/dirent.rs index d6f33ce94..ae2278450 100644 --- a/crates/fspy_preload_unix/src/interceptions/dirent.rs +++ b/crates/fspy_preload_unix/src/interceptions/dirent.rs @@ -17,7 +17,7 @@ unsafe extern "C" fn scandir( compar: *const c_void, ) -> c_int { // SAFETY: dirname is a valid C string pointer provided by the caller of the interposed function - unsafe { handle_open(fspy_nostd::CStr::from_ptr(dirname), AccessMode::READ_DIR) } + unsafe { handle_open(fspy_nostd::CStr::from_ptr(dirname.cast()), AccessMode::READ_DIR) } // SAFETY: calling the original libc scandir() with the same arguments forwarded from the interposed function unsafe { scandir::original()(dirname, namelist, select, compar) } } @@ -39,7 +39,7 @@ mod macos_only { compar: *const c_void, ) -> c_int { // SAFETY: dirname is a valid C string pointer provided by the caller of the interposed function - unsafe { handle_open(fspy_nostd::CStr::from_ptr(dirname), AccessMode::READ_DIR) }; + unsafe { handle_open(fspy_nostd::CStr::from_ptr(dirname.cast()), AccessMode::READ_DIR) }; // SAFETY: calling the original libc scandir_b() with the same arguments forwarded from the interposed function unsafe { scandir_b::original()(dirname, namelist, select, compar) } } @@ -82,7 +82,7 @@ unsafe extern "C" fn fdopendir(fd: c_int) -> *mut DIR { intercept!(opendir(64): unsafe extern "C" fn (*const c_char) -> *mut DIR); unsafe extern "C" fn opendir(dir_name: *const c_char) -> *mut DIR { // SAFETY: dir_name is a valid C string pointer provided by the caller of the interposed function - unsafe { handle_open(fspy_nostd::CStr::from_ptr(dir_name), AccessMode::READ_DIR) }; + unsafe { handle_open(fspy_nostd::CStr::from_ptr(dir_name.cast()), AccessMode::READ_DIR) }; // SAFETY: calling the original libc opendir() with the same arguments forwarded from the interposed function unsafe { opendir::original()(dir_name) } } diff --git a/crates/fspy_preload_unix/src/interceptions/open.rs b/crates/fspy_preload_unix/src/interceptions/open.rs index 43891513b..e4e6138a2 100644 --- a/crates/fspy_preload_unix/src/interceptions/open.rs +++ b/crates/fspy_preload_unix/src/interceptions/open.rs @@ -28,7 +28,7 @@ type Mode = c_int; intercept!(open(64): unsafe extern "C" fn(*const c_char, c_int, args: ...) -> c_int); unsafe extern "C" fn open(path: *const c_char, flags: c_int, mut args: ...) -> c_int { // SAFETY: path is a valid C string pointer provided by the caller of the interposed function - unsafe { handle_open(fspy_nostd::CStr::from_ptr(path), OpenFlags(flags)) }; + unsafe { handle_open(fspy_nostd::CStr::from_ptr(path.cast()), OpenFlags(flags)) }; if has_mode_arg(flags) { // SAFETY: when O_CREAT or O_TMPFILE is set, a mode_t argument is required by the open() contract let mode: Mode = unsafe { args.next_arg() }; @@ -67,7 +67,7 @@ intercept!(open_nocancel: unsafe extern "C" fn(*const c_char, c_int, ...) -> c_i #[cfg(target_os = "macos")] unsafe extern "C" fn open_nocancel(path: *const c_char, flags: c_int, mut args: ...) -> c_int { // SAFETY: path is a valid C string pointer provided by the caller of open$NOCANCEL - unsafe { handle_open(fspy_nostd::CStr::from_ptr(path), OpenFlags(flags)) }; + unsafe { handle_open(fspy_nostd::CStr::from_ptr(path.cast()), OpenFlags(flags)) }; if has_mode_arg(flags) { // SAFETY: O_CREAT requires a mode argument, matching the open$NOCANCEL contract let mode: Mode = unsafe { args.next_arg() }; @@ -104,7 +104,7 @@ unsafe extern "C" fn openat_nocancel( intercept!(fopen(64): unsafe extern "C" fn(path: *const c_char, mode: *const c_char) -> *mut FILE); unsafe extern "C" fn fopen(path: *const c_char, mode: *const c_char) -> *mut libc::FILE { // SAFETY: path and mode are valid C string pointers provided by the caller of the interposed function - unsafe { handle_open(fspy_nostd::CStr::from_ptr(path), ModeStr(mode)) }; + unsafe { handle_open(fspy_nostd::CStr::from_ptr(path.cast()), ModeStr(mode)) }; // SAFETY: calling the original libc fopen() with the same arguments forwarded from the interposed function unsafe { fopen::original()(path, mode) } } @@ -116,7 +116,7 @@ unsafe extern "C" fn freopen( stream: *mut FILE, ) -> *mut FILE { // SAFETY: path and mode are valid C string pointers provided by the caller of the interposed function - unsafe { handle_open(fspy_nostd::CStr::from_ptr(path), ModeStr(mode)) }; + unsafe { handle_open(fspy_nostd::CStr::from_ptr(path.cast()), ModeStr(mode)) }; // SAFETY: calling the original libc freopen() with the same arguments forwarded from the interposed function unsafe { freopen::original()(path, mode, stream) } } diff --git a/crates/fspy_preload_unix/src/interceptions/spawn/exec/mod.rs b/crates/fspy_preload_unix/src/interceptions/spawn/exec/mod.rs index 57dd902c4..aac3faf0c 100644 --- a/crates/fspy_preload_unix/src/interceptions/spawn/exec/mod.rs +++ b/crates/fspy_preload_unix/src/interceptions/spawn/exec/mod.rs @@ -212,7 +212,7 @@ mod linux_only { // pointer by construction rather than by convention. handle_exec( ExecResolveConfig::search_path_disabled(), - abs_path.as_ptr(), + abs_path.as_ptr().cast(), argv.cast(), envp.cast(), ) diff --git a/crates/fspy_preload_unix/src/interceptions/stat.rs b/crates/fspy_preload_unix/src/interceptions/stat.rs index 534a39dc9..ea34ceff6 100644 --- a/crates/fspy_preload_unix/src/interceptions/stat.rs +++ b/crates/fspy_preload_unix/src/interceptions/stat.rs @@ -12,7 +12,7 @@ intercept!(stat(64): unsafe extern "C" fn(path: *const c_char, buf: *mut stat_st unsafe extern "C" fn stat(path: *const c_char, buf: *mut stat_struct) -> c_int { // SAFETY: path is a valid C string pointer provided by the caller of the interposed function unsafe { - handle_open(fspy_nostd::CStr::from_ptr(path), AccessMode::READ); + handle_open(fspy_nostd::CStr::from_ptr(path.cast()), AccessMode::READ); } // SAFETY: calling the original libc stat() with the same arguments forwarded from the interposed function unsafe { stat::original()(path, buf) } @@ -23,7 +23,7 @@ unsafe extern "C" fn lstat(path: *const c_char, buf: *mut stat_struct) -> c_int // TODO: add accessmode ReadNoFollow // SAFETY: path is a valid C string pointer provided by the caller of the interposed function unsafe { - handle_open(fspy_nostd::CStr::from_ptr(path), AccessMode::READ); + handle_open(fspy_nostd::CStr::from_ptr(path.cast()), AccessMode::READ); } // SAFETY: calling the original libc lstat() with the same arguments forwarded from the interposed function unsafe { lstat::original()(path, buf) } diff --git a/crates/fspy_preload_windows/Cargo.toml b/crates/fspy_preload_windows/Cargo.toml index 9dd1c50cc..ba4426aca 100644 --- a/crates/fspy_preload_windows/Cargo.toml +++ b/crates/fspy_preload_windows/Cargo.toml @@ -12,6 +12,7 @@ crate-type = ["cdylib"] wincode = { workspace = true } constcat = { workspace = true } fspy_detours_sys = { workspace = true } +fspy_nostd = { workspace = true } fspy_shared = { workspace = true } ntapi = { workspace = true } smallvec = { workspace = true } diff --git a/crates/fspy_preload_windows/src/windows/detour.rs b/crates/fspy_preload_windows/src/windows/detour.rs index d7a8c8a30..48b10805c 100644 --- a/crates/fspy_preload_windows/src/windows/detour.rs +++ b/crates/fspy_preload_windows/src/windows/detour.rs @@ -1,10 +1,13 @@ -use std::{cell::UnsafeCell, ffi::CStr, mem::transmute_copy, os::raw::c_void, ptr::null_mut}; +use std::{ + cell::UnsafeCell, + ffi::{CStr, c_void}, + mem::transmute_copy, + ptr::{NonNull, null_mut}, +}; use fspy_detours_sys::{DetourAttach, DetourDetach}; -use winapi::{ - shared::minwindef::HMODULE, - um::libloaderapi::{GetProcAddress, LoadLibraryA}, -}; +use fspy_nostd::{get_module_handle, wide_cstr}; +use winapi::um::libloaderapi::GetProcAddress; use winsafe::SysResult; use crate::windows::winapi_utils::ck_long; @@ -53,24 +56,17 @@ pub struct DetourAny { } pub struct AttachContext { - kernelbase: HMODULE, - kernel32: HMODULE, - ntdll: HMODULE, + kernelbase: Option>, + kernel32: NonNull, + ntdll: NonNull, } impl AttachContext { - #[must_use] - pub fn new() -> Self { - // SAFETY: LoadLibraryA is safe to call with valid C string pointers to system DLLs - let kernelbase = unsafe { LoadLibraryA(c"kernelbase".as_ptr()) }; - // SAFETY: LoadLibraryA is safe to call with valid C string pointers to system DLLs - let kernel32 = unsafe { LoadLibraryA(c"kernel32".as_ptr()) }; - // SAFETY: LoadLibraryA is safe to call with valid C string pointers to system DLLs - let ntdll = unsafe { LoadLibraryA(c"ntdll".as_ptr()) }; - assert_ne!(kernelbase, null_mut()); - assert_ne!(kernel32, null_mut()); - assert_ne!(ntdll, null_mut()); - Self { kernelbase, kernel32, ntdll } + pub fn new() -> fspy_nostd::Result { + let kernelbase = get_module_handle(wide_cstr!("kernelbase.dll")).ok(); + let kernel32 = get_module_handle(wide_cstr!("kernel32.dll"))?; + let ntdll = get_module_handle(wide_cstr!("ntdll.dll"))?; + Ok(Self { kernelbase, kernel32, ntdll }) } } @@ -80,28 +76,35 @@ impl DetourAny { pub unsafe fn attach(&self, ctx: &AttachContext) -> SysResult<()> { // SAFETY: dereferencing pointer to static CStr symbol name let symbol_name = unsafe { *self.symbol_name }.as_ptr(); - // SAFETY: GetProcAddress FFI call with valid module handle and symbol name - let symbol_in_kernelbase = unsafe { GetProcAddress(ctx.kernelbase, symbol_name) }; - if symbol_in_kernelbase.is_null() { - // SAFETY: reading target pointer to check if symbol was already resolved - if unsafe { *self.target }.is_null() { - // dynamic symbol - look up from kernel32 or ntdll + if let Some(kernelbase) = ctx.kernelbase { + // SAFETY: GetProcAddress FFI call with valid module handle and symbol name + let symbol_in_kernelbase = + unsafe { GetProcAddress(kernelbase.as_ptr().cast(), symbol_name) }; + if !symbol_in_kernelbase.is_null() { + // Stub symbols in kernel32 and other DLLs forward here. Hooking + // the shared implementation covers every stub. + // https://github.com/microsoft/Detours/issues/328#issuecomment-2494147615 + // SAFETY: writing resolved symbol address to target pointer for Detours API + unsafe { *self.target = symbol_in_kernelbase.cast() }; + } + } + // SAFETY: reading target pointer to check if the statically imported + // target was resolved or KernelBase supplied an implementation. + if unsafe { *self.target }.is_null() { + // Dynamic symbols may come from kernel32 or ntdll. + // SAFETY: GetProcAddress FFI call with valid module handle and symbol name + let symbol_in_kernel32 = + unsafe { GetProcAddress(ctx.kernel32.as_ptr().cast(), symbol_name) }; + if symbol_in_kernel32.is_null() { // SAFETY: GetProcAddress FFI call with valid module handle and symbol name - let symbol_in_kernel32 = unsafe { GetProcAddress(ctx.kernel32, symbol_name) }; - if symbol_in_kernel32.is_null() { - // SAFETY: GetProcAddress FFI call with valid module handle and symbol name - let symbol_in_ntdll = unsafe { GetProcAddress(ctx.ntdll, symbol_name) }; - // SAFETY: writing resolved symbol address to target pointer - unsafe { *self.target = symbol_in_ntdll.cast() }; - } else { - // SAFETY: writing resolved symbol address to target pointer - unsafe { *self.target = symbol_in_kernel32.cast() }; - } + let symbol_in_ntdll = + unsafe { GetProcAddress(ctx.ntdll.as_ptr().cast(), symbol_name) }; + // SAFETY: writing resolved symbol address to target pointer + unsafe { *self.target = symbol_in_ntdll.cast() }; + } else { + // SAFETY: writing resolved symbol address to target pointer + unsafe { *self.target = symbol_in_kernel32.cast() }; } - } else { - // stub symbol: https://github.com/microsoft/Detours/issues/328#issuecomment-2494147615 - // SAFETY: writing resolved symbol address to target pointer for Detours API - unsafe { *self.target = symbol_in_kernelbase.cast() }; } // SAFETY: reading target pointer to check if symbol was resolved if unsafe { *self.target }.is_null() { diff --git a/crates/fspy_preload_windows/src/windows/mod.rs b/crates/fspy_preload_windows/src/windows/mod.rs index 0d1612a39..d187ed000 100644 --- a/crates/fspy_preload_windows/src/windows/mod.rs +++ b/crates/fspy_preload_windows/src/windows/mod.rs @@ -49,7 +49,10 @@ fn dll_main(_hinstance: HINSTANCE, reason: u32) -> winsafe::SysResult<()> { // SAFETY: setting the global client during single-threaded DLL_PROCESS_ATTACH unsafe { set_global_client(client) }; - let ctx = AttachContext::new(); + let ctx = AttachContext::new().map_err(|error| { + // SAFETY: every raw Windows error code is a valid `ERROR` value. + unsafe { winsafe::co::ERROR::from_raw(error.raw_os_error()) } + })?; // SAFETY: FFI call to begin a Detours transaction ck_long(unsafe { DetourTransactionBegin() })?; diff --git a/crates/fspy_shared_unix/src/payload.rs b/crates/fspy_shared_unix/src/payload.rs index f9547c48a..cd196c960 100644 --- a/crates/fspy_shared_unix/src/payload.rs +++ b/crates/fspy_shared_unix/src/payload.rs @@ -62,7 +62,7 @@ pub fn decode_payload_from_env( ) -> anyhow::Result { let Some(encoded_string) = envs.find_map(|(name, value)| { if AsRef::<[u8]>::as_ref(name) == PAYLOAD_ENV_NAME.as_bytes() { - value.map(|value| BString::from(value.as_bytes())) + value.map(|value| BString::from(value.as_units())) } else { None }