β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-3321

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3321 Β· 12 files
FileTypeDescriptionSize
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
VERDICT.md
↓ download 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)

  1. tapwrite β†’ ether_input: ip_lengthcheck() runs for the OUTER IPv4 header and sets M_LENCHECKED (sys/netinet/ip_demux.c:268).
  2. Outer IP proto 4 (IPPROTO_IPV4) β†’ encap4_input() (sys/netinet/in_proto.c:192) β†’ gif_encapcheck/gif_validate4 match (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).
  3. gif_input() (sys/net/gif/if_gif.c:417) clears M_HASH only (if_gif.c:467) and netisr_queue(NETISR_IP, m).
  4. ip_input() (sys/netinet/ip_input.c:489-504): M_HASH clear β‡’ ip_hashfn() runs β‡’ ip_demux.c:295-298 sees the stale M_LENCHECKED and skips ip_lengthcheck() for the inner header.
  5. Inner hlen=60 > ip_len=40 β‡’ ip_input.c:575 KASSERT (PoC A); inner TCP th_off*4 > tlen β‡’ tcp_input.c:765-768 KASSERT (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(): clear M_LENCHECKED after the existing M_HASH clear.
  • mpls_input() label-0 and label-2: clear M_HASH|M_LENCHECKED.
  • X_ipip_input(): clear M_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 (the ip_lengthcheck TCP gate: iplen < iphlen+20, ip_demux.c:219-221).
  • PoC B: no panic, guest up, tcp "discarded for bad header offset field" 1β†’2 (the thoff + iphlen > iplen gate, 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

fixed
baseline reproduced→ patch + rebuild →patched clean

fix.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)
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #1: Thu Sep 10 20:30:33 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

Confirmed kernel references

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.