# 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.
