Skip to content

exectop: scoped process-launch monitor - #1

Merged
necco-c merged 1 commit into
mainfrom
add-exectop
Sep 1, 2026
Merged

exectop: scoped process-launch monitor#1
necco-c merged 1 commit into
mainfrom
add-exectop

Conversation

@necco-c

@necco-c necco-c commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

Adds exectop: a scoped process-launch monitor. It shows every program one application starts, folds repetition into one row per command kind, and ranks anything unusual above the rest.

The scaffold (build system, toolchain, kernel-matrix CI, Apache-2.0) is already on main. This PR is the tool.

Reviewer action items

  • Apply the About description (below) — needs repo admin
  • Apply the topic tags (below) — needs repo admin
  • Confirm the kernel matrix passes on 6.1 / 6.6 / 6.12 / bpf-next
  • Sanity-check the supply-chain framing in the README's questions section (see "Positioning" below)
  • Re-record the hero GIF if you want it to show the idle-state and sparkline behavior added after it was captured

About description

Scoped process-launch monitor for Linux: every program one app starts, folded, with the unusual on top

Topic tags

ebpf linux observability yeet showcase tracepoints process-monitoring exec supply-chain build-tools containers cli tui

What it does

Three sched tracepoints maintain one idea: the set of tgids belonging to a traced application, and the stream of execs they perform. sched_process_fork propagates membership and depth to children, sched_process_exit reaps it, and sched_process_exec emits when the task is in scope. That in-kernel propagation is what makes the scope a process tree rather than a pid.

Three ways to name the application:

mode invocation guarantee
Launch ./bin/exectop -- npm ci Complete tree. The target is parked with SIGSTOP until the probe attaches.
Container yeet run . -- --container api cgroup-scoped when one resolves.
Pid yeet run . -- --pid 4242 Carries an attach race, and says so on screen.

Findings worth a look

argv without fighting the verifier. The obvious approach walks the userspace argv pointer array, which is the exact shape the verifier rejects. The kernel already stores the arguments contiguously at mm->arg_start..arg_end as a NUL-separated blob, so one bounded bpf_probe_read_user copies the lot and JS splits it. Compiles and loads clean on 6.12 arm64, no verifier complaints.

The outlier design was wrong and real data corrected it. Rarity-first scoring flagged a third of a real npm install, because 11 of 34 distinct commands in an ordinary build run exactly once. Rarity now gates (count ≤ 3) but never scores: a finding also needs an observed behavior. Verified across nine real workloads, six benign and three adversarial. The benign set includes a build that legitimately curls six times and correctly stays silent.

Two yeet envelope traps, both silent. A char[] field arrives truncated at the first NUL, so a NUL-separated argv yields only argv[0] while the length field still reports the true size. And a map declared with scalar __type(key, __u32) silently drops writes from userspace, because the JS map API serializes through BTF and a scalar has no struct to name. Both cost real debugging time and are documented inline.

bin/exectop is source, not a build artifact. It was covered by the /bin/* ignore rule and nearly shipped missing. Launch mode has to spawn a process and a yeet isolate deliberately cannot, so that wrapper owns the SIGSTOP handoff.

Positioning

Two of the README's eight questions lean on the supply-chain angle (an untrusted npm package, "is this a replacement for Snyk"). That is the strongest retrieval surface the tool has, and it invites being judged as a security detector, which it is not. Three mitigations are in the draft: the Snyk answer says no plainly, "What gets flagged" explains the rarity gate and the false-positive reasoning, and "What it can't see" leads with the limit that matters most: anything that doesn't exec is invisible, so a package doing damage inside Node with fetch() rather than curl produces no rows at all. Worth a second opinion on whether that balance is right.

Naming

Renamed from execsnoop before publishing. That name is Brendan Gregg's bcc tool, a decade old and shipped in bpfcc-tools on every major distro. exectop keeps exec (the vocabulary that shows up in category queries) and takes the honest category noun: it ranks and aggregates rather than streaming a feed. bcc's execsnoop is still named in three places in the README, deliberately, as a referral rather than a leftover.

Testing

test/heuristics.test.mjs runs the folding and the outlier scoring against five recorded captures with no kernel involved (16 assertions). The fixtures are real probe output rather than synthesized, because an earlier synthetic version of this suite passed while the heuristics were badly wrong.

The kernel matrix runs on this PR across 6.1, 6.6, 6.12 and bpf-next.

What changed

Added — the BPF probe, the three-tier TUI, the aggregation and outlier scoring, three scope modes, the launcher, five demo entry points, the regression suite and its fixtures, and the README.

Shows every program one application starts, folds repetition into one row per
command kind, and ranks anything unusual above the rest.

Three sched tracepoints maintain one idea: the set of tgids belonging to a
traced application, and the stream of execs they perform. fork propagates
membership (and depth) to children, exit reaps it, exec emits when the task is
in scope. The fork/exit membership pattern follows agent-lock and omp-jail;
cgroup-first scoping follows hotspot's argument that a per-task attach races
churn and misses anything spawned after you look.

argv is read from mm->arg_start..arg_end, which is already a NUL-separated
blob, rather than walking the userspace argv pointer array. That avoids the
bounded-loop-over-indexed-userspace-pointers the verifier fights, and the
object loads clean on 6.12 arm64 with no verifier complaints.

Three scope modes. Launch mode (bin/exectop -- <cmd>) parks the target with
SIGSTOP until the probe attaches, so the process tree is genuinely complete;
it lives in a wrapper script because a yeet isolate deliberately cannot spawn
a process. Container mode resolves a name to its root pid and cgroup through
the system graph. Pid mode carries an unavoidable attach race, and says so on
screen rather than implying otherwise.

The outlier pass was designed wrong and real data corrected it. Rarity-first
scoring flagged a third of a real npm install, because 11 of 34 distinct
commands in an ordinary build run exactly once. Rarity now gates (count <= 3)
but never scores: a finding also needs an observed behavior such as fetching
from the network, evaluating constructed input, widening permissions, or
touching credential paths. Verified across nine real workloads, six benign and
three adversarial: the benign ones stay silent, including a build that
legitimately curls six times, and the adversarial ones fire.

test/heuristics.test.mjs runs the folding and scoring against five recorded
captures with no kernel involved. The fixtures are real probe output rather
than synthesized: an earlier synthetic version of this suite passed while the
heuristics were badly wrong.
@necco-c

necco-c commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator Author

CI and gh: verification

Kernel matrix: green on all four. 6.1 · 6.6 · 6.12 · bpf-next (run). Every BPF program passes the verifier on each.

yeet run gh: verified end to end in a Lima VM: clone → make (clang + bpftool + esbuild) → attach → render.

 ● exectop   ▏  scope pid 1  ▏  pid-subtree (pre-existing children not tracked)
  no execs in 23s — nothing in this scope has launched a process
  a service that is up and serving usually execs nothing at all

Empty because pid 1 forked nothing in that window, which is the correct reading and the reason the idle state says so explicitly rather than showing a spinner.

Two notes on the mechanism:

  • The pre-merge form is gh:yeet-src/exectop@add-exectop. Plain gh:yeet-src/exectop currently resolves to main, which is the bare scaffold with no BPF source, so bpftool gen object fails with 'object' needs at least 2 arguments. That resolves the moment this merges, and it is also why the kernel-matrix run against main shows red: it built a scaffold with nothing to compile. The run that matters is the one on this PR.
  • #branch is not valid syntax; @ref is.

The README's hero uses the plain post-merge form, which is correct once this lands.

@necco-c
necco-c merged commit 7489cf9 into main Sep 1, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant