Skip to content

File each lost observation against the packet it belonged to - #18

Merged
copyleftdev merged 1 commit into
mainfrom
attribute-loss-to-packets
Aug 8, 2026
Merged

File each lost observation against the packet it belonged to#18
copyleftdev merged 1 commit into
mainfrom
attribute-loss-to-packets

Conversation

@copyleftdev

Copy link
Copy Markdown
Owner

Supersedes #16, which GitHub auto-closed when its base branch was deleted as part of merging #15. Same content, rebuilt on top of main — see the note at the end.

Evidence change

#15 narrowed a hole to a probe, but left every absence unusable. Knowing ip_rcv failed to emit somewhere says nothing about whether a particular packet's missing hop was never reached or merely never observed, so complete: false still downgraded every absence in the capture.

This files each hole against the packet it belonged to. Two packets from the same globally-incomplete capture (16,382 reserve failures), on a live kernel:

$ skbx explain light.jsonl event:...38e8
  "evidence": {"verdict": "lost", "observations_lost": 10}     6 hops observed
$ skbx explain light.jsonl event:...08e8
  "evidence": {"verdict": "complete"}                         10 hops observed

For the second packet, a function absent from those 10 hops was never reached. That is "this function was never hit" separated from "we lost that one" — per packet, inside a capture that is not complete overall.

Closes the question from the DEV thread that prompted #15.

Why a plain hash, not an LRU

This is the load-bearing decision. A full LRU evicts silently, and a silently evicted entry would make a packet look like it lost nothing — turning the feature into a lie under exactly the load that makes it matter. A plain hash refuses the insert and counts the refusal in kernel_skb_loss_unattributed.

That refusal is what makes the negative claim provable rather than merely likely: while the counter is zero the table is exhaustive, so a packet absent from it lost nothing. An LRU would trade that proof for a bounded amount of memory.

Reserve failures are the only loss a packet can own

loss_is_fully_attributed() requires the skb table to be exhaustive and kernel_recursion_misses, userspace_decode_failures, userspace_enrichment_failures and output_failures to all be zero. A recursion miss never reaches the emit path; a decode, enrichment or output failure discards a record after the kernel handed it over, when nothing knows which packet it described. Any of those leaves a hole no packet can be cleared of.

Read failures are deliberately excluded — they degrade fields on an event that was still emitted, and are already visible on it.

An earlier draft checked only the skb table and would have cleared packets in captures with output loss. Test loss_that_no_packet_owns_defeats_attribution pins each case.

Design note: this is not what the follow-up in #15 described

#15 proposed per-skb emit sequencing — stamp a counter into every record, infer loss from gaps. The identity turns out to be in scope at all 16 failure sites, which makes recording the loss directly strictly better:

sequencing this
hot-path cost map op on every event failure path only
record ABI grows 8 bytes unchanged
lost trailing event invisible, chain just ends recorded
wholly-lost packet invisible, no records to inspect recorded
opt-in required yes no

The trailing-event blind spot decided it: #15 already noted sequencing could not see a lost last hop. This has no such gap, and needs no flag because it costs nothing until something is already failing.

Packets that were previously invisible

The first draft of the summary line printed 363 of 320 packets affected. Not a formatting bug — 104 packets lost every observation, so they appear in the ledger but nowhere in the capture. A wholly-dropped chain previously left no trace at all. Replay now reports these as skbs_lost_entirely, separate from distinct_skbs, which counts only packets actually observed.

Change design

  1. Kernel fact — which observation of which packet was dropped, keyed by the identity already stamped on every event, so it joins directly against a replayed chain.
  2. Target validation — no new attachment surface; the identity is already resolved at every failure site.
  3. Bounds — one BPF_MAP_TYPE_HASH, 4096 entries, preallocated. Written only on the failure path. Overflow is counted, never silent.
  4. Failure visibilitykernel_skb_loss_unattributed is the single gate on whether any packet may be cleared; non-zero collapses the claim for all of them at once.
  5. Checks — below.
  6. Schema — append-only. New fields are #[serde(default)] and absent from required; the pre-existing fixture still parses and replays.

Checks run

  • make check and make build, both exit 0.
  • 14 new tests covering the exhaustiveness gate, each loss kind that defeats it, read failures that do not, the three explain verdicts, missing-footer, and entirely-lost packets.
  • Live kernel 6.17.0-41-generic, 64 CPUs, across all three regimes: light loss (table under capacity, unattributed=0) → per-packet complete/lost; heavy loss (table full at 4096, 11,866 unfiled) → unknown for every packet; lossless → complete. Filed + unfiled reconciled against kernel_reserve_failures exactly.

Review fixes carried over

CodeRabbit's findings on #15 apply to this field too and are included:

  • skb_loss() collects through a map keyed by site rather than a list. keys() is bpf_map_get_next_key() iteration, which can revisit a key when entries are inserted while it runs, and probes keep firing throughout. A duplicate matters more here than in the per-probe table: it would put the same packet in the ledger twice, and that ledger is what a reader consults to decide whether a packet lost anything.
  • kernel_loss_by_skb is a parameter of into_reliability rather than a field the caller patches in afterwards, so it cannot be left unset.

Untested

Unchanged from #15: the TC/XDP program-site path never executes here. Now verified live. Both paths were exercised under forced loss in isolated network namespaces, and the per-packet ledger carries the program id the same way the per-probe table does:

"kernel_loss_by_skb": [{"skb": "0xffff8ac285589f00", "function": null, "program_id": 432, "reserve_failures": 1}]

explain rules correctly on TC-traced packets from a lossy capture — {"verdict": "lost", "observations_lost": 1} for an affected packet, {"verdict": "complete"} for one the ledger clears.

Full live suite: 13/14 pass. live-bpf-helper fails identically on main at b378c01, so it is pre-existing on this host, not caused by this change.

4096 entries is sized for the light-loss case this feature is for. Under heavy loss the table fills and every verdict degrades to unknown, which is correct but means the guarantee is only available when loss is bounded. That ceiling is a judgement call, not a measurement.

🤖 Generated with Claude Code


Note on how this branch was rebuilt

#15 was squash-merged, so its original commits are not in main's history and this branch could not simply retarget — GitHub closed the original PR when the base branch went away.

The branch was rebuilt by replaying this change's net delta onto the new main. Verified equivalent: git diff between the rebuilt branch and the reviewed one is empty across crates/ and docs/architecture.md. The only difference is the presence of #17's script and validation-doc changes, which the rebuilt branch inherits from main and the old branch predated.

make check passes on the rebuilt branch, and the live gates were re-run after the review fixes: live-tc-program, live-xdp-program, live-stack-lifetime, live-metadata, live-rotation all pass, plus a fresh lossy capture confirming no duplicate entries in either breakdown (2 probe entries / 2 distinct, 1427 packet entries / 1427 distinct, kernel_skb_loss_unattributed = 0).

Attributing a hole to a probe narrowed where to look but left every
absence unusable: knowing that ip_rcv failed to emit somewhere says
nothing about whether a particular packet's missing hop was never
reached or merely never observed.

The identity of the packet is already in scope wherever a reserve
fails, so record the loss itself rather than inferring it later: a
table keyed by packet and probe, written only on the failure path.

Deliberately a plain hash and not an LRU. A full LRU evicts silently,
and a silently evicted entry would make a packet look like it lost
nothing, which is exactly the claim this table exists to support. A
plain hash refuses the insert and counts the refusal instead. While
that count is zero the table is exhaustive, so a packet absent from it
provably lost nothing and a function missing from its chain was never
reached. explain now rules per packet: complete, lost, or unknown.

Reserve failures are the only loss kind a packet can own. A recursion
miss never reaches the emit path, and a decode, enrichment or output
failure discards a record after the kernel has handed it over, by which
point nothing knows which packet it described. Any of those leaves a
hole no packet can be cleared of, so clearing any packet requires all
of them to be zero.

A packet can now appear in the ledger while appearing nowhere in the
capture. That is a packet whose every observation was dropped, which
previously left no trace at all; replay counts these separately from
the packets it actually observed.

Carries the review fixes applied to the parent branch through to the
new field: skb_loss dedupes revisited keys from bpf_map_get_next_key
iteration, and both breakdowns are parameters of into_reliability so
neither can be left unset by a future call site.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 8, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@copyleftdev, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 23 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: fa573ab1-852e-40ec-a9fd-755770be4818

📥 Commits

Reviewing files that changed from the base of the PR and between c95ee16 and b3dc1cd.

📒 Files selected for processing (8)
  • crates/skbx-cli/src/main.rs
  • crates/skbx-contract/src/lib.rs
  • crates/skbx-core/src/replay.rs
  • crates/skbx-sensor/bpf/skbx.bpf.c
  • crates/skbx-sensor/src/lib.rs
  • crates/skbx-sensor/src/live.rs
  • crates/skbx-sensor/src/raw.rs
  • docs/architecture.md

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@copyleftdev
copyleftdev merged commit de61bfd into main Aug 8, 2026
5 checks passed
@copyleftdev
copyleftdev deleted the attribute-loss-to-packets branch August 8, 2026 02:46
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