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