gre(4) decapsulation keeps the outer packet's M_LENCHECKED and computes the GRE header length from attacker flag bits with no presence check (m_len/m_pkthdr.len int underflow) β one unauthenticated GRE packet panics the default INVARIANTS kernel; lying-length/negative-m_len state flows on production kernels (DF-3321's fix covered gif+mpls+ip_mroute but missed gre)
Summary
gre_input2() computes hlen = off + 4 [+4 CP] [+4 KP] [+4 SP] purely from the GRE flag bits without checking those bytes exist, so hlen can exceed the packet length: m->m_len -= hlen and m->m_pkthdr.len -= hlen go negative (int underflow) and m_data is pushed past the data end into attacker-controlled ethernet frame padding. It then clears only M_HASH, keeping the outer packet's M_LENCHECKED, so ip_hashfn() skips ip_lengthcheck() and ip_input() trusts the fake 'inner' header (the always-on guard does a signed/unsigned comparison and misses the negative length). VERIFIED twice on stock INVARIANTS guest ('complete IP header not in one mbuf' ip_input.c:539, via both the fake-IHL and m_len=-12 paths) - one proto-47 packet with CP|KP|SP flags from any off-path spoofer (tunnel endpoint addresses are fixed known values) panics a default-configured gre tunnel (GRE is the clone default). Production kernels process the negative-length mbuf onward into ip_dooptions, ip_reass and transport demux with attacker-chosen ip_p (DF-3293 negative-tlen family). Fix validated (pkthdr.len<hlen guard + clear M_LENCHECKED): both variants + 20 stress injections dropped, guest up.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3422 Β· 15 files| File | Type | Description | Size | |
|---|---|---|---|---|
| gre_poc.c | β | 9.9 KB | view raw | |
| setup.sh | β | 602 B | view raw | |
| run.sh | β | 246 B | view raw | |
| build.sh | β | 100 B | view raw | |
| build.log | β | 9 B | view raw | |
| run.gre40.log | β | 409 B | view raw | |
| run.greuflow.log | β | 215 B | view raw | |
| run.patched.log | β | 3.4 KB | view raw | |
| panic.gre40.txt | β | 397 B | view raw | |
| panic.greuflow.txt | β | 437 B | view raw | |
| fixbuild.log | β | 598 B | view raw | |
| fix.diff | β | 2.1 KB | view raw | |
| env.txt | β | 290 B | view raw | |
| VERDICT.md | β | 3.6 KB | β raw | |
| verdict.json | β | 6.2 KB | view raw |
DF-3422 β VERDICT
Status: reproduced (impact: panic on the default INVARIANTS kernel; lying-length / negative-m_len corruption state on production kernels). Confidence: certain.
What was run
Guest: stock X86_64_GENERIC INVARIANTS kernel
DragonFly dfbsd 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 (env.txt).
gre(4) is not in the kernel config; the stock if_gre.ko module from
/boot/kernel was loaded (kldload if_gre). Tunnel setup is a standard
deployment (setup.sh): tap0 10.99.0.1/24, gre0 tunnel 10.99.0.1 10.99.0.2,
link0 up (GRE mode is the clone default). The PoC injects one ethernet frame
into /dev/tap0 β byte-for-byte what the remote tunnel peer (or anyone who
can spoof the two fixed tunnel addresses) would send.
Baseline results (unmodified kernel + stock module)
gre40(outer ip_len=76, GRE flags CP|KP|SP β kernel hlen=36, fake inner header at m_data with IHL=15, m_len=40 after decap):panic: complete IP header not in one mbufβip_input+0xd61(KASSERTip_input.c:539), backtraceip_input β netmsg_service_loop(panic.gre40.txt).greuflow(outer ip_len=24, same flags β hlen=36 > 24):m->m_len -= 36β -12,m->m_pkthdr.len -= 36β -12 (ip_gre.c:181-182, int underflow) β the same panic fires (panic.greuflow.txt). Notably the always-on guard atip_input.c:463(m->m_len < sizeof(struct ip)) does a signed/unsigned comparison and does NOT catch the negative length ((size_t)(-12) > 20); on a production kernel the negative-length mbuf therefore flows into the body of ip_input (options walk, reass, transport demux by attacker-chosen ip_p) instead of being dropped β traced, and consistent with DF-3321's documented production-kernel continuation.
Both panics are one packet each, from an unauthenticated sender, on the
default INVARIANTS kernel β same class and bar as DF-3321 (gif), whose fix
covered gif+mpls+ip_mroute but left ip_gre.c untouched (verified against
findings/poc/DF-3321/fix.diff and current source: ip_gre.c:192 still
clears only M_HASH).
Fix validation
fix.diff (authored against the read-only sys/ tree, applied to the
guest's /usr/src copy): gre_input2() bounds hlen by m->m_pkthdr.len
before the shift and clears M_HASH|M_LENCHECKED before
netisr_queue(). Rebuilt if_gre.ko only (cd /usr/src/sys/net/gre &&
make && make install, fixbuild.log; module build is sufficient β the defect
is entirely inside the module). Re-ran both variants plus 20 stress
injections:
- no panic, guest stays up (
run.patched.log,STRESS_20_INJECTIONS_DONE,uptimehealthy); netstat -s -p ip: "with header length < data size" 0β1 β the fake inner header now rejected byip_lengthcheck(ips_badhlen,ip_demux.c:176) instead of being trusted;dmesgfree of the ip_input atomicy splat.
Baseline panics gone on the patched module β fix_status: fixed.
Why not uid=0
The demonstrated write primitive is bounded to the lying-length/negative-m_len state flowing into ip_input's downstream consumers (option-byte writes stay inside the victim mbuf's own cluster; DF-3321 documented the same ceiling for gif). The mobile-mode twin (DF-3423) carries the stronger unbounded-copy primitive and is where escalation effort belongs.
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, if_gre.ko rebuilt (make; make install in sys/net/gre, fixbuild.log). Both baseline panicking variants re-run byte-identically plus 20 stress injections: no panic, guest up, inner packet rejected by ip_lengthcheck (netstat -s -p ip 'with header length < data size' 0->1), dmesg free of the ip_input atomicy splat.
run.patched.log (patched module, all variants rc=0, guest up, counter delta); fixbuild.log; panic.gre40.txt/panic.greuflow.txt (baseline panics); fix.diff
Confirmed kernel references
Detail
Exploit chain
tap frame -> ether_input -> ip_lengthcheck validates OUTER header, sets M_LENCHECKED -> ip_input -> transport_processing_oncpu -> encap4_input (proto 47, in_proto.c:219) on netisr0 -> gre_lookup match (iff src==g_dst, dst==g_src, IFF_UP) -> gre_input2: hlen=off+4+12 from flags, m_data += hlen (past data end into frame padding), m_len/m_pkthdr.len -= hlen (negative), clears M_HASH only -> netisr_queue(NETISR_IP) -> ip_hashfn sees stale M_LENCHECKED, skips ip_lengthcheck, hashes the fake inner header -> ip_input: version from fake byte 0x4F OK, hlen 60, KASSERT(m_len >= hlen) panic (INVARIANTS). Production-kernel continuation (traced, consistent with DF-3321/DF-3293 documentation): negative m_len passes the signed/unsigned guard at ip_input.c:463, lying ip_len/ip_hl/ip_p flow into ip_dooptions (option-area walk past the data), ip_reass (m_len -= hlen underflow) and tcp_input (negative tlen).
Evidence (decisive lines)
["panic.gre40.txt: 'panic: complete IP header not in one mbuf' ip_input+0xd61 <- netmsg_service_loop (baseline, gre40)", 'panic.greuflow.txt: same KASSERT via the m_len=-12 underflow variant (baseline, greuflow)', "run.patched.log: fixed if_gre.ko - both variants + 20 stress injections, no panic, guest up, STRESS_20_INJECTIONS_DONE, 'with header length < data size' 0->1", 'fixbuild.log: in-guest module rebuild of if_gre.ko with fix.diff (make/make install, sys/net/gre)', 'fix.diff: hlen bounded by m_pkthdr.len + M_HASH|M_LENCHECKED cleared in gre_input2 (and the DF-3423 mobile hunks)', 'run.gre40.log / run.greuflow.log: injection runs (ssh hang at tool timeout = guest frozen at ddb, see pack note)']
PoC changes
gre_poc.c written fresh for gre (DF-3321's gif harness adapted to proto-47 GRE encapsulation with CP|KP|SP flag arithmetic); greuflow variant added to demonstrate the int underflow; mobile variants (see DF-3423) share the file. gre40 layout revised after source-trace: ip_input.c:463's m_len<20 gate requires m_len>=20 for the clean KASSERT-539 path, so outer ip_len=76 / fake IHL=15 (m_len 40 < hlen 60) instead of the initial 40/5 layout; mobile hcrc must be stored native-endian because gre_in_cksum() sums native u_short words (first attempt stored big-endian and was checksum-rejected).
Verified recommended fix
In gre_input2(): reject hlen > m->m_pkthdr.len before adjusting the mbuf, and clear M_LENCHECKED alongside M_HASH before netisr_queue() (see fix.diff).
Verdict
REPRODUCED on the stock INVARIANTS guest: gre_input2() (sys/netinet/ip_gre.c:178-193) computes the decap offset hlen purely from the attacker-controlled GRE flag bits (CP|KP|SP -> +12 bytes) with no presence check, so hlen can exceed the packet length and 'm->m_len -= hlen' / 'm->m_pkthdr.len -= hlen' go NEGATIVE (int underflow, ip_gre.c:181-182), and it clears only M_HASH (ip_gre.c:192), keeping the outer packet's M_LENCHECKED so ip_hashfn() (ip_demux.c:295) skips ip_lengthcheck() and ip_input() trusts the 'inner' header that lies past the data end (in the attacker-controlled ethernet frame padding inside the mbuf cluster). One unauthenticated GRE frame from the tunnel peer (or anyone spoofing the two fixed tunnel addresses) panics the default kernel twice over: fake inner IHL=15 -> panic 'complete IP header not in one mbuf' at ip_input.c:539 (gre40); hlen>ip_len -> m_len=-12 flows to the same KASSERT (greuflow) -- note ip_input.c:463's always-on m_len
No comments yet.