# DF-3293 VERDICT — tcp_input() th_off validation gap

**Status: REPRODUCED (panic, both triggers) · Fix: FIXED (validated by kernel rebuild)**

## What was claimed

`sys/netinet/tcp_input.c:760-768` validates the TCP data offset (`th->th_off`)
only with a `KASSERT` — compiled out of production (non-INVARIANTS) kernels —
and the comment claiming "already checked and pulled up in ip_demux()" is
false for two reachable packet classes:

1. **IPv6**: no code on the v6 path validates `th_off` against the segment
   length. `ip6_input` enforces only `pkthdr.len == 40 + ip6_plen`
   (ip6_input.c:628-639); `tcp6_input` pulls up only the 20-byte base header
   (tcp_input.c:618); the `off <= tlen`-equivalent pull-up at tcp_input.c:771
   runs only for `off > 20` and only *after* the KASSERT at 766.
2. **IPv4 behind a decap path that leaves a stale `M_LENCHECKED`**:
   `gre_input2()` (sys/netinet/ip_gre.c:180-193) clears `M_HASH` but not
   `M_LENCHECKED`, so the re-injected inner packet skips
   `ip_lengthcheck()` in `ip_hashfn()` (ip_demux.c:295-298) and its TCP
   header is never validated. (wg(4) clears both flags — if_wg.c:85 —
   showing the intended protocol; gre(4) forgot.)

## What was demonstrated (guest: DragonFly 6.5-DEVELOPMENT, X86_64_GENERIC
— the default config, which ships `options INVARIANTS`, sys/config/X86_64_GENERIC:56)

### Baseline kernel #0 (stock INVARIANTS)

- **PoC A** (`tcp_off6.c`): one IPv6/TCP packet, `th_off=4`, valid checksums,
  to a *closed* port on a v6-capable interface — injected via `tap(4)` write,
  which lands in `ether_input()` exactly like a wire packet:

  ```
  panic: bad TCP data offset 16 (tlen 20)
  tcp_input() at tcp_input+0x11e1
  tcp6_input() at tcp6_input+0x64
  transport6_processing_handler() ...
  ```

  → unauthenticated, one-packet, **no listener, no config**, default kernel.
  Guest died (`vm.sh status` → down). Evidence: `panic.txt`.

- **PoC B** (`tcp_off4_gre.c`): gre0 tunnel configured (root setup; attacker
  = spoofed tunnel peer), outer IP/GRE frame, inner `th_off=15` with
  `ip_len=40` (`tlen=20`):

  ```
  panic: bad TCP data offset 60 (tlen 20)
  tcp_input() at tcp_input+0x11e1
  transport_processing_oncpu.isra.7() ...
  ip_input() at ip_input+0x71f
  ```

  Proves the stale-`M_LENCHECKED` bypass end-to-end (`gre0 Ipkts` confirmed
  the decap). Evidence: `panic.b.txt`.

### Patched kernel #1 (fix.diff applied in-guest, `make nativekernel`, reboot)

Both PoCs re-run byte-identically:

- PoC A: no panic; `netstat -s -p tcp` "discarded for bad header offset
  fields" 0→1; guest up. (`run.log`)
- PoC B: no panic; counter 1→2; `gre0 Ipkts=1` (decap still happens);
  guest up. (`run.2.log`)

## Production-kernel impact (reasoned, not executed — the guest has one kernel)

With `INVARIANTS` off, `tlen -= off` (tcp_input.c:768) produces a **negative
`tlen`** that flows through the whole segment state machine:

- inline fast data path (tcp_input.c:1346) and step-6 inline path (2438) do
  `tp->rcv_nxt += tlen` → receiver sequence regression / permanent stall;
- `tcp_reass()` queues entries whose `tqe_len` lies about the mbuf contents
  (tcp_input.c:523) → reassembly bookkeeping corruption (`m_adj()` clamps
  the actual trims — uipc_mbuf.c:1870-1883 — so the corruption stays in TCP
  state, not mbuf memory);
- with `TH_URG` set and `th_urp <= (u_long)tlen` (tcp_input.c:2387, always
  true for negative tlen), `tcp_pulloutofband()` walks past the data and
  hits the unconditional `panic("tcp_pulloutofband")` (tcp_input.c:2798) —
  a plain `panic()`, present in production builds.

So: INVARIANTS (default) kernels = remote one-packet panic; production
kernels = remote protocol-state corruption DoS (+ unconditional panic via
URG on the GRE vector).

## Exploit chain

Not a memory-corruption primitive (m_adj clamps the trims), so there is no
uid-0 route; the impact ceiling is remote denial of service. No escalation
chain attempted beyond that.

## Root cause line references

- `sys/netinet/tcp_input.c:764-768` — the KASSERT-only validation (the bug).
- `sys/netinet/ip_demux.c:295-298` — `ip_hashfn` skips `ip_lengthcheck`
  when `M_LENCHECKED` is set (assumes flag always accurate).
- `sys/netinet/ip_gre.c:180-193` — GRE decap clears `M_HASH` only.
- `sys/config/X86_64_GENERIC:56` — default config ships INVARIANTS.

## Fix

`fix.diff`: runtime check (`off < sizeof(struct tcphdr) || off > tlen` →
count `tcps_rcvbadoff`, `goto drop`), matching FreeBSD's long-standing
practice. Validated: baseline panics gone, drops counted, guest stable.
Recommended companion (separate commit, defense in depth):
`gre_input2()` should clear `M_LENCHECKED` as well.

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