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

gre_mobile_input(): neither M_HASH nor M_LENCHECKED cleared, inner ip_len double-counts the outer IP header (ANY valid mobile-GRE packet panics INVARIANTS kernels), and a truncated datagram yields a negative-length bcopy β€” reproduced as an unbounded kernel-heap overwrite (mbuf-metadata smash, secondary panic in m_free)

Summary

gre_mobile_input() recomputes ip->ip_len as ntohs(ip_len)+sizeof(struct ip)-msiz although DragonFly delivers the FULL outer datagram length (the stale 'rip_input already decreased' premise), so every decapped packet claims 20 bytes more than the mbuf holds; it requeues without clearing M_HASH or M_LENCHECKED so ip_input trusts the lie (KASSERT 'mbuf too short' for any valid packet); and for a datagram shorter than outer-header+msiz the bcopy length goes negative and is passed to bcopy as (size_t)-8. VERIFIED: mobile-mode tunnel (ifconfig gre0 -link0 up - one flag away); a single WELL-FORMED 28-byte packet panics ('mbuf too short'); one 20-byte truncated packet executes an unbounded ascending kernel-heap copy - 'panic: overflowed mbuf' with a SECONDARY PANIC in m_free<-sosendtcp (the copy corrupted a neighbouring mbuf's metadata): a genuine unauthenticated remote heap-corruption primitive on production kernels, not merely DoS. Fix validated (presence guard + corrected ip_len arithmetic + clear both flags): both variants + 20 stress injections dropped, guest up.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3423 Β· 14 files
FileTypeDescriptionSize
gre_poc.c β€” 9.9 KB view raw
setup.sh β€” 602 B view raw
run.sh β€” 241 B view raw
build.sh β€” 100 B view raw
build.log β€” 9 B view raw
run.mobile.log β€” 215 B view raw
run.mobileuflow.log β€” 215 B view raw
run.patched.log β€” 3.4 KB view raw
panic.mobile.txt β€” 303 B view raw
panic.mobileuflow.txt β€” 620 B view raw
fix.diff β€” 2.1 KB view raw
env.txt β€” 290 B view raw
VERDICT.md β€” 3.5 KB ↓ raw
verdict.json β€” 5.7 KB view raw
VERDICT.md
↓ download raw

DF-3423 β€” VERDICT

Status: reproduced (impact: panic β€” including a demonstrated unbounded kernel-heap bcopy overwrite effective on any kernel). Confidence: certain.

What was run

Same guest/kernel/module as DF-3422 (env.txt). Tunnel put in MOBILE mode with ifconfig gre0 -link0 up (gre_ioctl SIOCSIFFLAGS β†’ g_proto = IPPROTO_MOBILE, if_gre.c:461-470; encap re-attached with in_mobile_protosw, if_gre.c:563-566).

Baseline results

  • mobile β€” outer ip_len=28 (20 IP + 8-byte mobile header, S-bit clear, checksum crafted to 0). This is a well-formed minimal mobile-GRE packet; the decap's ip->ip_len = ntohs(...)+sizeof(struct ip)-msiz (ip_gre.c:247) claims 40 bytes for a 20-byte mbuf (the "+20" double-counts the outer header; the comment's rip_input premise does not hold on DragonFly), and since neither M_HASH nor M_LENCHECKED is ever cleared (ip_gre.c:259-261), ip_input trusts it: panic: mbuf too short β€” ip_input+0xfaf (KASSERT ip_input.c:576), backtrace ip_input ← netmsg_service_loop (panic.mobile.txt). Deterministic for ANY valid mobile-GRE packet β€” mobile tunnels cannot work on INVARIANTS kernels; on production kernels every decapped packet is processed with ip_len 20 bytes longer than the data (in-cluster OOB reads by transports).
  • mobileuflow β€” outer ip_len=20 (NO mobile header; the 8 "header" bytes are read from the frame padding past ip_len, checksum still valid). msiz=8 β‡’ bcopy(src, dst, m->m_len - msiz - (ip->ip_hl<<2)) = bcopy(..., -8) (ip_gre.c:237-238) β‡’ (size_t)-8: panic: overflowed mbuf 0xfffff80117e0c400 with SECONDARY PANIC ON CPU 0 in m_free ← sosendtcp ← soo_write ← kern_pwritev (panic.mobileuflow.txt): the unbounded copy had smashed neighbouring kernel heap objects β€” the ssh session's own mbuf came back with corrupted metadata. On a production kernel the m_free INVARIANTS check is absent: this is a silent, unbounded, attacker-initiated kernel-heap overwrite (source bytes partially attacker-controlled via the frame), i.e. a genuine memory-corruption primitive, not just a DoS.

Fix validation

fix.diff (shared with DF-3422, ip_gre.c only): guard m->m_len >= (ip->ip_hl<<2) + msiz before the checksum/bcopy; ip->ip_len corrected to ntohs(ip->ip_len) - msiz; M_HASH|M_LENCHECKED cleared before netisr_queue. Rebuilt if_gre.ko, re-ran both variants + 20 stress injections: no panic, guest up, dmesg clean (run.patched.log). fix_status: fixed.

Escalation note

The mobileuflow primitive (huge ascending kernel copy whose source prefix is attacker-controlled frame bytes) is the strongest of the batch; turning it into a controlled uid=0 chain would require heap-layout steering of the copy direction/extent β€” the bcopy copies upward through the direct map with a fixed relationship between src and dst (dst = src - msiz), so every byte between them is preserved while everything above the source range is overwritten by higher memory β€” a destructive smear rather than a surgical write. We characterize it and stop there; the fix removes the primitive entirely.

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 and installed. Both baseline-panicking variants re-run byte-identically plus 20 stress injections: no panic, no trap, guest up, dmesg clean (run.patched.log).

run.patched.log (patched module, all variants rc=0, STRESS_20_INJECTIONS_DONE, uptime healthy); panic.mobile.txt/panic.mobileuflow.txt (baseline); fixbuild.log; 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 (proto 55) -> ether_input -> outer validated, M_LENCHECKED+M_HASH set -> ip_input -> transport_processing_oncpu -> encap4_input on netisr0 (in_mobile_protosw, attached by gre_ioctl recompute when LINK0 is cleared) -> gre_lookup(IPPROTO_MOBILE) match -> gre_mobile_input: [mobileuflow] ip_len=20 -> mobile 'header' read from frame padding past the data end, checksum crafted valid from the same padding -> msiz=8 -> bcopy(dst=ip+20, src=ip+28, (size_t)(20-8-20)) = huge ascending kernel copy -> neighbouring kernel-heap objects overwritten -> ssh session's mbuf metadata corrupted -> secondary panic in m_free (INVARIANTS) / silent heap corruption on production kernels. [mobile] valid 28-byte packet -> bcopy len 0 -> ip_len := 28+20-8 = 40 with m_pkthdr.len 20 -> stale M_HASH/M_LENCHECKED -> ip_input skips characterization -> KASSERT(pkthdr.len >= ip_len) panic 'mbuf too short'.

Evidence (decisive lines)

["panic.mobile.txt: 'panic: mbuf too short' ip_input+0xfaf <- netmsg_service_loop (baseline, VALID mobile packet)", "panic.mobileuflow.txt: 'panic: overflowed mbuf 0xfffff80117e0c400' + SECONDARY PANIC in m_free<-sosendtcp<-soo_write<-kern_pwritev (baseline, negative bcopy)", 'run.patched.log: fixed if_gre.ko - both variants + 20 stress injections, no panic, guest up', 'fixbuild.log + fix.diff: guard m->m_len >= (ip->ip_hl<<2)+msiz, ip_len -= msiz only, clear M_HASH|M_LENCHECKED', 'run.mobile.log / run.mobileuflow.log: injection runs (ssh hang at tool timeout = guest frozen at ddb, see pack note)']

PoC changes

mobile hcrc must be stored native-endian (gre_in_cksum sums native u_short words); first attempt stored big-endian and the packet was checksum-rejected (visible in run.mobile.log's first attempt 'cksum(kernel-mirror)=0xa659'); the mobileuflow variant relies on the ethernet frame padding past ip_len carrying a checksum-valid mobile header -- both deterministic with tap injection.

Verified recommended fix

In gre_mobile_input(): require m->m_len >= (ip->ip_hl<<2)+msiz before checksum/bcopy, compute ip->ip_len = htons(ntohs(ip->ip_len) - msiz), and clear M_HASH|M_LENCHECKED before netisr_queue() (see fix.diff).

Verdict

REPRODUCED on the stock INVARIANTS guest, gre0 in MOBILE mode (ifconfig gre0 -link0 up). gre_mobile_input() (sys/netinet/ip_gre.c:204-263) has three stacked defects: (1) ip->ip_len is recomputed as ntohs(ip_len)+sizeof(struct ip)-msiz although DragonFly delivers the FULL outer datagram length (the comment's 'rip_input already decreased' premise is stale NetBSD behaviour), so the inner header claims 20 bytes more than the mbuf holds FOR EVERY well-formed mobile-GRE packet; (2) neither M_HASH nor M_LENCHECKED is ever cleared before netisr_queue (ip_gre.c:259-261), so ip_input trusts the lying length -- a single VALID minimal mobile packet panics the default kernel: 'panic: mbuf too short' at ip_input.c:576 (mobile). (3) the bcopy length 'm->m_len - msiz - (ip->ip_hl<<2)' goes negative for a datagram shorter than outer-header+msiz and is passed to bcopy as (size_t)-8 (ip_gre.c:237-238) -- 'mobileuflow' reproduced as 'panic: overflowed mbuf 0xfffff80117e0c400' with a SECONDARY PANIC in m_free<-sosendtcp<-soo_write: the unbounded copy had smashed neighbouring kernel-heap objects (the ssh session's mbuf came back corrupted). That is an unbounded attacker-initiated kernel-heap overwrite effective on production kernels too (the m_free INVARIANTS check merely makes it visible). Fix (header-presence guard, corrected ip_len arithmetic, M_HASH|M_LENCHECKED cleared) validated by rebuilding if_gre.ko in-guest: both variants + 20 stress injections harmlessly dropped, guest up.