gif(4) decapsulation keeps the outer packet's M_LENCHECKED β inner IPv4 header never length-validated on requeue into ip_input(): one-packet unauthenticated panic on default INVARIANTS kernels, lying-length corruption on production kernels (mpls label-0 and ip_mroute X_ipip_input decaps clear neither flag)
Summary
ip_hashfn() skips ip_lengthcheck() whenever M_LENCHECKED is set and ip_input() skips ip_hashfn() entirely when M_HASH is set - the flags certify the packet CURRENTLY in the mbuf. gif_input() clears M_HASH but keeps the stale M_LENCHECKED set by the ether path for the OUTER header, and in_gif_input() pulled up only 20 bytes of the inner packet - so the inner IPv4 header, its options, and its transport lengths are trusted sight-unseen. Two same-class decaps clear NEITHER flag: mpls_input() label-0 (stale M_HASH makes ip_input skip ip_hashfn altogether) and X_ipip_input(). gif is a default-kernel pseudo-device. VERIFIED on stock INVARIANTS guest: one raw tap frame with outer IP proto-4 src=tunnel-remote -> PoC A (inner IHL=15): panic 'total length incl header'; PoC B (honest inner + TCP th_off=15): panic 'bad TCP data offset 60' - the DF-3293 primitive through a new path. Any party that can deliver one spoofed-source proto-4 packet to a host with an UP gif tunnel: instant panic on default INVARIANTS builds; on production builds forged hlen/ip_len/th_off flow into ip_dooptions (option-area OOB read/write past the 20 pulled-up bytes), ip_reass (m_len/ip_len underflows) and tcp_input (negative tlen incl. unconditional pulloutofband panic). Fix validated in-guest (clear M_LENCHECKED in gif_input + same for mpls label-0/label-2 + X_ipip_input): both PoCs dropped at the lengthcheck gates, guest stays up. No uid0 route from this primitive alone.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3321 Β· 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| gif_ihl15.c | β | 5.1 KB | view raw | |
| gif_tcp_off15.c | β | 4.5 KB | view raw | |
| build.sh | β | 85 B | view raw | |
| run.sh | β | 256 B | view raw | |
| build.log | β | 833 B | view raw | |
| run.patched.log | β | 1.6 KB | view raw | |
| panic.txt | β | 2.1 KB | view raw | |
| panic.b.txt | β | 2.1 KB | view raw | |
| env.txt | β | 547 B | view raw | |
| fix.diff | β | 1.8 KB | view raw | |
| code_hashes.txt | β | 160 B | view raw | |
| VERDICT.md | β | 5.8 KB | β raw |
DF-3321 β VERDICT
Status: REPRODUCED (panic, both triggers) on the stock INVARIANTS guest; fix validated by full kernel rebuild.
What was verified
Baseline (stock kernel #0: Thu Jul 2 06:02:54 UTC 2026, INVARIANTS)
PoC A β gif_ihl15 (inner IHL=15 β hlen 60, inner ip_len=40,
68 real bytes, valid 60-byte checksum):
panic: total length incl header
cpuid = 1
ip_input() at ip_input+0xf42
ip_input() at ip_input+0xf42
netmsg_service_loop() at netmsg_service_loop+0xe9
Debugger("panic")
(panic.txt) β the KASSERT(ip_len >= hlen, ("total length incl header"))
at sys/netinet/ip_input.c:575.
PoC B β gif_tcp_off15 (honest 20-byte inner IP header + TCP
th_off=15, tlen 20):
panic: bad TCP data offset 60 (tlen 20)
cpuid = 1
tcp_input() at tcp_input+0x11e1
transport_processing_oncpu.isra.7() at transport_processing_oncpu+0x6b
ip_input() at ip_input+0x71f
netmsg_service_loop() at netmsg_service_loop+0xe9
Debugger("panic")
(panic.b.txt) β the exact DF-3293 primitive, delivered through gif(4)
instead of gre(4).
Both panics are one packet each, unauthenticated, from the "tunnel peer"
(raw frame written to /dev/tap0; on a real deployment: anyone on the
path who can source-spoof the configured remote endpoint β
gif_validate4() matches only src==gif_pdst, there is no authentication).
Why it works (path:line)
tapwrite β ether_input:ip_lengthcheck()runs for the OUTER IPv4 header and setsM_LENCHECKED(sys/netinet/ip_demux.c:268).- Outer IP proto 4 (
IPPROTO_IPV4) βencap4_input()(sys/netinet/in_proto.c:192) βgif_encapcheck/gif_validate4match (tunnel 10.99.0.1β10.99.0.2, UP) βin_gif_input()(sys/netinet/in_gif.c:220):m_adj(m, off)strips the outer header,m_pullup(m, sizeof(struct ip))pulls up only 20 bytes of the inner packet (in_gif.c:246-253). gif_input()(sys/net/gif/if_gif.c:417) clearsM_HASHonly (if_gif.c:467) andnetisr_queue(NETISR_IP, m).ip_input()(sys/netinet/ip_input.c:489-504): M_HASH clear βip_hashfn()runs βip_demux.c:295-298sees the staleM_LENCHECKEDand skipsip_lengthcheck()for the inner header.- Inner
hlen=60 > ip_len=40βip_input.c:575KASSERT (PoC A); inner TCPth_off*4 > tlenβtcp_input.c:765-768KASSERT (PoC B).
Production (non-INVARIANTS) continuation β traced, not executed here
The guest is an INVARIANTS kernel, so both PoCs stop at KASSERTs. On a
production build the KASSERTs are compiled out and the same packet
flows on with lying lengths: ip_dooptions() (ip_input.c:661) walks
and writes option bytes bounded only by hlen (60) while only 20
bytes were pulled up (reads/writes stay inside the single tap mbuf's
cluster β contained, not cross-object); a fragmented inner packet makes
ip_reass() execute m->m_len -= hlen (ip_input.c:1089) and
ip_len -= hlen (ip_input.c:1136) with hlen > m_len β negative
m_len / uint16 underflow state inside the reassembly queue; TCP gets a
negative tlen (the DF-3293 segment-state corruption, incl.
panic("tcp_pulloutofband") which fires even on production kernels
with TH_URG set). Escalation beyond DoS was not pursued: the write
stays within the victim mbuf's own storage on the paths exercised.
Same-class sibling paths enumerated (in fix.diff, not separately PoC'd)
| path | flags cleared today | extra precondition |
|---|---|---|
mpls_input() label-0/label-2 decap (sys/netproto/mpls/mpls_input.c:117-121,136-140) |
neither M_HASH nor M_LENCHECKED (stale M_HASH from the eth frame makes ip_input() skip ip_hashfn() entirely) |
mpls module loaded |
X_ipip_input() mroute tunnel decap (sys/net/ip_mroute/ip_mroute.c:1746-1766) |
neither | ip_mroute + mrouted |
Safe paths confirmed for contrast: wg(4) clears both
(sys/net/if_wg.c:85), CARP re-input goes back through
ether_demux_oncpu which re-runs ip_lengthcheck when the flag is
clear (sys/net/if_ethersubr.c:1120-1123), tun/ng_iface/sl/plip/ic
deliver fresh flag-less mbufs, and gre was fixed for DF-3293.
Fix validation
fix.diff (authored after reproduction; applies with git apply -p1
or patch -p1):
gif_input(): clearM_LENCHECKEDafter the existingM_HASHclear.mpls_input()label-0 and label-2: clearM_HASH|M_LENCHECKED.X_ipip_input(): clearM_HASH|M_LENCHECKED.
Applied in-guest to /usr/src, make -j8 nativekernel KERNCONF=X86_64_GENERIC
+ make installkernel, reboot into #1: Thu Sep 10 20:30:33 UTC 2026.
Both PoCs re-run byte-identically (run.patched.log):
- PoC A: no panic, guest up,
tcp "discarded because packet too short"1β2 (theip_lengthcheckTCP gate:iplen < iphlen+20,ip_demux.c:219-221). - PoC B: no panic, guest up,
tcp "discarded for bad header offset field"1β2 (thethoff + iphlen > iplengate,ip_demux.c:233-237).
i.e. after the fix the inner packet is validated exactly where it should have been, and dropped.
Bottom line
ip_input()'s M_LENCHECKED discipline is violated by the gif(4) decap
path (default-kernel pseudo-device gif); a single unauthenticated
packet from a configured tunnel's remote endpoint panics default
INVARIANTS kernels (ip_input.c:575 or tcp_input.c:766) and feeds
lying lengths into ip_dooptions/ip_reass/tcp_input on production
kernels. Severity High (matches DF-3293/DF-0740 precedent for the same
class). The one-line-per-site fix is verified effective.
Fix status (batch validation)
Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.
Fix verification
fixedfix.diff applied to /usr/src in-guest, make -j8 nativekernel KERNCONF=X86_64_GENERIC + make installkernel + reboot. Both PoCs re-run byte-identically: no panic, guest up, packets dropped at the ip_lengthcheck gates (tcp 'too short' 1->2, 'bad header offset' 1->2, run.patched.log). Baseline panics absent on patched kernel.
run.patched.log (patched kernel #1, both PoCs rc=0 guest up, counter deltas); panic.txt/panic.b.txt (baseline panics); fix.diff (applied content)
Confirmed kernel references
- sys/netinet/ip_input.c:489-504
- sys/netinet/ip_input.c:536-576
- sys/netinet/ip_input.c:1088-1089
- sys/netinet/ip_input.c:1131-1136
- sys/net/gif/if_gif.c:465-468
- sys/netinet/in_gif.c:246-253
- sys/netinet/ip_demux.c:295-298
- sys/netproto/mpls/mpls_input.c:117-121
- sys/net/ip_mroute/ip_mroute.c:1746-1766
- sys/netinet/tcp_input.c:765-768
Detail
Exploit chain
raw frame to tap0/ether -> ip_lengthcheck validates OUTER header, sets M_LENCHECKED -> ip_input -> encap4_input (proto 4) -> gif_validate4 match -> in_gif_input strips outer 20B, pulls up only 20B of inner -> gif_input clears M_HASH only -> netisr_queue(NETISR_IP) -> ip_input -> ip_hashfn sees stale M_LENCHECKED, skips ip_lengthcheck -> inner header trusted: KASSERT(ip_len >= hlen) panic (A) / th_off*4 > tlen KASSERT panic in tcp_input (B). Production-kernel continuation (traced, not executed): option OOB-read/write within the mbuf cluster, ip_reass negative-m_len/uint16 underflow state, negative tlen into the TCP state machine (incl. unconditional panic('tcp_pulloutofband') with TH_URG per DF-3293).
Evidence (decisive lines)
["panic.txt: 'panic: total length incl header' with ip_input backtrace (PoC A, baseline)", "panic.b.txt: 'panic: bad TCP data offset 60 (tlen 20)' with tcp_input<-transport_processing_oncpu<-ip_input backtrace (PoC B, baseline)", "run.patched.log: fixed kernel #1 Sep 10 20:30:33 - both PoCs rc=0 guest up, tcp 'discarded because packet too short' 1->2 and 'discarded for bad header offset field' 1->2 (ip_lengthcheck gates now rejecting the inner packet)", 'fix.diff: clear M_LENCHECKED in gif_input + M_HASH|M_LENCHECKED in mpls_input label-0/2 and X_ipip_input', 'env.txt: guest uname, gif0/tap0 config, reass sysctls']
PoC changes
Adapted DF-3293's tap/gre injection harness to proto-4 gif encapsulation (no GRE header; gif_validate4 requires outer src==gif_pdst, dst==gif_psrc, non-martian outer src); PoC A computes the inner IP checksum over the claimed 60-byte header so the checksum gate passes before the KASSERT; fixed a nested / / comment that broke the first compile.
Verified recommended fix
Clear M_LENCHECKED alongside M_HASH in gif_input() (sys/net/gif/if_gif.c:467) and clear M_HASH|M_LENCHECKED in mpls_input() label-0/label-2 decap and X_ipip_input() before netisr_queue(NETISR_IP).
Verdict
REPRODUCED on the stock INVARIANTS guest: gif_input() (sys/net/gif/if_gif.c:467) clears M_HASH but keeps the outer packet's M_LENCHECKED, so on requeue ip_hashfn() (ip_demux.c:295) skips ip_lengthcheck() for the inner IPv4 packet. One unauthenticated proto-4 frame from the tunnel peer panics the default kernel twice over: inner IHL=15/ip_len=40 -> panic 'total length incl header' at ip_input.c:575 (PoC A); inner TCP th_off=15 -> panic 'bad TCP data offset 60 (tlen 20)' in tcp_input via ip_input->transport_processing_oncpu (PoC B) - the DF-3293 primitive through a new path. gif is a default-kernel pseudo-device (X86_64_GENERIC:288); setup is a standard configured tunnel, the attack is a single packet. On production kernels the KASSERTs vanish and the lying lengths flow into ip_dooptions (option-area walk past the 20 pulled-up bytes), ip_reass (m_len -= hlen / ip_len -= hlen underflows, ip_input.c:1089/1136) and tcp_input (negative tlen). Fix (clear M_LENCHECKED in gif_input; also clear M_HASH|M_LENCHECKED in the mpls label-0 and X_ipip_input decaps, same class) validated by make nativekernel + reboot: both PoCs then dropped by ip_lengthcheck (tcp 'too short'/'bad header offset' counters 1->2), guest stays up.
No comments yet.