diff --git a/crates/fspy/Cargo.toml b/crates/fspy/Cargo.toml index fe5b8a476..f80313b1a 100644 --- a/crates/fspy/Cargo.toml +++ b/crates/fspy/Cargo.toml @@ -28,7 +28,12 @@ fspy_seccomp_unotify = { workspace = true, features = ["supervisor"] } nix = { workspace = true, features = ["uio"] } tokio = { workspace = true, features = ["bytes"] } -[target.'cfg(unix)'.dependencies] +# The supervision machinery (`fspy_shared_unix` exec/payload helpers and the +# `nix` syscalls) is only used by the Linux seccomp and macOS Detours backends +# in `src/unix/`. FreeBSD builds the no-op backend in `src/freebsd.rs` and does +# not compile `src/unix/`, so keep those deps off the FreeBSD build graph (and +# out of its transitive `fspy_nostd`/`fspy_client_unix` subtree). +[target.'cfg(all(unix, not(target_os = "freebsd")))'.dependencies] fspy_shared_unix = { workspace = true } nix = { workspace = true, features = ["fs", "process", "socket", "feature"] } @@ -43,7 +48,7 @@ nix = { workspace = true, features = ["fs", "process", "socket", "feature"] } # preload) don't build a useless empty cdylib. Scoping artifact deps under # `[target.cfg…]` is only safe for normal deps: the same shape under # `[target.cfg….build-dependencies]` panics cargo's resolver on cross-compile. -[target.'cfg(all(unix, not(target_env = "musl")))'.dependencies] +[target.'cfg(all(unix, not(target_env = "musl"), not(target_os = "freebsd")))'.dependencies] fspy_preload_unix = { workspace = true } [target.'cfg(target_os = "windows")'.dependencies] diff --git a/crates/fspy/src/command.rs b/crates/fspy/src/command.rs index fb150b26c..b4766c398 100644 --- a/crates/fspy/src/command.rs +++ b/crates/fspy/src/command.rs @@ -4,7 +4,7 @@ use std::{ process::Stdio, }; -#[cfg(unix)] +#[cfg(all(unix, not(target_os = "freebsd")))] use fspy_shared_unix::exec::Exec; use rustc_hash::FxHashMap; use tokio::process::Command as TokioCommand; @@ -50,7 +50,7 @@ impl Command { } } - #[cfg(unix)] + #[cfg(all(unix, not(target_os = "freebsd")))] #[must_use] pub(crate) fn get_exec(&self) -> Exec { use std::{ @@ -74,7 +74,7 @@ impl Command { } } - #[cfg(unix)] + #[cfg(all(unix, not(target_os = "freebsd")))] pub(crate) fn set_exec(&mut self, mut exec: Exec) { use std::os::unix::ffi::OsStringExt; diff --git a/crates/fspy/src/freebsd.rs b/crates/fspy/src/freebsd.rs new file mode 100644 index 000000000..4d220bcb9 --- /dev/null +++ b/crates/fspy/src/freebsd.rs @@ -0,0 +1,88 @@ +//! FreeBSD backend for `fspy`. +//! +//! Linux uses a seccomp-user-notifications supervisor and macOS uses Detours +//! + `LD_PRELOAD` interposition to record the file-system accesses of a child +//! process. FreeBSD has neither of those facilities in this crate, so it +//! provides a no-op backend: the child process is spawned and waited on +//! normally, and [`PathAccessIterable::iter`] returns an empty iterator. +//! +//! Callers (e.g. `vp_command::run_command_with_fspy`) keep working on +//! FreeBSD — they simply observe zero path accesses, which is an honest +//! result for a platform without tracing support. + +use std::io; + +use futures_util::future::FutureExt; +use tokio_util::sync::CancellationToken; + +use crate::{ChildTermination, Command, TrackedChild, error::SpawnError}; +use fspy_shared::ipc::PathAccess; + +/// No-op backend: initialize without creating any on-disk artifacts. +pub struct SpyImpl; + +impl SpyImpl { + /// Initialize the (empty) backend. `dir` is unused on FreeBSD because + /// there is no preload library or Detours artifact to materialize. + #[allow( + clippy::unused_self, + reason = "init_in takes a directory for parity with the other backends" + )] + pub fn init_in(_dir: &std::path::Path) -> io::Result { + Ok(Self) + } + + /// Spawn the command and wait for its status; report no path accesses. + pub async fn spawn( + &self, + command: Command, + cancellation_token: CancellationToken, + ) -> Result { + // `into_tokio_command` applies `current_dir`, `arg0`, args, envs, + // stdio, and any pre_exec closures the caller registered. On FreeBSD + // none of the supervision machinery is attached, so this is a plain + // spawn. `tokio::process::Command::spawn` is synchronous (and the + // pre_exec closures may block), so run it off the async runtime the + // same way the Linux/macOS backends do. + let mut tokio_command = command.into_tokio_command(); + + let mut child = tokio::task::spawn_blocking(move || tokio_command.spawn()) + .await + .map_err(|err| SpawnError::OsSpawn(err.into()))? + .map_err(SpawnError::OsSpawn)?; + + // Take the stdio handles before `child` is moved into the background + // wait task, matching the Linux/macOS backends. + let stdin = child.stdin.take(); + let stdout = child.stdout.take(); + let stderr = child.stderr.take(); + + // Keep polling for the child to exit in the background even if the + // caller never awaits the wait handle; this matches the Linux/macOS + // backends (which also need to release supervision resources on + // exit). + let wait_handle = tokio::spawn(async move { + let status = tokio::select! { + status = child.wait() => status?, + () = cancellation_token.cancelled() => { + child.start_kill()?; + child.wait().await? + } + }; + io::Result::Ok(ChildTermination { status, path_accesses: Ok(PathAccessIterable) }) + }) + .map(|f| f?) // flatten JoinError and io::Result + .boxed(); + + Ok(TrackedChild { stdin, stdout, stderr, wait_handle }) + } +} + +/// No-op path-access iterator: yields no entries on FreeBSD. +pub struct PathAccessIterable; + +impl PathAccessIterable { + pub fn iter(&self) -> impl Iterator> { + std::iter::empty() + } +} diff --git a/crates/fspy/src/lib.rs b/crates/fspy/src/lib.rs index 5621547f5..267709df3 100644 --- a/crates/fspy/src/lib.rs +++ b/crates/fspy/src/lib.rs @@ -2,18 +2,27 @@ pub mod error; -#[cfg(not(target_env = "musl"))] +#[cfg(all(not(target_env = "musl"), not(target_os = "freebsd")))] mod ipc; -#[cfg(unix)] +// The `unix/` supervision backend (seccomp supervisor on Linux, Detours +// artifacts + LD_PRELOAD interposer on macOS) is implemented for those two +// platforms only. FreeBSD compiles a no-op backend (below) that runs the +// command without file-access tracing, so the supervision modules and their +// preload/shared-UNIX dependencies are excluded from the FreeBSD build graph. +#[cfg(all(unix, not(target_os = "freebsd")))] #[path = "./unix/mod.rs"] mod os_impl; +#[cfg(target_os = "freebsd")] +#[path = "./freebsd.rs"] +mod os_impl; + #[cfg(target_os = "windows")] #[path = "./windows/mod.rs"] mod os_impl; -#[cfg(unix)] +#[cfg(all(unix, not(target_os = "freebsd")))] mod arena; mod command; diff --git a/crates/fspy_nostd/Cargo.toml b/crates/fspy_nostd/Cargo.toml index bddf0046d..58fa1aeb0 100644 --- a/crates/fspy_nostd/Cargo.toml +++ b/crates/fspy_nostd/Cargo.toml @@ -16,6 +16,9 @@ bitflags = { workspace = true } [target.'cfg(target_os = "macos")'.dependencies] libc = { workspace = true } +[target.'cfg(target_os = "freebsd")'.dependencies] +libc = { workspace = true } + [target.'cfg(any(target_os = "linux", target_os = "none"))'.dependencies] # Parsing remains allocation-free and no-std; `atoi` enables `std` by default. atoi = { version = "3.1.0", default-features = false } diff --git a/crates/fspy_nostd/src/c_str.rs b/crates/fspy_nostd/src/c_str.rs index bf36cdf86..2be728444 100644 --- a/crates/fspy_nostd/src/c_str.rs +++ b/crates/fspy_nostd/src/c_str.rs @@ -48,7 +48,7 @@ pub type WideCStr<'a, R> = CStr<'a, R, u16>; /// A borrowed NUL-terminated string of the platform's native path code /// units: bytes on Unix and wide (`u16`) code units on Windows. -#[cfg(any(target_os = "linux", target_os = "none", target_os = "macos"))] +#[cfg(any(target_os = "linux", target_os = "none", target_os = "macos", target_os = "freebsd"))] pub type OsCStr<'a, R> = CStr<'a, R>; /// A borrowed NUL-terminated string of the platform's native path code /// units: bytes on Unix and wide (`u16`) code units on Windows. diff --git a/crates/fspy_nostd/src/error.rs b/crates/fspy_nostd/src/error.rs index e6d3d9af4..05b28e262 100644 --- a/crates/fspy_nostd/src/error.rs +++ b/crates/fspy_nostd/src/error.rs @@ -1,10 +1,10 @@ -#[cfg(target_os = "macos")] +#[cfg(any(target_os = "macos", target_os = "freebsd"))] use libc::__error; #[cfg(windows)] use windows_sys::Win32::Foundation::GetLastError; /// An operating-system error code. -#[cfg(any(target_os = "linux", target_os = "none", target_os = "macos"))] +#[cfg(any(target_os = "linux", target_os = "none", target_os = "macos", target_os = "freebsd"))] #[derive(Clone, Copy, Debug, Eq, PartialEq)] #[repr(transparent)] pub struct Error(i32); @@ -15,7 +15,7 @@ pub struct Error(i32); #[repr(transparent)] pub struct Error(u32); -#[cfg(any(target_os = "linux", target_os = "none", target_os = "macos"))] +#[cfg(any(target_os = "linux", target_os = "none", target_os = "macos", target_os = "freebsd"))] impl Error { pub const BADF: Self = Self(errno::BADF); pub const INVAL: Self = Self(errno::INVAL); @@ -40,7 +40,7 @@ impl Error { /// /// Call this immediately after the failing libc call: anything in between, /// including drops, can overwrite the thread-local error code. - #[cfg(target_os = "macos")] + #[cfg(any(target_os = "macos", target_os = "freebsd"))] #[must_use] pub fn last_os_error() -> Self { // SAFETY: libSystem exposes the calling thread's errno through this @@ -91,7 +91,7 @@ mod errno { pub const RANGE: i32 = linux_raw_sys::errno::ERANGE.cast_signed(); } -#[cfg(target_os = "macos")] +#[cfg(any(target_os = "macos", target_os = "freebsd"))] mod errno { pub const BADF: i32 = libc::EBADF; pub const INVAL: i32 = libc::EINVAL; diff --git a/crates/fspy_nostd/src/fd.rs b/crates/fspy_nostd/src/fd.rs index 202411033..e48e2beba 100644 --- a/crates/fspy_nostd/src/fd.rs +++ b/crates/fspy_nostd/src/fd.rs @@ -76,7 +76,7 @@ impl Drop for OwnedFd { // once. Close errors cannot be acted on during drop. let _ = unsafe { syscalls::syscall!(syscalls::Sysno::close, self.fd) }; } - #[cfg(target_os = "macos")] + #[cfg(any(target_os = "macos", target_os = "freebsd"))] { // SAFETY: this type owns the descriptor and closes it exactly // once. Close errors cannot be acted on during drop. @@ -87,7 +87,7 @@ impl Drop for OwnedFd { #[cfg(any(target_os = "linux", target_os = "none"))] const CWD_RAW: RawFd = linux_raw_sys::general::AT_FDCWD; -#[cfg(target_os = "macos")] +#[cfg(any(target_os = "macos", target_os = "freebsd"))] const CWD_RAW: RawFd = libc::AT_FDCWD; /// The reserved directory descriptor representing the current directory. diff --git a/crates/fspy_nostd/src/fs/mac.rs b/crates/fspy_nostd/src/fs/mac.rs index 943dfd417..1b3dc0e2d 100644 --- a/crates/fspy_nostd/src/fs/mac.rs +++ b/crates/fspy_nostd/src/fs/mac.rs @@ -1,9 +1,12 @@ -use core::{mem::MaybeUninit, slice}; - +#[cfg(target_os = "macos")] +use crate::CWD; use crate::{ - BorrowedFd, CStr, CWD, Error, Fat, OwnedFd, Result, Thin, + BorrowedFd, CStr, Error, Fat, OwnedFd, Result, Thin, fs::{AtFlags, Mode, OFlags, Stat}, }; +use core::mem::MaybeUninit; +#[cfg(target_os = "macos")] +use core::slice; // Darwin UAPI `MAXPATHLEN`. pub(super) const PATH_MAX: usize = 1024; @@ -84,9 +87,12 @@ pub(super) fn ftruncate(fd: BorrowedFd<'_>, len: u64) -> Result<()> { /// This function performs one `fcntl` call and does not retry. `F_GETPATH` /// accepts no buffer length and always uses its fixed `MAXPATHLEN` storage. /// +/// macOS-only: `F_GETPATH` is a Darwin `fcntl` command. +/// /// # Errors /// /// Returns the error reported by `fcntl`. +#[cfg(target_os = "macos")] pub fn fcntl_getpath<'buf>( fd: BorrowedFd<'_>, buf: &'buf mut [MaybeUninit; PATH_MAX], @@ -104,6 +110,7 @@ pub fn fcntl_getpath<'buf>( Ok(unsafe { CStr::from_ptr(buf.as_ptr().cast()) }) } +#[cfg(target_os = "macos")] pub(super) fn getcwd(buf: &mut [MaybeUninit]) -> Result> { let (chunks, remainder) = buf.as_chunks_mut::(); let Some(full) = chunks.first_mut() else { @@ -112,6 +119,33 @@ pub(super) fn getcwd(buf: &mut [MaybeUninit]) -> Result> { getcwd_full(full) } +/// Writes the absolute pathname of the current working directory into `buf`. +/// +/// Uses the standard `getcwd(2)` call, which writes a NUL-terminated +/// pathname into the caller-provided buffer. +/// +/// # Errors +/// +/// Returns [`Error::RANGE`] when `buf` is too small to hold the pathname; +/// the empty buffer cannot hold any pathname at all. FreeBSD reports that +/// case as `EINVAL`, so it is normalized here to match the macOS path. +#[cfg(target_os = "freebsd")] +pub(super) fn getcwd(buf: &mut [MaybeUninit]) -> Result> { + // SAFETY: `buf` is writable for `buf.len()` bytes; `getcwd` writes no + // more than that and returns a NUL-terminated pathname or null on error. + let ptr = unsafe { libc::getcwd(buf.as_mut_ptr().cast(), buf.len()) }; + if ptr.is_null() { + if buf.is_empty() { + return Err(Error::RANGE); + } + return Err(Error::last_os_error()); + } + // SAFETY: `getcwd` wrote a valid NUL-terminated pathname into `buf`. + let thin = unsafe { CStr::::from_ptr(ptr.cast()) }; + Ok(thin.count()) +} + +#[cfg(target_os = "macos")] // Keep the `PATH_MAX` scratch storage in a separate stack frame so the // large-buffer path does not reserve it. `inline(never)` preserves that // conditional stack allocation after optimization. @@ -142,6 +176,7 @@ fn getcwd_small(buf: &mut [MaybeUninit]) -> Result> { /// common resolution there is. /// /// [`getcwd`]: https://github.com/apple-oss-distributions/Libc/blob/Libc-1752.120.2/gen/FreeBSD/getcwd.c#L62-L138 +#[cfg(target_os = "macos")] fn getcwd_full(buf: &mut [MaybeUninit; PATH_MAX]) -> Result> { // SAFETY: the byte string contains one trailing NUL. let dot_path = unsafe { CStr::::from_units_with_nul_unchecked(b".\0") }; diff --git a/crates/fspy_nostd/src/fs/mod.rs b/crates/fspy_nostd/src/fs/mod.rs index bb661566a..c72b8ab5c 100644 --- a/crates/fspy_nostd/src/fs/mod.rs +++ b/crates/fspy_nostd/src/fs/mod.rs @@ -2,17 +2,22 @@ #[cfg(any(target_os = "linux", target_os = "none"))] mod linux; -#[cfg(target_os = "macos")] +#[cfg(any(target_os = "macos", target_os = "freebsd"))] mod mac; -#[cfg(any(target_os = "linux", target_os = "none", target_os = "macos"))] +#[cfg(any(target_os = "linux", target_os = "none", target_os = "macos", target_os = "freebsd"))] mod unix; #[cfg(windows)] mod windows; -#[cfg(any(target_os = "linux", target_os = "none", target_os = "macos"))] +#[cfg(any( + target_os = "linux", + target_os = "none", + target_os = "macos", + target_os = "freebsd" +))] pub use unix::*; #[cfg(windows)] pub use windows::*; -#[cfg(all(test, any(target_os = "linux", target_os = "macos")))] +#[cfg(all(test, any(target_os = "linux", target_os = "macos", target_os = "freebsd")))] mod tests; diff --git a/crates/fspy_nostd/src/fs/unix.rs b/crates/fspy_nostd/src/fs/unix.rs index 36c7ceeec..dc1d8f62a 100644 --- a/crates/fspy_nostd/src/fs/unix.rs +++ b/crates/fspy_nostd/src/fs/unix.rs @@ -6,7 +6,7 @@ use bitflags::bitflags; use super::linux as imp; #[cfg(any(target_os = "linux", target_os = "none"))] pub use super::linux::readlinkat; -#[cfg(target_os = "macos")] +#[cfg(any(target_os = "macos", target_os = "freebsd"))] use super::mac as imp; #[cfg(target_os = "macos")] pub use super::mac::fcntl_getpath; diff --git a/crates/fspy_nostd/src/lib.rs b/crates/fspy_nostd/src/lib.rs index 8a0ec38cb..7f375e7a0 100644 --- a/crates/fspy_nostd/src/lib.rs +++ b/crates/fspy_nostd/src/lib.rs @@ -9,25 +9,42 @@ mod c_str; mod error; -#[cfg(any(target_os = "linux", target_os = "none", target_os = "macos"))] +#[cfg(any(target_os = "linux", target_os = "none", target_os = "macos", target_os = "freebsd"))] mod fd; #[cfg(windows)] mod windows; #[cfg(any(target_os = "linux", target_os = "none", target_os = "macos"))] pub mod env; -#[cfg(any(target_os = "linux", target_os = "none", target_os = "macos", windows))] +#[cfg(any( + target_os = "linux", + target_os = "none", + target_os = "macos", + windows, + target_os = "freebsd" +))] pub mod fs; #[cfg(any(target_os = "linux", target_os = "none"))] pub mod io; -#[cfg(any(target_os = "linux", target_os = "none", target_os = "macos", windows))] +#[cfg(any( + target_os = "linux", + target_os = "none", + target_os = "macos", + windows, + target_os = "freebsd" +))] pub mod mm; -#[cfg(any(target_os = "linux", target_os = "none", target_os = "macos"))] +#[cfg(any(target_os = "linux", target_os = "none", target_os = "macos", target_os = "freebsd"))] pub mod param; pub use c_str::{CStr, CStrUnit, Fat, OsCStr, Thin, Units, WideCStr}; pub use error::{Error, Result}; -#[cfg(any(target_os = "linux", target_os = "none", target_os = "macos"))] +#[cfg(any( + target_os = "linux", + target_os = "none", + target_os = "macos", + target_os = "freebsd" +))] pub use fd::{BorrowedFd, CWD, OwnedFd, RawFd}; #[cfg(windows)] pub use windows::{ diff --git a/crates/fspy_nostd/src/mm.rs b/crates/fspy_nostd/src/mm.rs index 1547a60c3..8af2c9e18 100644 --- a/crates/fspy_nostd/src/mm.rs +++ b/crates/fspy_nostd/src/mm.rs @@ -2,19 +2,19 @@ #[cfg(any(target_os = "linux", target_os = "none"))] mod linux; -#[cfg(target_os = "macos")] +#[cfg(any(target_os = "macos", target_os = "freebsd"))] mod mac; #[cfg(windows)] mod windows; #[cfg(any(target_os = "linux", target_os = "none"))] use linux as imp; -#[cfg(target_os = "macos")] +#[cfg(any(target_os = "macos", target_os = "freebsd"))] use mac as imp; #[cfg(windows)] pub use windows::*; -#[cfg(any(target_os = "linux", target_os = "none", target_os = "macos"))] +#[cfg(any(target_os = "linux", target_os = "none", target_os = "macos", target_os = "freebsd"))] mod unix { use core::ffi::c_void; @@ -133,5 +133,10 @@ mod unix { } } -#[cfg(any(target_os = "linux", target_os = "none", target_os = "macos"))] +#[cfg(any( + target_os = "linux", + target_os = "none", + target_os = "macos", + target_os = "freebsd" +))] pub use unix::*; diff --git a/crates/fspy_nostd/src/param.rs b/crates/fspy_nostd/src/param.rs index 08140a890..d5eaa8b24 100644 --- a/crates/fspy_nostd/src/param.rs +++ b/crates/fspy_nostd/src/param.rs @@ -14,7 +14,7 @@ #[cfg(any(target_os = "linux", target_os = "none"))] pub use linux::page_size; -#[cfg(target_os = "macos")] +#[cfg(any(target_os = "macos", target_os = "freebsd"))] pub use mac::page_size; #[cfg(any(target_os = "linux", target_os = "none"))] @@ -134,7 +134,7 @@ mod linux { } } -#[cfg(target_os = "macos")] +#[cfg(any(target_os = "macos", target_os = "freebsd"))] mod mac { /// Returns the process page size, or zero if libSystem cannot report it. #[must_use]