Skip to content

Commit a5ecd4d

Browse files
wan9chicodex
andcommitted
feat(fspy): add universal Linux seccomp tracing
Co-authored-by: GPT-5.6 <codex@openai.com>
1 parent c06e6c3 commit a5ecd4d

48 files changed

Lines changed: 4286 additions & 164 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.dockerignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
.git
2+
/node_modules
3+
**/target
4+
.DS_Store

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@ dist
1010
/.vscode/settings.json
1111
*.snap.new
1212
*.md.new
13+
/.docker-host-ca.crt

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# Changelog
22

3+
- **Fixed** Linux automatic file tracking now covers direct syscalls from dynamic programs and across dynamic/static process transitions, while retaining preload acceleration for supported calls.
34
- **Fixed** Windows builds no longer hang on CI when a `node_modules/.bin` `.cmd` shim is routed through PowerShell: the npm/pnpm/yarn `.ps1` wrappers read stdin and block forever on a non-TTY pipe, so the PowerShell rewrite is now skipped when stdin is not an interactive terminal, falling back to the `.cmd` (which never reads stdin) ([#491](https://github.com/voidzero-dev/vite-task/pull/491)).
45
- **Added** First-party support for caching `vite build` with zero cache config, giving Vite projects correct cache hits out of the box ([vitejs/vite#22453](https://github.com/vitejs/vite/pull/22453)).
56
- **Added** Support for specifying tasks from dependency packages in `dependsOn`, such as `dependsOn: [{ "task": "build", "from": "dependencies" }]` ([#479](https://github.com/voidzero-dev/vite-task/pull/479)).

Cargo.lock

Lines changed: 19 additions & 10 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,13 +78,18 @@ fspy_preload_windows = { path = "crates/fspy_preload_windows", artifact = "cdyli
7878
fspy_seccomp_unotify = { path = "crates/fspy_seccomp_unotify" }
7979
fspy_shared = { path = "crates/fspy_shared" }
8080
fspy_shared_unix = { path = "crates/fspy_shared_unix" }
81+
fspy_syscall_intercept = { path = "crates/fspy_syscall_intercept" }
8182
futures = "0.3.31"
8283
futures-util = "0.3.31"
8384
globset = "0.4.18"
8485
jsonc-parser = { version = "0.32.0", features = ["serde"] }
86+
iced-x86 = { version = "1.21.0", default-features = false, features = [
87+
"decoder",
88+
"instr_info",
89+
"std",
90+
] }
8591
libc = "0.2.185"
8692
libtest-mimic = "0.8.2"
87-
memmap2 = "0.9.7"
8893
monostate = "1.0.2"
8994
napi = "3"
9095
napi-build = "2"
@@ -99,6 +104,7 @@ ouroboros = "0.18.5"
99104
owo-colors = { version = "4.1.0", features = ["supports-colors"] }
100105
passfd = { git = "https://github.com/polachok/passfd", rev = "d55881752c16aced1a49a75f9c428d38d3767213", default-features = false }
101106
notify = "8.0.0"
107+
object = { version = "0.37.3", default-features = false, features = ["elf", "read_core", "std"] }
102108
path-clean = "1.0.1"
103109
pathdiff = "0.2.3"
104110
petgraph = "0.8.2"

Dockerfile.linux

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# syntax=docker/dockerfile:1
2+
3+
FROM node:22.19.0-bookworm-slim@sha256:4a4884e8a44826194dff92ba316264f392056cbe243dcc9fd3551e71cea02b90 AS gnu
4+
5+
RUN apt-get update \
6+
&& apt-get install --yes --no-install-recommends build-essential ca-certificates curl git python3 \
7+
&& rm -rf /var/lib/apt/lists/*
8+
9+
ENV CARGO_HOME=/cargo \
10+
CARGO_TARGET_DIR=/workspace/target \
11+
PNPM_HOME=/pnpm \
12+
RUSTUP_HOME=/rustup
13+
ENV PATH="${CARGO_HOME}/bin:${PNPM_HOME}/bin:${PATH}"
14+
15+
WORKDIR /workspace
16+
COPY .docker-host-ca.crt /usr/local/share/ca-certificates/docker-host-ca.crt
17+
RUN if [ -s /usr/local/share/ca-certificates/docker-host-ca.crt ]; then update-ca-certificates; fi
18+
RUN curl --proto '=https' --tlsv1.2 --fail --silent --show-error https://sh.rustup.rs --output /tmp/rustup-init.sh \
19+
&& sh /tmp/rustup-init.sh -y --default-toolchain none \
20+
&& rm /tmp/rustup-init.sh \
21+
&& corepack enable \
22+
&& corepack prepare pnpm@11.1.2 --activate \
23+
&& pnpm config set --global store-dir /pnpm/store
24+
25+
COPY rust-toolchain.toml ./
26+
RUN rustup show \
27+
&& case "$(uname -m)" in \
28+
x86_64) rustup target add x86_64-unknown-linux-musl ;; \
29+
aarch64) rustup target add aarch64-unknown-linux-musl ;; \
30+
*) echo "unsupported architecture: $(uname -m)" >&2; exit 1 ;; \
31+
esac
32+
33+
COPY . .
34+
35+
CMD ["bash", "-ceu", "pnpm install --frozen-lockfile; FSPY_TEST_REQUIRE_ACCELERATION=1 cargo test --locked -p fspy --test syscall_acceleration; cargo test --locked; cargo test --locked -- --ignored"]
36+
37+
FROM node:22.19.0-alpine3.21@sha256:6b2127043c2faa4f15cdcdeb65a39fc9afbecf5559301898b754bbff561a8aa9 AS musl
38+
39+
COPY .docker-host-ca.crt /usr/local/share/ca-certificates/docker-host-ca.crt
40+
RUN sed -i 's|https://|http://|g' /etc/apk/repositories \
41+
&& apk add --no-cache ca-certificates \
42+
&& update-ca-certificates \
43+
&& sed -i 's|http://|https://|g' /etc/apk/repositories \
44+
&& apk add --no-cache bash curl git musl-dev gcc g++ python3
45+
46+
ENV CARGO_HOME=/cargo \
47+
CARGO_TARGET_DIR=/workspace/target \
48+
PNPM_HOME=/pnpm \
49+
RUSTUP_HOME=/rustup \
50+
RUSTFLAGS="--cfg tokio_unstable -D warnings -C target-feature=-crt-static" \
51+
RUST_TEST_THREADS=1
52+
ENV PATH="${CARGO_HOME}/bin:${PNPM_HOME}/bin:${PATH}"
53+
54+
WORKDIR /workspace
55+
RUN curl --proto '=https' --tlsv1.2 --fail --silent --show-error https://sh.rustup.rs --output /tmp/rustup-init.sh \
56+
&& sh /tmp/rustup-init.sh -y --default-toolchain none \
57+
&& rm /tmp/rustup-init.sh \
58+
&& corepack enable \
59+
&& corepack prepare pnpm@11.1.2 --activate \
60+
&& pnpm config set --global store-dir /pnpm/store
61+
62+
COPY rust-toolchain.toml ./
63+
RUN rustup show
64+
65+
COPY . .
66+
67+
CMD ["bash", "-ceu", "pnpm install --frozen-lockfile; cargo test --locked; cargo test --locked -- --ignored"]

crates/fspy/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ ouroboros = { workspace = true }
1818
rustc-hash = { workspace = true }
1919
tempfile = { workspace = true }
2020
thiserror = { workspace = true }
21-
tokio = { workspace = true, features = ["net", "process", "io-util", "sync", "rt"] }
21+
tokio = { workspace = true, features = ["net", "process", "io-util", "sync", "rt", "macros"] }
2222
tokio-util = { workspace = true }
2323
which = { workspace = true, features = ["tracing"] }
2424

crates/fspy/README.md

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,13 @@
22

33
Run a command and capture all the paths it tries to access.
44

5-
## macOS/Linux (glibc) implementation
5+
## macOS implementation
66

7-
It uses `DYLD_INSERT_LIBRARIES` on macOS and `LD_PRELOAD` on Linux to inject a shared library that intercepts file system calls.
8-
The injection process is almost identical on both platforms other than the environment variable name. The implementation is in `src/unix`.
7+
It uses `DYLD_INSERT_LIBRARIES` to inject a shared library that intercepts file system calls.
98

10-
## Linux-specific implementation for fully static binaries
9+
## Linux implementation
1110

12-
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`.
11+
Linux installs one inherited `seccomp_unotify` filter for the complete task process tree. Dynamically linked executables also receive an `LD_PRELOAD` library that accelerates safely patchable syscall sites. Static binaries, direct syscalls, and every declined acceleration case continue through seccomp.
1312

1413
## Linux musl implementation
1514

crates/fspy/src/lib.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,11 @@ pub struct ChildTermination {
3232
pub status: ExitStatus,
3333
/// The path accesses captured from the child process.
3434
pub path_accesses: PathAccessIterable,
35+
/// The error that made filesystem tracing incomplete, if any.
36+
///
37+
/// The target process is allowed to continue after a seccomp notification
38+
/// decoding error, so its exit status remains available to callers.
39+
pub tracing_error: Option<io::Error>,
3540
}
3641

3742
pub struct TrackedChild {

crates/fspy/src/unix/mod.rs

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ use fspy_shared_unix::payload::Artifacts;
1616
use fspy_shared_unix::{
1717
exec::ExecResolveConfig,
1818
payload::{Payload, encode_payload},
19-
spawn::handle_exec,
19+
spawn::handle_root_exec,
2020
};
2121
use futures_util::FutureExt;
2222
#[cfg(target_os = "linux")]
@@ -28,6 +28,11 @@ use tokio_util::sync::CancellationToken;
2828
use crate::ipc::{OwnedReceiverLockGuard, SHM_CAPACITY};
2929
use crate::{ChildTermination, Command, TrackedChild, arena::PathAccessArena, error::SpawnError};
3030

31+
// Reserved for the preload syscall gate. The payload communicates the exact post-syscall PC so
32+
// the injected library does not need to duplicate this address.
33+
#[cfg(target_os = "linux")]
34+
const GATE_POST_SYSCALL_PC: u64 = 0x0000_0001_0000_0008;
35+
3136
#[derive(Debug)]
3237
pub struct SpyImpl {
3338
#[cfg(target_os = "macos")]
@@ -75,7 +80,8 @@ impl SpyImpl {
7580
cancellation_token: CancellationToken,
7681
) -> Result<TrackedChild, SpawnError> {
7782
#[cfg(target_os = "linux")]
78-
let supervisor = supervise::<SyscallHandler>().map_err(SpawnError::Supervisor)?;
83+
let supervisor =
84+
supervise::<SyscallHandler>(GATE_POST_SYSCALL_PC).map_err(SpawnError::Supervisor)?;
7985

8086
#[cfg(not(target_env = "musl"))]
8187
let (ipc_channel_conf, ipc_receiver) =
@@ -99,7 +105,7 @@ impl SpyImpl {
99105

100106
let mut exec = command.get_exec();
101107
let mut exec_resolve_accesses = PathAccessArena::default();
102-
let pre_exec = handle_exec(
108+
let pre_exec = handle_root_exec(
103109
&mut exec,
104110
ExecResolveConfig::search_path_enabled(None),
105111
&encoded_payload,
@@ -149,13 +155,18 @@ impl SpyImpl {
149155
let arenas = std::iter::once(exec_resolve_accesses);
150156
// Stop the supervisor and collect path accesses from it.
151157
#[cfg(target_os = "linux")]
152-
let arenas = arenas.chain(
153-
supervisor
154-
.stop()
155-
.await?
156-
.into_iter()
157-
.map(syscall_handler::SyscallHandler::into_arena),
158-
);
158+
let supervisor_outcome = supervisor.stop().await;
159+
#[cfg(target_os = "linux")]
160+
let tracing_error = supervisor_outcome.error;
161+
#[cfg(target_os = "linux")]
162+
let supervisor_arenas = supervisor_outcome
163+
.handlers
164+
.into_iter()
165+
.map(syscall_handler::SyscallHandler::into_arena);
166+
#[cfg(target_os = "linux")]
167+
let arenas = arenas.chain(supervisor_arenas);
168+
#[cfg(not(target_os = "linux"))]
169+
let tracing_error = None;
159170
let arenas = arenas.collect::<Vec<_>>();
160171

161172
// Lock the ipc channel after the child has exited.
@@ -169,7 +180,7 @@ impl SpyImpl {
169180
ipc_receiver_lock_guard,
170181
};
171182

172-
io::Result::Ok(ChildTermination { status, path_accesses })
183+
io::Result::Ok(ChildTermination { status, path_accesses, tracing_error })
173184
})
174185
.map(|f| f?) // flatten JoinError and io::Result
175186
.boxed(),

0 commit comments

Comments
 (0)