Skip to content
Open
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
9 changes: 7 additions & 2 deletions crates/fspy/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }

Expand All @@ -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]
Expand Down
6 changes: 3 additions & 3 deletions crates/fspy/src/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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::{
Expand All @@ -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;

Expand Down
88 changes: 88 additions & 0 deletions crates/fspy/src/freebsd.rs
Original file line number Diff line number Diff line change
@@ -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<Self> {
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<TrackedChild, SpawnError> {
// `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<Item = PathAccess<'_>> {
std::iter::empty()
}
}
15 changes: 12 additions & 3 deletions crates/fspy/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
3 changes: 3 additions & 0 deletions crates/fspy_nostd/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
2 changes: 1 addition & 1 deletion crates/fspy_nostd/src/c_str.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
10 changes: 5 additions & 5 deletions crates/fspy_nostd/src/error.rs
Original file line number Diff line number Diff line change
@@ -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);
Expand All @@ -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);
Expand All @@ -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
Expand Down Expand Up @@ -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;
Expand Down
4 changes: 2 additions & 2 deletions crates/fspy_nostd/src/fd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.
Expand Down
41 changes: 38 additions & 3 deletions crates/fspy_nostd/src/fs/mac.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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<u8>; PATH_MAX],
Expand All @@ -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<u8>]) -> Result<CStr<'_, Fat>> {
let (chunks, remainder) = buf.as_chunks_mut::<PATH_MAX>();
let Some(full) = chunks.first_mut() else {
Expand All @@ -112,6 +119,33 @@ pub(super) fn getcwd(buf: &mut [MaybeUninit<u8>]) -> Result<CStr<'_, Fat>> {
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<u8>]) -> Result<CStr<'_, Fat>> {
// 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::<Thin>::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.
Expand Down Expand Up @@ -142,6 +176,7 @@ fn getcwd_small(buf: &mut [MaybeUninit<u8>]) -> Result<CStr<'_, Fat>> {
/// 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<u8>; PATH_MAX]) -> Result<CStr<'_, Fat>> {
// SAFETY: the byte string contains one trailing NUL.
let dot_path = unsafe { CStr::<Fat>::from_units_with_nul_unchecked(b".\0") };
Expand Down
13 changes: 9 additions & 4 deletions crates/fspy_nostd/src/fs/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
2 changes: 1 addition & 1 deletion crates/fspy_nostd/src/fs/unix.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading