Skip to content

feat(bpf): userspace ring-buffer reader + decoder + per-probe attach + gone-pid guard fix#63

Merged
ebenali merged 2 commits into
feat/v04-hybrid-bpf-progfrom
feat/v04-hybrid-reader
Jun 19, 2026
Merged

feat(bpf): userspace ring-buffer reader + decoder + per-probe attach + gone-pid guard fix#63
ebenali merged 2 commits into
feat/v04-hybrid-bpf-progfrom
feat/v04-hybrid-reader

Conversation

@ebenali

@ebenali ebenali commented Jun 19, 2026

Copy link
Copy Markdown
Member

Wires the kernel birth program to userspace. Stacked on #62 — GitHub will retarget this to master once #62 merges.

What

  • backend/linux/BpfBirthReader.{h,cpp} (gated on QIFTOP_HAVE_BPF): loads the CO-RE skeleton, attaches the fexit/fentry probes, drains the ring buffer on a dedicated std::thread, feeds each birth to a Sink. Routes libbpf logging into the verbose category.
  • backend/linux/BirthDecode.h: the PURE wire-event → BirthKey/BirthRecord decoder (no kernel/libbpf, always built + tested). Network-order bytes → QHostAddress (v4/v6), host-order ports, IANA proto → L4Proto, direction byte → Direction, start_boottime_ns/proc field-22 clock ticks via sysconf(_SC_CLK_TCK). Pinned by test_birth_decode.
  • Per-probe tolerant attach: each program attached individually (bpf_program__attach); the ones that take are kept, so a kernel where one traced function is renamed/inlined/non-attachable still yields births from the rest. start() succeeds if ≥1 attaches (logs attached N/M probes). We own + free the links.
  • Correctness fixes to the merged scaffolding:
    • start_timestart_boottime capture (the field /proc field-22 derives from — verified against fs/proc/array.c), so the PID-reuse guard's equality check is sound.
    • PID-reuse guard now serves a GONE pid (live==0) instead of rejecting it — that's exactly the short-lived process the hybrid exists to catch. Rejects only when a DIFFERENT live process holds the pid. Test flipped (goneShortLivedPidIsServed).

Validation

  • Unit: full build + 44/44 tests + clang-tidy gate clean (HAVE_BPF on); skip-safe off-build verified (reader → empty TU).
  • Graceful degradation proven on a BTF-less kernel (the dev box): start() returns false + inert, no crash, both unprivileged (memlock EPERM) and root (BTF missing).
  • End-to-end on a real BTF kernel (Vagrant Ubuntu 6.8) — built with BPF on, ran a load harness:
    bpf birth: attached 4/4 probes; draining ring buffer
    [OURS] pid=3297 comm=bpf_vmtest dir=outbound 192.168.121.122:43030 -> 1.1.1.1:80 proto=Tcp
           pid=3300 comm=curl       dir=outbound ...:43046 -> 1.1.1.1:80
    RESULT: total_births=3  ours=2  exit 0
    
    Our own connect()s attributed to the right pid/comm/tuple/direction, and the short-lived curl child captured — the conntrack gap, closed.

Next

Factory wiring (construct BpfBirthResolver first, point a reader at its onBirth, setLoaded(true) only on a successful start(), advertise the birth-attribution token) is the follow-up PR. Contract documented in AGENTS.md §8b.

ebenali and others added 2 commits June 19, 2026 13:45
…ard fix

Wires the kernel side of the birth+conntrack hybrid to userspace:

- backend/linux/BpfBirthReader.{h,cpp} (gated on QIFTOP_HAVE_BPF): loads the
  CO-RE skeleton (birth.skel.h), attaches the fexit/fentry probes, and drains
  the ring buffer on a dedicated std::thread, feeding each birth to a Sink
  callback. Skip-safe — start() returns false and stays inert on any kernel
  without BTF / trampolines / CAP_BPF, so the chain runs conntrack-only. Routes
  libbpf logging into the verbose category.
- backend/linux/BirthDecode.h: the PURE wire-event → BirthKey/BirthRecord
  decoder (no kernel/libbpf, so it's always built + tested). Network-order
  bytes → QHostAddress (v4/v6), host-order ports, IANA proto → L4Proto,
  direction byte → Direction, and start_boottime_ns → /proc field-22 clock
  ticks via sysconf(_SC_CLK_TCK). test_birth_decode pins the byte-order/units
  contract with hand-built events.
- CMake: backend_linux gains BpfBirthReader.cpp + links libbpf (gated).

Correctness fix to the merged BpfBirthResolver PID-reuse guard: it rejected
when live==0 (pid gone), which would reject exactly the short-lived processes
the hybrid exists to capture (they've exited by the time the conntrack flow
resolves). Now it serves a gone pid and rejects ONLY when a DIFFERENT live
process holds the pid (live!=0 && live!=captured). Test flipped accordingly
(missingPidStarttimeIsRejected → goneShortLivedPidIsServed).

Also fixes the captured starttime field (start_time → start_boottime, the one
/proc field 22 derives from) so the guard's equality check is sound.

AGENTS.md §8b documents the reader, the decoder units contract, and the
gone-pid guard semantics. Validated: full build + 44/44 tests + clang-tidy
gate clean (HAVE_BPF on, toolchain present); skip-safe off-build verified.
Factory wiring (construct resolver first, point reader at it, advertise the
birth-attribution token) is the next PR.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Attach each eBPF program individually (bpf_program__attach over
bpf_object__for_each_program) and keep the ones that take, instead of the
skeleton's all-or-nothing qiftop_birth__attach. A kernel where one traced
function is renamed/inlined/non-attachable (or hits the trampoline limit)
still yields birth attribution from the probes that DID attach; start()
succeeds when >=1 probe attaches and logs "attached N/M probes". We own the
returned bpf_links and destroy them in stop().

Load stays whole-object on purpose: CO-RE relocation + verification is
per-object, so if load fails nothing is attachable anyway. Still fully
skip-safe — zero probes attached tears down and returns false (conntrack-only).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@ebenali
ebenali merged commit 701ef1e into feat/v04-hybrid-bpf-prog Jun 19, 2026
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