Skip to content

Fix BPF verifier "zero-sized read" rejection on Linux 6.12+#5

Open
jevansnyc wants to merge 1 commit into
masterfrom
fix/bpf-skb-load-bytes-zero-sized-read
Open

Fix BPF verifier "zero-sized read" rejection on Linux 6.12+#5
jevansnyc wants to merge 1 commit into
masterfrom
fix/bpf-skb-load-bytes-zero-sized-read

Conversation

@jevansnyc

Copy link
Copy Markdown

Problem

make && yeet run . fails to load the eBPF program on newer kernels. On Linux 6.12.73 (aarch64) the verifier rejects the capture read with:

R4 invalid zero-sized read: u64=[0,510]   @ httptop.bpf.c
POST /v8/isolates (400 Bad Request)

bpf_skb_load_bytes()'s length argument is ARG_CONST_SIZE, so the verifier requires it to be provably 1..sizeof(dst). The existing cap &= (DATA_MAX-1); if (cap == 0) return; guard is logically correct, but clang keeps a parallel, un-narrowed copy of cap alive (to store e->captured) and passes that register as the helper length. The instruction trace shows the cap == 0 check narrowing one register while the call sources the length from another that still ranges [0,510].

Fix

Route the length through a volatile stack slot and bound it (1..DATA_MAX) immediately before the load. The volatile reload forces the value the helper sees to be a fresh memory load that the verifier can only narrow via the adjacent check — so the exact register passed is provably in range.

Things that did not work (documented in the commit, to save the next person the detour):

  • barrier_var(cap) before the guard — clang still kept a stale copy.
  • Reordering e->captured = cap after the guard — same.

The dropped cap &= (DATA_MAX - 1) mask is now redundant; the explicit rlen > DATA_MAX bound supersedes it.

Testing

Verified on Linux 6.12.73 aarch64 with the repo's own headless check, capturing live loopback HTTP:

$ yeet run verify.js
[verify] attaching to ifindexes 1,2
[verify] deduped 68 loopback double-sightings
[verify] aggregated endpoints (count desc):
   18  POST 127.0.0.1:8099 /login
   17  GET 127.0.0.1:8099 /api/products

Note

Draft: I only have a 6.12.73/aarch64 box to test on. Worth a sanity check that the program still loads on the older kernels/arches the prebuilt object was originally validated against (CO-RE should keep it portable, but the verifier behavior that triggered this is version-dependent).

🤖 Generated with Claude Code

bpf_skb_load_bytes()'s length argument is ARG_CONST_SIZE, so the verifier
requires it to be provably 1..sizeof(dst). clang kept a parallel,
un-narrowed copy of `cap` (kept live to store e->captured) and passed that
register as the length, so stricter verifiers (Linux 6.12+, observed on
6.12.73 aarch64) reject the capture read with:

    R4 invalid zero-sized read: u64=[0,510]   @ httptop.bpf.c

Route the length through a volatile stack slot and bound it (1..DATA_MAX)
immediately before the load, so the register the helper sees is a fresh
memory reload the verifier can only narrow via that adjacent check. Note:
a barrier_var() and reordering the e->captured store alone did NOT fix it
-- clang still sourced the helper length from the stale, un-narrowed
register; the volatile reload is what forces the bound onto the exact
register passed.

The dropped `cap &= (DATA_MAX - 1)` mask is now redundant: the explicit
`rlen > DATA_MAX` bound supersedes it.

Verified on Linux 6.12.73 aarch64 with `yeet run verify.js` capturing
live loopback HTTP.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@julian-goldstein julian-goldstein marked this pull request as ready for review June 23, 2026 00:18
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.

2 participants