From dd06f47f6b7e6d2a6cc022217606eee7401cbc77 Mon Sep 17 00:00:00 2001 From: Jason E Date: Mon, 22 Jun 2026 20:12:10 -0400 Subject: [PATCH] Fix BPF verifier "zero-sized read" rejection on Linux 6.12+ 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) --- httptop.bpf.c | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/httptop.bpf.c b/httptop.bpf.c index 25033e3..da4f11c 100644 --- a/httptop.bpf.c +++ b/httptop.bpf.c @@ -163,7 +163,6 @@ static __always_inline int handle(struct __sk_buff *skb, __u8 dir) __u32 limit = kind == KIND_RESPONSE ? RESP_CAP : (DATA_MAX - 1); if (cap > limit) cap = limit; - cap &= (DATA_MAX - 1); /* make the bound explicit for the verifier */ if (cap == 0) return TCX_NEXT; @@ -178,8 +177,22 @@ static __always_inline int handle(struct __sk_buff *skb, __u8 dir) e->dir = dir; e->kind = kind; e->total_len = plen; - e->captured = cap; - if (bpf_skb_load_bytes(skb, poff, e->data, cap) < 0) { + /* bpf_skb_load_bytes()'s length is ARG_CONST_SIZE: the verifier requires it + to be provably 1..sizeof(dst). clang otherwise keeps a parallel, + un-narrowed copy of `cap` (kept live to store e->captured) and passes + *that* register as the length, so stricter verifiers (Linux 6.12+) reject + the read as possibly zero-sized ("R4 invalid zero-sized read"). Forcing + the length through a volatile stack slot makes the value the helper sees a + fresh memory reload that the verifier can only bound via the check below — + so the exact register passed is provably in range. */ + volatile __u32 vlen = cap; + __u32 rlen = vlen; + if (rlen == 0 || rlen > DATA_MAX) { + bpf_ringbuf_discard(e, 0); + return TCX_NEXT; + } + e->captured = rlen; + if (bpf_skb_load_bytes(skb, poff, e->data, rlen) < 0) { bpf_ringbuf_discard(e, 0); return TCX_NEXT; }