Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions .github/workflows/fspy-benchmark.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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: |
Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/fspy_preload_unix/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
65 changes: 39 additions & 26 deletions crates/fspy_preload_unix/src/client/convert.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,71 +2,85 @@
use std::ffi::CString;
use std::{
ffi::{CStr, OsStr},
os::{fd::RawFd, unix::ffi::OsStrExt as _},
os::unix::ffi::OsStrExt as _,
path::PathBuf,
};

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<Option<PathBuf>> {
if fd == libc::AT_FDCWD {
fn get_fd_path(fd: BorrowedFd<'_>) -> nix::Result<Option<PathBuf>> {
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),
}
}

#[cfg(target_os = "macos")]
fn get_fd_path(fd: RawFd) -> nix::Result<Option<PathBuf>> {
if fd == libc::AT_FDCWD {
fn get_fd_path(fd: BorrowedFd<'_>) -> nix::Result<Option<PathBuf>> {
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),
}
}

pub trait ToAbsolutePath {
unsafe fn to_absolute_path<R, F: FnOnce(Option<&BStr>) -> nix::Result<R>>(
fn to_absolute_path<R, F: FnOnce(Option<&BStr>) -> nix::Result<R>>(
self,
f: F,
) -> nix::Result<R>;
}

pub struct Fd(pub c_int);
impl ToAbsolutePath for Fd {
unsafe fn to_absolute_path<R, F: FnOnce(Option<&BStr>) -> nix::Result<R>>(
impl ToAbsolutePath for BorrowedFd<'_> {
fn to_absolute_path<R, F: FnOnce(Option<&BStr>) -> nix::Result<R>>(
self,
f: F,
) -> nix::Result<R> {
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<R, F: FnOnce(Option<&BStr>) -> nix::Result<R>>(
impl ToAbsolutePath for PathAt<'_, '_> {
fn to_absolute_path<R, F: FnOnce(Option<&BStr>) -> nix::Result<R>>(
self,
f: F,
) -> nix::Result<R> {
// 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())
Expand All @@ -82,13 +96,12 @@ impl ToAbsolutePath for PathAt {
}
}

impl ToAbsolutePath for *const c_char {
unsafe fn to_absolute_path<R, F: FnOnce(Option<&BStr>) -> nix::Result<R>>(
impl ToAbsolutePath for sigsafe::CStr<'_, sigsafe::Thin> {
fn to_absolute_path<R, F: FnOnce(Option<&BStr>) -> nix::Result<R>>(
self,
f: F,
) -> nix::Result<R> {
// 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)
}
}

Expand Down
15 changes: 6 additions & 9 deletions crates/fspy_preload_unix/src/client/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(())
}
Expand Down
4 changes: 2 additions & 2 deletions crates/fspy_preload_unix/src/interceptions/access.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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) }
Expand All @@ -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) }
Expand Down
20 changes: 9 additions & 11 deletions crates/fspy_preload_unix/src/interceptions/dirent.rs
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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,
Expand All @@ -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) }
}
Expand All @@ -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) }
}
Expand All @@ -68,23 +66,23 @@ 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) }
}

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) }
}

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) }
}
12 changes: 6 additions & 6 deletions crates/fspy_preload_unix/src/interceptions/linux_syscall.rs
Original file line number Diff line number Diff line change
@@ -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,
};

Expand Down Expand Up @@ -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
Expand Down
12 changes: 6 additions & 6 deletions crates/fspy_preload_unix/src/interceptions/open.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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() };
Expand All @@ -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
Expand All @@ -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() };
Expand All @@ -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() };
Expand All @@ -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) }
}
Expand All @@ -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) }
}
17 changes: 8 additions & 9 deletions crates/fspy_preload_unix/src/interceptions/spawn/exec/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
Loading