Skip to content

feat(agent): process-exec probe — fentry on bprm_check_security (JEF-53)#26

Merged
thejefflarson merged 1 commit into
mainfrom
feat/jef-53-process-exec-probe
Jun 21, 2026
Merged

feat(agent): process-exec probe — fentry on bprm_check_security (JEF-53)#26
thejefflarson merged 1 commit into
mainfrom
feat/jef-53-process-exec-probe

Conversation

@thejefflarson

Copy link
Copy Markdown
Owner

What

A new eBPF probe that captures process execs and emits a new Behavior::ProcessExec { path } — the runtime signal for "unexpected process spawned" (Falco-rule parity, ADR-0014, JEF-53).

How

eBPF probe (agent/protector-agent-ebpf/src/main.rs): fentry on bprm_check_security(struct linux_binprm *bprm) — the LSM exec hook, fired on every execve once the new binary is resolved. Reads bprm->filename (a kernel char *) directly with bpf_probe_read_kernel_str — the same technique as the library-load probe, and deliberately not bpf_d_path (the verifier rejects it in hooks off the kernel's d_path allowlist — JEF-68). Emits a FileEvent carrying the exec'd path under a new KIND_EXEC = 4. On ring-reserve failure it calls record_drop() (JEF-58).

behavior (behavior/src/lib.rs): new Behavior::ProcessExec { path }; summary"executed {path}"; fingerprint_key coarsens to the basename (exec:{basename}) so repeated execs of the same binary collapse to one stable key and don't bust the verdict cache (mirrors how LibraryLoaded keys on the lib name, not the full path).

userspace (agent/protector-agent/src/observer.rs): a RawEvent::Exec { pid, path } variant, a KIND_EXEC decode arm, into_behaviorProcessExec, and the bprm_check fentry added to the best-effort attach table. All on the JEF-64 decoupled drain/worker path — no blocking I/O reintroduced on the drain.

engine (engine/src/engine/reason/proof.rs): corroborates() gets a non-corroborating ProcessExec arm. Wiring exec → corroboration is explicitly out of scope (JEF-49).

Validation

  • cargo +nightly check on the eBPF crate — clean.
  • docker build -f agent/Dockerfile --target builderEXIT 0 (compiles + links the eBPF object incl. the new probe with bpf-linker, and the feature-gated --features ebpf userspace).
  • Workspace: cargo build, cargo test, cargo clippy --all-targets -- -D warnings, cargo fmt — all green.
  • Agent crate: cargo build, cargo test, cargo fmt — all green.

Tests

  • behavior: ProcessExec fingerprint coarsens to basename (exec:bash from both /usr/bin/bash and /bin/bash) + summary.
  • observer: a KIND_EXEC FileEvent decodes to RawEvent::Exec and maps to Behavior::ProcessExec with the basename fingerprint.

Concerns

  • Verifier-accept is only confirmable on-node (the JEF-68 risk). The Docker builder confirms the probe compiles and links, but eBPF verifier acceptance can only be confirmed when loaded against a real kernel's BTF. The fentry attach is best-effort (a load/verify failure is logged and skipped, leaving the other probes running), so a verifier reject degrades gracefully rather than crashing the agent.
  • Hook symbol: attached to bprm_check_security (the per-LSM callback). If that symbol isn't fentry-attachable on the target 6.8 kernel, the alternative is the wrapper security_bprm_check — a one-line change in the FENTRY_PROBES table + the #[fentry(function = ...)] attribute.

🤖 Generated with Claude Code

Add an eBPF process-exec probe and a Behavior::ProcessExec { path } — the
runtime signal for "unexpected process spawned" (Falco-rule parity, ADR-0014).

eBPF: fentry on bprm_check_security(struct linux_binprm *bprm), fired on every
execve once the binary is resolved. Reads bprm->filename (a kernel char*)
directly with bpf_probe_read_kernel_str — NOT bpf_d_path, which the verifier
rejects in hooks off the d_path allowlist (JEF-68). Emits a FileEvent with the
new KIND_EXEC = 4 (path carries the exec'd binary). On reserve failure calls
record_drop() (JEF-58).

behavior: new Behavior::ProcessExec; summary "executed {path}"; fingerprint_key
coarsens to the basename ("exec:{basename}") so exec churn doesn't bust the
verdict cache (mirrors LibraryLoaded's stable key).

userspace (observer.rs): RawEvent::Exec, a KIND_EXEC decode arm, into_behavior
→ ProcessExec, and the bprm_check fentry in the best-effort attach table — all
on the JEF-64 decoupled drain/worker path (no blocking I/O reintroduced).

engine: proof.rs corroborates() gets a non-corroborating ProcessExec arm —
wiring exec → corroboration is out of scope (JEF-49).

Tests: behavior fingerprint/summary basename test; observer KIND_EXEC →
RawEvent::Exec → ProcessExec decode test. Verifier-accept is only confirmable
on-node (the JEF-68 risk); the Docker builder confirms it compiles + links.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VtjoJttCvBY4dzCoE4f9vP
@thejefflarson thejefflarson force-pushed the feat/jef-53-process-exec-probe branch from 76dd0f8 to 28caa04 Compare June 21, 2026 18:54
@thejefflarson thejefflarson merged commit bcbbfe8 into main Jun 21, 2026
4 checks passed
@thejefflarson thejefflarson deleted the feat/jef-53-process-exec-probe branch June 21, 2026 18:55
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