Skip to content

Commit 6ef30b2

Browse files
bmiddhaCopilot
andcommitted
fix: align eBPF tracing with ptrace behavior
Drop metadata-only syscall probes disabled by the legacy production tracer, add opt-in Rust and eBPF profiling, and preserve successful exec paths with paired entry/exit capture. Extend VM coverage for execve, execveat, secondary-thread exec, and the reduced syscall set. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: b3fe3fcd-33e7-4ea3-b563-8492847b2469
1 parent 5e98c71 commit 6ef30b2

17 files changed

Lines changed: 1138 additions & 306 deletions

File tree

docs/configuration.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,5 +40,7 @@ Multiple paths can be specified by separating them with a colon `:`.
4040
- `FSTRACE_LOG_FILE`: Write log output to this explicit file path (all binaries). See [Logging](logging.md).
4141
- `FSTRACE_LOG_DIR`: Write log output into this directory, one file per process (`fstrace-daemon.log`, `fstrace-<pid>.log`), so the daemon and concurrent clients never interleave.
4242
- `FSTRACE_DEBUG_FILE`: Legacy alias for `FSTRACE_LOG_FILE` (single shared file).
43+
- `FSTRACE_PROFILE`: Set to `1` in both daemon and client environments to emit
44+
aggregate Rust and eBPF performance summaries. See [Logging](logging.md).
4345
- `FSTRACE_REPORT_FILE`: Write the report stream to this path instead of file descriptor 3. Recommended when running under `sudo` (see [Reports and file descriptor 3](reports-and-fd3.md)).
4446
- `FSTRACE_SOCKET`: Path to the daemon's Unix socket (default `/run/fstrace/fstrace.sock`). Both the daemon and the client honour it; set it to run a private daemon.

docs/logging.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,27 @@ RUST_LOG=fstrace=trace FSTRACE_LOG_DIR=/var/log/fstrace fstrace ls / 3>/dev/null
3838

3939
`FSTRACE_LOG_DIR` is created if it does not exist. `FSTRACE_DEBUG_FILE` remains
4040
supported as a legacy alias for `FSTRACE_LOG_FILE`.
41+
42+
## Aggregate performance profiling
43+
44+
Set `FSTRACE_PROFILE=1` in both the daemon and client environments to emit
45+
low-volume timing summaries instead of one log entry per event:
46+
47+
```bash
48+
sudo env FSTRACE_PROFILE=1 FSTRACE_LOG_FILE=/tmp/fstrace-daemon.profile \
49+
fstrace-daemon &
50+
FSTRACE_PROFILE=1 FSTRACE_LOG_FILE=/tmp/fstrace-client.profile \
51+
fstrace npm run build 3>/dev/null
52+
```
53+
54+
The client summary separates event classification/path resolution, filtering,
55+
debouncing, report formatting/writes, queue latency, and post-exit draining.
56+
The daemon summary covers event encoding/routing, queue drops, wire bytes, and
57+
socket writes. The eBPF summary reports per-syscall capture time, pathname-copy
58+
time and bytes, successful ring-buffer submissions, and ring-buffer drops.
59+
Per-syscall detail is sorted by total processing time.
60+
61+
Profiling is opt-in because it reads clocks and updates counters for every
62+
traced event. Normal runs do not collect those timings. eBPF interval snapshots
63+
are global to the daemon, so profile one client at a time when exact attribution
64+
matters.

docs/output-format.md

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,16 @@ File access reports are printed to file descriptor 3 in the following format:
66
<access_type><file_type> <file_path>
77
```
88

9-
- `<access_type>`: `R` for read, `W` for write/create, `D` for delete, `E` for enumerate
10-
- `<file_type>`: `F` for file, `D` for directory, `X` for does not exist
9+
- `<access_type>`: `R` for read, `W` for write/create, `D` for delete. `E` is
10+
reserved for directory enumeration.
11+
- `<file_type>`: `F` for file, `D` for directory, `X` for does not exist, `L`
12+
for symbolic link, `?` when the type is unknown.
13+
14+
fstrace intentionally does not attach probes for the metadata-only
15+
`stat`/`access`/`readlink` families or for `getdents` directory enumeration.
16+
Those calls dominate dependency-resolution workloads while duplicating paths
17+
already reported by `open`/`openat`; omitting them matches the original
18+
ptrace tracer's production seccomp filter.
1119

1220
See [Reports and file descriptor 3](reports-and-fd3.md) for how to redirect the
1321
report stream (and how to handle `sudo`).

docs/testing.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ cargo xtask test # or: cargo test --release --workspace --exclude fstrace
1010
Run `cargo xtask check` to reproduce the full CI lint/test gate locally
1111
(format check + clippy + unit tests).
1212

13-
The privileged loader and full syscall coverage are exercised end-to-end inside a
13+
The privileged loader and enabled syscall set are exercised end-to-end inside a
1414
throwaway QEMU VM, so the eBPF programs never load on the host. The harness is
1515
written entirely in Rust: `xtask` drives the VM orchestration, the `fstrace-vmtest`
1616
crate provides the in-guest suite, and a thin companion binary, `fstrace-scenario`,

fstrace-common/src/lib.rs

Lines changed: 66 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ pub const EVENT_FORK: u32 = 0xFFFF_0001;
2525
/// pid from its routing table.
2626
pub const EVENT_EXIT: u32 = 0xFFFF_0002;
2727

28+
/// Number of profile slots, covering every stable [`Syscall`] wire value up to
29+
/// the highest enabled discriminant (including reserved gaps).
30+
pub const SYSCALL_COUNT: usize = 33;
31+
2832
/// The set of syscalls fstrace traces.
2933
///
3034
/// The discriminants are stable wire values written into [`Event::syscall`] by
@@ -37,15 +41,6 @@ pub enum Syscall {
3741
Openat = 1,
3842
Openat2 = 2,
3943
Creat = 3,
40-
Stat = 4,
41-
Lstat = 5,
42-
Newfstatat = 6,
43-
Statx = 7,
44-
Access = 8,
45-
Faccessat = 9,
46-
Faccessat2 = 10,
47-
Readlink = 11,
48-
Readlinkat = 12,
4944
Unlink = 13,
5045
Unlinkat = 14,
5146
Rmdir = 15,
@@ -59,8 +54,6 @@ pub enum Syscall {
5954
Symlink = 23,
6055
Symlinkat = 24,
6156
Truncate = 25,
62-
Getdents = 26,
63-
Getdents64 = 27,
6457
Chdir = 28,
6558
Fchdir = 29,
6659
Execve = 30,
@@ -77,15 +70,6 @@ impl Syscall {
7770
1 => Openat,
7871
2 => Openat2,
7972
3 => Creat,
80-
4 => Stat,
81-
5 => Lstat,
82-
6 => Newfstatat,
83-
7 => Statx,
84-
8 => Access,
85-
9 => Faccessat,
86-
10 => Faccessat2,
87-
11 => Readlink,
88-
12 => Readlinkat,
8973
13 => Unlink,
9074
14 => Unlinkat,
9175
15 => Rmdir,
@@ -99,8 +83,6 @@ impl Syscall {
9983
23 => Symlink,
10084
24 => Symlinkat,
10185
25 => Truncate,
102-
26 => Getdents,
103-
27 => Getdents64,
10486
28 => Chdir,
10587
29 => Fchdir,
10688
30 => Execve,
@@ -110,8 +92,62 @@ impl Syscall {
11092
};
11193
Some(syscall)
11294
}
95+
96+
/// Stable lowercase name used in diagnostics and profile output.
97+
pub const fn name(self) -> &'static str {
98+
use Syscall::*;
99+
match self {
100+
Open => "open",
101+
Openat => "openat",
102+
Openat2 => "openat2",
103+
Creat => "creat",
104+
Unlink => "unlink",
105+
Unlinkat => "unlinkat",
106+
Rmdir => "rmdir",
107+
Rename => "rename",
108+
Renameat => "renameat",
109+
Renameat2 => "renameat2",
110+
Mkdir => "mkdir",
111+
Mkdirat => "mkdirat",
112+
Link => "link",
113+
Linkat => "linkat",
114+
Symlink => "symlink",
115+
Symlinkat => "symlinkat",
116+
Truncate => "truncate",
117+
Chdir => "chdir",
118+
Fchdir => "fchdir",
119+
Execve => "execve",
120+
Execveat => "execveat",
121+
Close => "close",
122+
}
123+
}
113124
}
114125

126+
/// Per-syscall counters populated by the eBPF capture path when profiling is
127+
/// enabled. The map is per-CPU, so these fields can be updated without atomic
128+
/// operations and summed by the daemon after a client disconnects.
129+
#[repr(C)]
130+
#[derive(Copy, Clone, Debug, Default)]
131+
pub struct EbpfProfileStat {
132+
/// Traced syscall exits that entered the capture path.
133+
pub calls: u64,
134+
/// Events successfully submitted to the ring buffer.
135+
pub submitted: u64,
136+
/// Events dropped because the ring buffer had no free reservation.
137+
pub ringbuf_drops: u64,
138+
/// Userspace pathname reads attempted.
139+
pub path_reads: u64,
140+
/// Path bytes copied from userspace.
141+
pub path_bytes: u64,
142+
/// Nanoseconds in capture after pid selection and profile enablement.
143+
pub capture_ns: u64,
144+
/// Nanoseconds within `capture_ns` spent in pathname read helpers.
145+
pub path_read_ns: u64,
146+
}
147+
148+
#[cfg(feature = "user")]
149+
unsafe impl aya::Pod for EbpfProfileStat {}
150+
115151
/// How a path was accessed. Serialised as the first output character.
116152
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
117153
pub enum AccessType {
@@ -225,10 +261,17 @@ mod tests {
225261

226262
#[test]
227263
fn syscall_roundtrips_through_u32() {
228-
for value in 0..=32u32 {
264+
for value in [
265+
0, 1, 2, 3, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 28, 29, 30, 31, 32,
266+
] {
229267
let syscall = Syscall::from_u32(value).expect("known discriminant");
230268
assert_eq!(syscall as u32, value);
231269
}
270+
for value in 4..=12 {
271+
assert_eq!(Syscall::from_u32(value), None);
272+
}
273+
assert_eq!(Syscall::from_u32(26), None);
274+
assert_eq!(Syscall::from_u32(27), None);
232275
assert_eq!(Syscall::from_u32(33), None);
233276
assert_eq!(Syscall::from_u32(u32::MAX), None);
234277
}

0 commit comments

Comments
 (0)