diff --git a/.github/workflows/fspy-benchmark.yml b/.github/workflows/fspy-benchmark.yml index c09c7ba08..db3ec05e4 100644 --- a/.github/workflows/fspy-benchmark.yml +++ b/.github/workflows/fspy-benchmark.yml @@ -79,9 +79,8 @@ jobs: if: github.event_name == 'pull_request' working-directory: ${{ env.BASE_DIR }} env: - # Share the head build's target directory: the crates.io graph is - # fingerprinted by package and profile, not by workspace, so only the - # fspy path crates build twice. + # Share the head build's target directory so registry dependencies + # only build once. Workspace artifacts are removed below. CARGO_TARGET_DIR: ${{ github.workspace }}/target run: | # Both fspy revisions must be measured by identical code, and the @@ -94,6 +93,9 @@ jobs: exe="" [[ "$RUNNER_OS" == "Windows" ]] && exe=.exe cp "$CARGO_TARGET_DIR/release/fspy_benchmark_launcher$exe" "$RUNNER_TEMP/fspy-base-launcher$exe" + # A baseline worktree created after the head can leave newer + # workspace artifacts that Cargo considers fresh for the head. + cargo clean --release --workspace - name: Run benchmark run: | diff --git a/Cargo.lock b/Cargo.lock index 72842724d..0d9049cd6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1313,6 +1313,7 @@ dependencies = [ "fspy_shared_unix", "libc", "nix 0.31.2", + "sigsafe", "sigsafe_alloc", "wincode", ] diff --git a/crates/fspy_preload_unix/Cargo.toml b/crates/fspy_preload_unix/Cargo.toml index be676c1d9..95b3ea016 100644 --- a/crates/fspy_preload_unix/Cargo.toml +++ b/crates/fspy_preload_unix/Cargo.toml @@ -17,6 +17,7 @@ fspy_shared = { workspace = true } fspy_shared_unix = { workspace = true } libc = { workspace = true } nix = { workspace = true, features = ["signal", "fs", "socket", "mman", "time"] } +sigsafe = { workspace = true } sigsafe_alloc = { workspace = true } [build-dependencies] diff --git a/crates/fspy_preload_unix/src/client/convert.rs b/crates/fspy_preload_unix/src/client/convert.rs index c42854b32..5a9f469cf 100644 --- a/crates/fspy_preload_unix/src/client/convert.rs +++ b/crates/fspy_preload_unix/src/client/convert.rs @@ -2,7 +2,7 @@ use std::ffi::CString; use std::{ ffi::{CStr, OsStr}, - os::{fd::RawFd, unix::ffi::OsStrExt as _}, + os::unix::ffi::OsStrExt as _, path::PathBuf, }; @@ -10,13 +10,16 @@ use bstr::{BStr, ByteSlice}; use fspy_shared::ipc::AccessMode; use libc::{c_char, c_int}; use nix::unistd::getcwd; +use sigsafe::{AsRawFd as _, BorrowedFd, CWD}; #[cfg(target_os = "linux")] -fn get_fd_path(fd: RawFd) -> nix::Result> { - if fd == libc::AT_FDCWD { +fn get_fd_path(fd: BorrowedFd<'_>) -> nix::Result> { + if fd.as_raw_fd() == CWD.as_raw_fd() { return Ok(Some(getcwd()?)); } - match nix::fcntl::readlink(CString::new(format!("/proc/self/fd/{fd}")).unwrap().as_c_str()) { + match nix::fcntl::readlink( + CString::new(format!("/proc/self/fd/{}", fd.as_raw_fd())).unwrap().as_c_str(), + ) { Ok(path) => Ok(Some(path.into())), Err(nix::Error::EBADF | nix::Error::ENOENT) => Ok(None), // invalid fd or no such file (Most likely a stdio fd) Err(e) => Err(e), @@ -24,16 +27,15 @@ fn get_fd_path(fd: RawFd) -> nix::Result> { } #[cfg(target_os = "macos")] -fn get_fd_path(fd: RawFd) -> nix::Result> { - if fd == libc::AT_FDCWD { +fn get_fd_path(fd: BorrowedFd<'_>) -> nix::Result> { + if fd.as_raw_fd() == CWD.as_raw_fd() { return Ok(Some(getcwd()?)); } let mut path = std::path::PathBuf::new(); - match nix::fcntl::fcntl( - // SAFETY: fd is a valid file descriptor provided by the caller, and the borrow does not outlive this function call - unsafe { std::os::fd::BorrowedFd::borrow_raw(fd) }, - nix::fcntl::FcntlArg::F_GETPATH(&mut path), - ) { + // SAFETY: this std view has the same descriptor and lifetime as the + // rustix view accepted by this function. + let fd = unsafe { std::os::fd::BorrowedFd::borrow_raw(fd.as_raw_fd()) }; + match nix::fcntl::fcntl(fd, nix::fcntl::FcntlArg::F_GETPATH(&mut path)) { Ok(_) => Ok(Some(path)), Err(nix::Error::EBADF | nix::Error::ENOENT) => Ok(None), // invalid fd or no such file (Most likely a stdio fd) Err(e) => Err(e), @@ -41,32 +43,44 @@ fn get_fd_path(fd: RawFd) -> nix::Result> { } pub trait ToAbsolutePath { - unsafe fn to_absolute_path) -> nix::Result>( + fn to_absolute_path) -> nix::Result>( self, f: F, ) -> nix::Result; } -pub struct Fd(pub c_int); -impl ToAbsolutePath for Fd { - unsafe fn to_absolute_path) -> nix::Result>( +impl ToAbsolutePath for BorrowedFd<'_> { + fn to_absolute_path) -> nix::Result>( self, f: F, ) -> nix::Result { - let path = get_fd_path(self.0)?; - f(path.as_ref().map(|p| p.as_os_str().as_bytes().as_bstr())) + let path = get_fd_path(self)?; + f(path.as_ref().map(|path| path.as_os_str().as_bytes().as_bstr())) } } -pub struct PathAt(pub c_int, pub *const c_char); +pub struct PathAt<'fd, 'path>(pub BorrowedFd<'fd>, pub sigsafe::CStr<'path, sigsafe::Thin>); + +impl PathAt<'_, '_> { + /// Borrows raw directory-descriptor and pathname arguments. + /// + /// # Safety + /// + /// `fd` must remain valid while the returned value is used, and `path` + /// must point to a valid NUL-terminated string. + 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 { sigsafe::CStr::from_ptr(path) }) + } +} -impl ToAbsolutePath for PathAt { - unsafe fn to_absolute_path) -> nix::Result>( +impl ToAbsolutePath for PathAt<'_, '_> { + fn to_absolute_path) -> nix::Result>( self, f: F, ) -> nix::Result { - // SAFETY: self.1 is a non-null pointer to a valid null-terminated C string, as guaranteed by the libc calling convention - let pathname = unsafe { CStr::from_ptr(self.1) }.to_bytes().as_bstr(); + // SAFETY: self.1 is a valid NUL-terminated string. + let pathname = unsafe { CStr::from_ptr(self.1.as_ptr()) }.to_bytes().as_bstr(); if pathname.first().copied() == Some(b'/') { f(pathname.into()) @@ -82,13 +96,12 @@ impl ToAbsolutePath for PathAt { } } -impl ToAbsolutePath for *const c_char { - unsafe fn to_absolute_path) -> nix::Result>( +impl ToAbsolutePath for sigsafe::CStr<'_, sigsafe::Thin> { + fn to_absolute_path) -> nix::Result>( self, f: F, ) -> nix::Result { - // SAFETY: delegates to PathAt::to_absolute_path with AT_FDCWD and the caller-provided C string pointer - unsafe { PathAt(libc::AT_FDCWD, self).to_absolute_path(f) } + PathAt(CWD, self).to_absolute_path(f) } } diff --git a/crates/fspy_preload_unix/src/client/mod.rs b/crates/fspy_preload_unix/src/client/mod.rs index daae12f5a..aac742ebc 100644 --- a/crates/fspy_preload_unix/src/client/mod.rs +++ b/crates/fspy_preload_unix/src/client/mod.rs @@ -109,15 +109,12 @@ impl Client { ) -> anyhow::Result<()> { // SAFETY: mode contains a valid pointer (if ModeStr) or a plain value, as provided by the caller let mode = unsafe { mode.to_access_mode() }; - // SAFETY: path contains valid pointers to C strings/file descriptors, as provided by the caller - let () = unsafe { - path.to_absolute_path(|abs_path| { - let Some(abs_path) = abs_path else { - return Ok(Ok(())); - }; - Ok(self.send(mode, Path::new(OsStr::from_bytes(abs_path)))) - }) - }??; + let () = path.to_absolute_path(|abs_path| { + let Some(abs_path) = abs_path else { + return Ok(Ok(())); + }; + Ok(self.send(mode, Path::new(OsStr::from_bytes(abs_path)))) + })??; Ok(()) } diff --git a/crates/fspy_preload_unix/src/interceptions/access.rs b/crates/fspy_preload_unix/src/interceptions/access.rs index 0cae39220..f4f9e5f20 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(pathname, AccessMode::READ); + handle_open(sigsafe::CStr::from_ptr(pathname), AccessMode::READ); } // SAFETY: calling the original libc access() with the same arguments forwarded from the interposed function unsafe { access::original()(pathname, mode) } @@ -25,7 +25,7 @@ unsafe extern "C" fn faccessat( ) -> c_int { // SAFETY: dirfd and pathname are valid arguments provided by the caller of the interposed function unsafe { - handle_open(PathAt(dirfd, pathname), AccessMode::READ); + handle_open(PathAt::borrow_raw(dirfd, pathname), AccessMode::READ); } // SAFETY: calling the original libc faccessat() with the same arguments forwarded from the interposed function unsafe { faccessat::original()(dirfd, pathname, mode, flags) } diff --git a/crates/fspy_preload_unix/src/interceptions/dirent.rs b/crates/fspy_preload_unix/src/interceptions/dirent.rs index d2b11d39a..c676f84b3 100644 --- a/crates/fspy_preload_unix/src/interceptions/dirent.rs +++ b/crates/fspy_preload_unix/src/interceptions/dirent.rs @@ -1,10 +1,8 @@ use fspy_shared::ipc::AccessMode; use libc::{DIR, c_char, c_int, c_long, c_void}; +use sigsafe::BorrowedFd; -use crate::{ - client::{convert::Fd, handle_open}, - macros::intercept, -}; +use crate::{client::handle_open, macros::intercept}; intercept!(scandir(64): unsafe extern "C" fn ( dirname: *const c_char, @@ -19,14 +17,14 @@ 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(dirname, AccessMode::READ_DIR) } + unsafe { handle_open(sigsafe::CStr::from_ptr(dirname), 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) } } #[cfg(target_os = "macos")] mod macos_only { - use super::{AccessMode, Fd, c_char, c_int, c_void, handle_open, intercept}; + use super::{AccessMode, BorrowedFd, c_char, c_int, c_void, handle_open, intercept}; intercept!(scandir_b: unsafe extern "C" fn ( dirname: *const c_char, @@ -41,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(dirname, AccessMode::READ_DIR) }; + unsafe { handle_open(sigsafe::CStr::from_ptr(dirname), 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) } } @@ -54,7 +52,7 @@ mod macos_only { basep: *mut i64, ) -> isize { // SAFETY: fd is a valid file descriptor provided by the caller of __getdirentries64 - unsafe { handle_open(Fd(fd), AccessMode::READ_DIR) }; + unsafe { handle_open(BorrowedFd::borrow_raw(fd), AccessMode::READ_DIR) }; // SAFETY: calling the original libc __getdirentries64() with the same arguments forwarded from the interposed function unsafe { __getdirentries64::original()(fd, buf, buf_len, basep) } } @@ -68,7 +66,7 @@ unsafe extern "C" fn getdirentries( basep: *mut c_long, ) -> c_int { // SAFETY: fd is a valid file descriptor provided by the caller of the interposed function - unsafe { handle_open(Fd(fd), AccessMode::READ_DIR) }; + unsafe { handle_open(BorrowedFd::borrow_raw(fd), AccessMode::READ_DIR) }; // SAFETY: calling the original libc getdirentries() with the same arguments forwarded from the interposed function unsafe { getdirentries::original()(fd, buf, nbytes, basep) } } @@ -76,7 +74,7 @@ unsafe extern "C" fn getdirentries( intercept!(fdopendir(64): unsafe extern "C" fn (fd: c_int) -> *mut DIR); unsafe extern "C" fn fdopendir(fd: c_int) -> *mut DIR { // SAFETY: fd is a valid file descriptor provided by the caller of the interposed function - unsafe { handle_open(Fd(fd), AccessMode::READ_DIR) }; + unsafe { handle_open(BorrowedFd::borrow_raw(fd), AccessMode::READ_DIR) }; // SAFETY: calling the original libc fdopendir() with the same arguments forwarded from the interposed function unsafe { fdopendir::original()(fd) } } @@ -84,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(dir_name, AccessMode::READ_DIR) }; + unsafe { handle_open(sigsafe::CStr::from_ptr(dir_name), 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/linux_syscall.rs b/crates/fspy_preload_unix/src/interceptions/linux_syscall.rs index 0731017d7..c6eed1314 100644 --- a/crates/fspy_preload_unix/src/interceptions/linux_syscall.rs +++ b/crates/fspy_preload_unix/src/interceptions/linux_syscall.rs @@ -1,11 +1,9 @@ use fspy_shared::ipc::AccessMode; use libc::{c_char, c_int, c_long}; +use sigsafe::BorrowedFd; use crate::{ - client::{ - convert::{Fd, PathAt}, - handle_open, - }, + client::{convert::PathAt, handle_open}, macros::intercept, }; @@ -41,11 +39,13 @@ unsafe extern "C" fn syscall(syscall_no: c_long, mut args: ...) -> c_long { if pathname.is_null() { if flags & libc::AT_EMPTY_PATH != 0 { // SAFETY: dirfd is provided by the statx syscall caller. - unsafe { handle_open(Fd(dirfd), AccessMode::READ) }; + unsafe { handle_open(BorrowedFd::borrow_raw(dirfd), AccessMode::READ) }; } } else { // SAFETY: pathname is a non-null C string pointer provided by the statx syscall caller. - unsafe { handle_open(PathAt(dirfd, pathname), AccessMode::READ) }; + unsafe { + handle_open(PathAt::borrow_raw(dirfd, pathname), AccessMode::READ); + }; } } // SAFETY: forwarding the syscall to the original libc syscall function with the extracted arguments diff --git a/crates/fspy_preload_unix/src/interceptions/open.rs b/crates/fspy_preload_unix/src/interceptions/open.rs index 641593a13..7dfd74263 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(path, OpenFlags(flags)) }; + unsafe { handle_open(sigsafe::CStr::from_ptr(path), 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() }; @@ -48,7 +48,7 @@ unsafe extern "C" fn openat( mut args: ... ) -> c_int { // SAFETY: dirfd and path are valid arguments provided by the caller of the interposed function - unsafe { handle_open(PathAt(dirfd, path), OpenFlags(flags)) }; + unsafe { handle_open(PathAt::borrow_raw(dirfd, path), OpenFlags(flags)) }; if has_mode_arg(flags) { // https://github.com/tailhook/openat/issues/21#issuecomment-535914957 @@ -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(path, OpenFlags(flags)) }; + unsafe { handle_open(sigsafe::CStr::from_ptr(path), 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() }; @@ -89,7 +89,7 @@ unsafe extern "C" fn openat_nocancel( mut args: ... ) -> c_int { // SAFETY: dirfd and path are valid arguments provided by the caller of openat$NOCANCEL - unsafe { handle_open(PathAt(dirfd, path), OpenFlags(flags)) }; + unsafe { handle_open(PathAt::borrow_raw(dirfd, path), OpenFlags(flags)) }; if has_mode_arg(flags) { // SAFETY: O_CREAT requires a mode argument, matching the openat$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(path, ModeStr(mode)) }; + unsafe { handle_open(sigsafe::CStr::from_ptr(path), 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(path, ModeStr(mode)) }; + unsafe { handle_open(sigsafe::CStr::from_ptr(path), 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 182226eea..e77c74404 100644 --- a/crates/fspy_preload_unix/src/interceptions/spawn/exec/mod.rs +++ b/crates/fspy_preload_unix/src/interceptions/spawn/exec/mod.rs @@ -195,15 +195,14 @@ mod linux_only { reason = "suppresses unused warning on *::original" )] let _unused = execveat::original; - // SAFETY: PathAt wraps a valid dirfd and pathname pointer from the interposed execveat call - let abs_path_result = unsafe { - PathAt(dirfd, pathname).to_absolute_path(|path| { - let Some(path) = path else { - return Ok(None); - }; - Ok(Some(CString::new(&**path).unwrap())) - }) - }; + // SAFETY: dirfd and pathname come from the interposed execveat call. + let path = unsafe { PathAt::borrow_raw(dirfd, pathname) }; + let abs_path_result = path.to_absolute_path(|path| { + let Some(path) = path else { + return Ok(None); + }; + Ok(Some(CString::new(&**path).unwrap())) + }); let abs_path = match abs_path_result { Ok(None) => { // SAFETY: forwarding the original arguments to the real execveat syscall diff --git a/crates/fspy_preload_unix/src/interceptions/stat.rs b/crates/fspy_preload_unix/src/interceptions/stat.rs index ac4af7651..27d067b91 100644 --- a/crates/fspy_preload_unix/src/interceptions/stat.rs +++ b/crates/fspy_preload_unix/src/interceptions/stat.rs @@ -1,8 +1,8 @@ use fspy_shared::ipc::AccessMode; use libc::{c_char, c_int, stat as stat_struct}; - #[cfg(target_os = "linux")] -use crate::client::convert::Fd; +use sigsafe::BorrowedFd; + use crate::{ client::{convert::PathAt, handle_open}, macros::intercept, @@ -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(path, AccessMode::READ); + handle_open(sigsafe::CStr::from_ptr(path), 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(path, AccessMode::READ); + handle_open(sigsafe::CStr::from_ptr(path), AccessMode::READ); } // SAFETY: calling the original libc lstat() with the same arguments forwarded from the interposed function unsafe { lstat::original()(path, buf) } @@ -38,7 +38,7 @@ unsafe extern "C" fn fstatat( ) -> c_int { // SAFETY: dirfd and pathname are valid arguments provided by the caller of the interposed function unsafe { - handle_open(PathAt(dirfd, pathname), AccessMode::READ); + handle_open(PathAt::borrow_raw(dirfd, pathname), AccessMode::READ); } // SAFETY: calling the original libc fstatat() with the same arguments forwarded from the interposed function unsafe { fstatat::original()(dirfd, pathname, buf, flags) } @@ -71,11 +71,11 @@ unsafe extern "C" fn statx( if pathname.is_null() { if flags & libc::AT_EMPTY_PATH != 0 { // SAFETY: dirfd is provided by the statx caller. - unsafe { handle_open(Fd(dirfd), AccessMode::READ) }; + unsafe { handle_open(BorrowedFd::borrow_raw(dirfd), AccessMode::READ) }; } } else { // SAFETY: pathname is a non-null C string pointer provided by the statx caller. - unsafe { handle_open(PathAt(dirfd, pathname), AccessMode::READ) }; + unsafe { handle_open(PathAt::borrow_raw(dirfd, pathname), AccessMode::READ) }; } // SAFETY: calling the original libc statx() with the same arguments forwarded from the interposed function unsafe { original(dirfd, pathname, flags, mask, statxbuf) } diff --git a/crates/sigsafe/Cargo.toml b/crates/sigsafe/Cargo.toml index efcb75645..d224295af 100644 --- a/crates/sigsafe/Cargo.toml +++ b/crates/sigsafe/Cargo.toml @@ -8,7 +8,7 @@ publish = false doctest = false [target.'cfg(unix)'.dependencies] -rustix = { workspace = true } +rustix = { workspace = true, features = ["fs"] } # 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. diff --git a/crates/sigsafe/src/c_str.rs b/crates/sigsafe/src/c_str.rs new file mode 100644 index 000000000..b0b49ec77 --- /dev/null +++ b/crates/sigsafe/src/c_str.rs @@ -0,0 +1,60 @@ +use core::{ffi::c_char, marker::PhantomData, ptr::NonNull}; + +/// Marks a [`CStr`] whose length is not known. +#[derive(Clone, Copy)] +pub struct Thin { + _private: (), +} + +/// A borrowed NUL-terminated string. +/// +/// [`CStr<'_, Thin>`] stores only the string pointer. +#[derive(Clone, Copy)] +pub struct CStr<'a, R> { + ptr: NonNull, + #[expect(dead_code, reason = "the representation value carries the type state")] + repr: R, + lifetime: PhantomData<&'a c_char>, +} + +impl CStr<'_, R> { + /// Returns a pointer to the first byte of this C string. + #[must_use] + pub const fn as_ptr(&self) -> *const c_char { + self.ptr.as_ptr() + } +} + +impl CStr<'_, Thin> { + /// Creates a thin C string view 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 { + Self { + // SAFETY: upheld by the caller. + ptr: unsafe { NonNull::new_unchecked(ptr.cast_mut()) }, + repr: Thin { _private: () }, + lifetime: PhantomData, + } + } +} + +#[cfg(test)] +mod tests { + use core::mem::size_of; + + use super::{CStr, Thin}; + + #[test] + fn thin_representation_is_one_pointer() { + // SAFETY: the input contains one trailing NUL. + let value = unsafe { CStr::::from_ptr(c"abc".as_ptr()) }; + + assert_eq!(size_of::>(), size_of::<*const u8>()); + assert_eq!(value.as_ptr(), c"abc".as_ptr()); + } +} diff --git a/crates/sigsafe/src/lib.rs b/crates/sigsafe/src/lib.rs index 478879d9a..e3889419f 100644 --- a/crates/sigsafe/src/lib.rs +++ b/crates/sigsafe/src/lib.rs @@ -14,10 +14,16 @@ #![cfg(unix)] #![cfg_attr(not(test), no_std)] +mod c_str; pub mod mm; pub mod param; -pub use rustix::io::Errno; +pub use c_str::{CStr, Thin}; +pub use rustix::{ + fd::{AsRawFd, BorrowedFd}, + fs::CWD, + io::Errno, +}; // Compile-time proof that rustix uses its raw-syscall backend (`linux_raw`) // on Linux — and with it, that no call in this crate goes through libc there.