Skip to content
Draft
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Changelog

- **Changed** Automatic input tracking for static Linux binaries and musl builds now uses seccomp-filtered `ptrace` instead of seccomp user notification, while preserving direct-syscall coverage.
- **Added** Tasks now run with `VP_RUN=1` set, so tools can tell they are running under `vp run` instead of being invoked directly ([#570](https://github.com/voidzero-dev/vite-task/pull/570)).
- **Fixed** The task cache now supports much larger automatically tracked input sets without hitting wincode's default 4 MiB sequence preallocation limit ([#554](https://github.com/voidzero-dev/vite-task/pull/554)).
- **Fixed** npm workspace patterns beginning with `./` now discover matching packages correctly ([vite-plus#2201](https://github.com/voidzero-dev/vite-plus/issues/2201), [#547](https://github.com/voidzero-dev/vite-task/pull/547)).
Expand Down
11 changes: 6 additions & 5 deletions Cargo.lock

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

5 changes: 2 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ fspy_benchmark_target = { path = "crates/fspy_benchmark_target", artifact = "bin
fspy_detours_sys = { path = "crates/fspy_detours_sys" }
fspy_preload_unix = { path = "crates/fspy_preload_unix", artifact = "cdylib", target = "target" }
fspy_preload_windows = { path = "crates/fspy_preload_windows", artifact = "cdylib", target = "target" }
fspy_seccomp_unotify = { path = "crates/fspy_seccomp_unotify" }
fspy_seccomp_ptrace = { path = "crates/fspy_seccomp_ptrace" }
fspy_shm = { path = "crates/fspy_shm" }
fspy_shared = { path = "crates/fspy_shared" }
fspy_shared_unix = { path = "crates/fspy_shared_unix" }
Expand Down Expand Up @@ -119,8 +119,7 @@ ref-cast = "1.0.24"
regex = "1.11.3"
rusqlite = "0.39.0"
rustc-hash = "2.1.1"
# SeccompAction::UserNotif (SECCOMP_RET_USER_NOTIF) was added after the latest published release (v0.5.0)
seccompiler = { git = "https://github.com/rust-vmm/seccompiler", rev = "08587106340b8e3cb361c7561411510039436857" }
seccompiler = "0.5.0"
serde = "1.0.219"
serde_json = "1.0.140"
serde_norway = "0.9.42"
Expand Down
2 changes: 1 addition & 1 deletion crates/fspy/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ tokio-util = { workspace = true }
which = { workspace = true, features = ["tracing"] }

[target.'cfg(target_os = "linux")'.dependencies]
fspy_seccomp_unotify = { workspace = true, features = ["supervisor"] }
fspy_seccomp_ptrace = { workspace = true, features = ["supervisor"] }
nix = { workspace = true, features = ["uio"] }
tokio = { workspace = true, features = ["bytes"] }

Expand Down
4 changes: 2 additions & 2 deletions crates/fspy/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ The injection process is almost identical on both platforms other than the envir

## Linux-specific implementation for fully static binaries

For fully static binaries (such as `esbuild`), `LD_PRELOAD` does not work. In this case, `seccomp_unotify` is used to intercept direct system calls. The handler is implemented in `src/unix/syscall_handler`.
For fully static binaries (such as `esbuild`), `LD_PRELOAD` does not work. In this case, a seccomp filter routes selected system calls through a `ptrace` supervisor. The handler is implemented in `src/unix/syscall_handler`.

## Linux musl implementation

On musl targets, only `seccomp_unotify`-based tracking is used (no preload library).
On musl targets, only seccomp-filtered `ptrace` tracking is used (no preload library).

## Windows implementation

Expand Down
2 changes: 1 addition & 1 deletion crates/fspy/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ pub enum SpawnError {
cause: which::Error,
},

#[error("failed to initialize seccomp_unotify supervisor: {0}")]
#[error("failed to initialize seccomp-filtered ptrace supervisor: {0}")]
Supervisor(std::io::Error),

#[error("failed to create IPC channel: {0}")]
Expand Down
4 changes: 2 additions & 2 deletions crates/fspy/src/unix/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ mod macos_artifacts;
use std::{io, path::Path};

#[cfg(target_os = "linux")]
use fspy_seccomp_unotify::supervisor::supervise;
use fspy_seccomp_ptrace::supervisor::supervise;
use fspy_shared::ipc::PathAccess;
#[cfg(not(target_env = "musl"))]
use fspy_shared::ipc::{NativeStr, channel::channel};
Expand Down Expand Up @@ -92,7 +92,7 @@ impl SpyImpl {
preload_path: self.preload_path.clone(),

#[cfg(target_os = "linux")]
seccomp_payload: supervisor.payload().clone(),
ptrace_payload: supervisor.payload().clone(),
};

let encoded_payload = encode_payload(payload);
Expand Down
2 changes: 1 addition & 1 deletion crates/fspy/src/unix/syscall_handler/execve.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use std::io;

use fspy_seccomp_unotify::supervisor::handler::arg::{CStrPtr, Caller, Fd};
use fspy_seccomp_ptrace::supervisor::handler::arg::{CStrPtr, Caller, Fd};

use super::SyscallHandler;

Expand Down
2 changes: 1 addition & 1 deletion crates/fspy/src/unix/syscall_handler/getdents.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use std::io;

use fspy_seccomp_unotify::supervisor::handler::arg::{Caller, Fd};
use fspy_seccomp_ptrace::supervisor::handler::arg::{Caller, Fd};

use super::SyscallHandler;

Expand Down
2 changes: 1 addition & 1 deletion crates/fspy/src/unix/syscall_handler/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use std::{
path::{Path, PathBuf},
};

use fspy_seccomp_unotify::{
use fspy_seccomp_ptrace::{
impl_handler,
supervisor::handler::arg::{CStrPtr, Caller, Fd},
};
Expand Down
2 changes: 1 addition & 1 deletion crates/fspy/src/unix/syscall_handler/open.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use std::{ffi::c_int, io};

use fspy_seccomp_unotify::supervisor::handler::arg::{CStrPtr, Caller, Fd, Ptr};
use fspy_seccomp_ptrace::supervisor::handler::arg::{CStrPtr, Caller, Fd, Ptr};

use super::SyscallHandler;

Expand Down
2 changes: 1 addition & 1 deletion crates/fspy/src/unix/syscall_handler/stat.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use std::io;

use fspy_seccomp_unotify::supervisor::handler::arg::{CStrPtr, Caller, Fd};
use fspy_seccomp_ptrace::supervisor::handler::arg::{CStrPtr, Caller, Fd};

use super::SyscallHandler;

Expand Down
6 changes: 5 additions & 1 deletion crates/fspy/src/windows/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,11 @@ impl SpyImpl {
Ok(Self { ansi_dll_path_with_nul: ansi_dll_path_with_nul.into() })
}

#[expect(clippy::unused_async, reason = "async signature required by SpyImpl trait")]
#[expect(
clippy::unused_async,
clippy::unused_async_trait_impl,
reason = "async signature must match the Unix SpyImpl"
)]
pub(crate) async fn spawn(
&self,
mut command: Command,
Expand Down
17 changes: 16 additions & 1 deletion crates/fspy/tests/static_executable.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//! Tests for fspy tracing of statically-linked executables (seccomp path).
//! Tests for fspy tracing of statically-linked executables (seccomp-filtered ptrace path).
//! Skipped on musl: the test binary is an artifact dep targeting musl, and when
//! the CI builds with `-crt-static` the binary becomes dynamically linked,
//! defeating the purpose of these tests.
Expand Down Expand Up @@ -120,3 +120,18 @@ async fn execve() {
let accesses = track_test_bin(&["execve", "/hello"], None).await;
assert_contains(&accesses, Path::new("/hello"), fspy::AccessMode::READ);
}

#[test(tokio::test)]
async fn spawned_from_dynamic_parent() {
let test_bin = test_bin_path().to_str().unwrap().to_owned();
let accesses = track_fn!(test_bin, |test_bin: String| {
let status = std::process::Command::new(test_bin)
.args(["open_read", "/fspy-static-child"])
.status()
.unwrap();
assert!(status.success());
})
.await
.unwrap();
assert_contains(&accesses, Path::new("/fspy-static-child"), fspy::AccessMode::READ);
}
1 change: 0 additions & 1 deletion crates/fspy_benchmark/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ workspace = true

[[bin]]
name = "fspy_benchmark"
test = false
doctest = false

[dependencies]
Expand Down
7 changes: 4 additions & 3 deletions crates/fspy_benchmark/README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
# fspy benchmark

Measures what fspy costs a process it tracks, and whether a change to fspy moved that cost. It reports two rows:
Measures what fspy costs a process it tracks, and whether a change to fspy moved that cost. It reports three rows:

- `launch`: the wall clock of a tracked launch that opens nothing. This is the cost of starting a process under tracking: injection, session setup, and teardown.
- `access`: how long a batch of opens takes, timed by two threads that each open their own path. This is the cost of interception itself, under the concurrency a tracked process normally has.
- `access-wall`: the wall clock of the same access workload, from spawn through reap. This is its end-to-end cost, including launch, all opens, and teardown.

Linux measures a dynamically linked target (`LD_PRELOAD`) and a `x86_64-unknown-linux-musl` target (seccomp user notification). macOS measures `DYLD_INSERT_LIBRARIES`. Windows measures Detours injection.
Linux measures a dynamically linked target (`LD_PRELOAD`) and a `x86_64-unknown-linux-musl` target (seccomp-filtered `ptrace`). macOS measures `DYLD_INSERT_LIBRARIES`. Windows measures Detours injection.

Run the overhead report locally with:

Expand All @@ -19,7 +20,7 @@ Comparing a pull request run against a saved result from an earlier `main` run d

So nothing is ever compared across runs. Both fspy revisions run side by side on one machine, in one job. Three pieces make that possible:

- `fspy_benchmark_target` (and its static twin) is the workload. It opens missing paths from several threads and prints the median batch time. It does not link fspy. Batches are a few microseconds long, so the median lands on batches the scheduler left alone; a whole-run wall clock would add up every disturbance instead.
- `fspy_benchmark_target` (and its static twin) is the workload. It opens missing paths from several threads and prints the median batch time. It does not link fspy. Batches are a few microseconds long, so the `access` median lands on batches the scheduler left alone. The `access-wall` row intentionally includes every disturbance to show end-to-end latency, so expect more variance there.
- `fspy_benchmark_launcher` runs the target once, tracked through `fspy::Command` or untracked, and prints the launch wall clock plus the target's number. It is the only piece that links fspy.
- `fspy_benchmark` is the harness. CI builds the launcher twice: against the fspy under review, and against the merge-base fspy. Both builds use the same launcher source, so both revisions are measured by identical code. The harness then launches both builds back to back, cycling every ordering. Whatever the runner does to the numbers, it does to both.

Expand Down
Loading
Loading