Why does the BPF redirect neighbor helper trip a kernel BUG on a packet whose Ethernet header is short, and how does the fix read the true header length differently on the receive path than on the transmit path?
Because skb->mac_len is only populated on the receive path. A bpf_redirect_neigh() fix that guards the pull with mac_len under ETH_HLEN is safe on ingress, but on the transmit path — locally generated packets through tc egress, or netkit forwarding — mac_len is 0 for every skb, so the same guard would silently drop all valid redirect-neighbor traffic from those paths. The header-length field that is valid on both paths is skb_network_offset(), the distance from skb->data to network_header; the corrected fix reads the length from that instead.
The mechanism
-
The bug.
bpf_redirect_neigh()runs throughskb_do_redirect()into__bpf_redirect_neigh()innet/core/filter.c. The original code didethh = eth_hdr(skb), checked onlymac_headerbelownetwork_header, then calledskb_pull(skb, sizeof(*ethh))— that is a 14-byteETH_HLENpull.__skb_pull()doesskb->len -= lenand then, if the linear part underflows,BUG(). After a VLAN pop-eth, a privileged tc program can leave the skb with a 1-byte effective MAC header and one byte of linear headroom (the reproducer isbpf_skb_change_head(skb, 1, 0)), then callbpf_redirect_neigh(); the old check passed and the 14-byte pull tripped the BUG. -
The v1 fix and its regression. The fix adds a guard before the pull and re-reads
ethhafter it: drop whenmac_lenis underETH_HLENor whenpskb_may_pull()cannot make the header linear, thenethh = eth_hdr(skb);.pskb_may_pull()linearizes without panicking, and themac_lencheck drops genuinely short packets. Butskb->mac_lenis written only on the receive path, by__netif_receive_skb_core()callingskb_reset_mac_len(), which sets it tonetwork_headerminusmac_header. Nothing on the transmit path writesmac_len;__dev_queue_xmit()anddev_loopback_xmit()callskb_reset_mac_header()but notskb_reset_mac_len(), so a locally generated packet carriesmac_len == 0. On tc egress and netkit,0 < ETH_HLENis always true, so every valid redirect-neighbor skb falls through to the drop path (kfree_skb,-ENOTSUPP) — a regression that would also reach stable. -
The path-independent fix. When
__bpf_redirect_neigh()runs,skb->datasits at the start of the L2 header on both paths: on ingress,sch_handle_ingress()pushedmac_lenbytes beforeskb_do_redirect(); on egress,dev_hard_header()already pushed the L2 header and__dev_queue_xmit()setmac_headerto that offset. So the true L2 header length isnetwork_headerminusdata, which is exactlyskb_network_offset(). The corrected guard drops whenskb_network_offset()is underETH_HLENor whenpskb_may_pull()cannot linearize; it still rejects the 1-byte header frombpf_skb_change_head, and it is correct on egress and netkit, wheremac_lenis 0. -
The kernel already knows this.
act_mirred.cpicksmac_lenat ingress andskb_network_offset()on every other path — it already treatsmac_lenas unreliable outside ingress. Thebpf_redirect_neighselftests attach at tc ingress, wheremac_lenis valid, so they cannot catch an egress or netkit regression.
How to verify and debug
-
See which field is zero. On the egress or netkit path, inspect a packet's
mac_len(0) againstskb_network_offset()(the real L2 header length) and note which one matches the header. The egress path never callsskb_reset_mac_len, somac_lenis 0 by construction. -
Reproduce the original BUG. Attach a privileged SCHED_CLS program at tc ingress, do a VLAN pop-eth, call
bpf_skb_change_head(skb, 1, 0)thenbpf_redirect_neigh(); on an unfixed kernel theskb->lenunderflow check inside__skb_pull()fires. -
Prove the regression. Attach the same redirect-neighbor program at tc egress on a locally generated socket packet, or forward it through netkit, and watch the v1
mac_lenguard drop the packet with-ENOTSUPP; theskb_network_offset()version keeps it.
The limitation
- The guard still drops any skb whose L2 header is shorter than
ETH_HLEN; that is intentional, not a defect. pskb_may_pull()may reallocateskb->head, which is why the fix re-readseth_hdr(skb)after it instead of reusing a pointer taken before the guard.- A
mac_headerabovenetwork_headeris a distinct failure mode from a too-short header;skb_network_offset()covers the too-short case, but the two are not the same check.
References
- Kernel
net/core/filter.c—__bpf_redirect_neighand the originalskb_pull(ETH_HLEN)path. - Kernel
include/linux/skbuff.h—skb_reset_mac_len,skb_reset_mac_header,skb_network_offset,__skb_pull. - Kernel
net/sched/act_mirred.c— the pickmac_lenat ingress,skb_network_offset()elsewhere. - Kernel
include/uapi/linux/if_ether.h—ETH_HLEN(14) andETH_ALEN(6). - Commit
b4ab31414970("bpf: Add redirect_neigh helper as redirect drop-in") — theFixes:target for the redirect-neighbor helper. - Public bpf mailing-list patch thread: fix
skb_pull()BUG in__bpf_redirect_neigh()— the public patch and the egress/netkit regression review.
Community discussion today
A public bpf-list patch fixes a skb_pull() BUG in __bpf_redirect_neigh() that a privileged tc program can trigger by shrinking the MAC header after a VLAN pop-eth. The first version guarded the pull with mac_len under ETH_HLEN, but an automated review caught that mac_len is only set on the receive path and is 0 on tc egress and netkit, so the guard would drop all valid redirect-neighbor traffic from those paths; the thread agreed and a follow-up reads the length from skb_network_offset() instead, which is valid on both paths. The discussion so far is narrow: the boundary is which field carries the L2 header length per path, and the existing redirect-neighbor selftests all attach at ingress, so they cannot catch an egress regression.
This run's archive-backed sources produced 0 messages in the 7-day window (the two opt-in archive sources were empty at snapshot time), so the question above was sourced from the permitted public bpf mailing list, reviewed through its public atom feed, and the patch thread was used as the primary reference. The Discord channels for eunomia-bpf and sched-ext and r/eBPF were not covered this run (no visible-browser session was available), so they are marked not-covered rather than quiet.
Continue exploring
Back to index
eBPF Q&A
Daily, source-grounded answers to recurring questions about eBPF, Linux observability, profiling, runtime extension, and security.
Previous
Why do non-sleepable BPF fentry and fexit programs silently skip calls from another task on the same CPU, and why do the skipped calls show up as recursion misses?
Because the trampoline recursion guard is per-CPU and is taken with preemption still enabled.
Next
Why does the iocost cgroup cost model file only switch the built-in model between auto and user coefficient sets, and how does a BPF struct ops cost model take over a device while that file stops selecting a model?
Because the cgroup file is the interface of the built-in model, and a BPF program is attached out of band.
- Last updated
- Oct 28, 2026
- First published
- Oct 28, 2026
- Contributors
- Yunwei
Was this page helpful?