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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3422 Β· 15 files
FileTypeDescriptionSize
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
VERDICT.md
↓ download 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 (KASSERT ip_input.c:539), backtrace ip_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 at ip_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, uptime healthy);
  • netstat -s -p ip: "with header length < data size" 0β†’1 β€” the fake inner header now rejected by ip_lengthcheck (ips_badhlen, ip_demux.c:176) instead of being trusted;
  • dmesg free 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

fixed
baseline reproduced→ patch + rebuild →patched clean

fix.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
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64 (same kernel; patched if_gre.ko module rebuilt from the same tree, kldstat-confirmed)

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_len1), guest stays up.