# DF-0594 — PoC: TKIP RX length underflow → OOB read + KASSERT panic

Remote unauthenticated single-frame DoS. `tkip_decap` accepts any frame that
`ieee80211_crypto_decap` lets through (≥32 B, the WEP-only minimum), but TKIP
needs ≥36 B for decrypt. The signed/unsigned mismatch in
`m->m_pkthdr.len - (hdrlen + tkip.ic_header + tkip.ic_trailer)` (int − u_int)
wraps to `0xFFFFFFFC` and is passed as `data_len` to `wep_decrypt`, whose
`KASSERT(data_len == 0)` fires on INVARIANTS kernels (the default
`X86_64_GENERIC`); on production kernels the ICV `*pos++` check reads past the
mbuf end (OOB read, CWE-125/787).

## Runtime PoC (original scaffold — needs real WiFi hardware)

`tkip_underflow.py` — scapy injection script. Requires a monitor-mode +
frame-injection-capable WiFi NIC (e.g. AR9271/`ath9k_htc`) and RF proximity to
a DragonFlyBSD hostap vap using TKIP with the SW crypto path. **Not runnable on
this KVM audit guest** (no wifi radio, no wlan kld).

```
# attacker (Linux + scapy + injection NIC in monitor mode):
sudo python3 tkip_underflow.py wlan0mon <target_ap_bssid>
# target (DragonFlyBSD hostap vap with SW TKIP decrypt):
#   ifconfig wlan0 create wlandev run0 wlanmode hostap ...
```

## Code-level proof (what runs on this guest)

Because this guest has no 802.11 path, the defect is proven deterministically
by `tkip_harness.c`, which embeds the **verbatim** `wep_decrypt()` function
(`ieee80211_crypto_tkip.c:662-723`) and replicates the exact signed/unsigned
arithmetic from `tkip_decrypt()` line 994, with faithful `INVARIANTS`/`KASSERT`
semantics from `sys/sys/systm.h`. Two builds:

- **INVARIANTS** (`./tkip_harness`) — the default-kernel analogue: the
  verbatim `wep_decrypt` hits `KASSERT(data_len==0)` at line 698 →
  `panic: out of buffers with data_len 4294967292` → abort (exit 134). This is
  the deterministic DoS on the default `X86_64_GENERIC` kernel.
- **NO_INVARIANTS** (`./tkip_harness_noinv`) — the production-kernel analogue:
  the frame buffer is placed at a page boundary with a `PROT_NONE` guard page
  after it; the ICV `*pos++` check at line 717 reads the first byte of the guard
  page → `SIGSEGV` — OOB read CONFIRMED (CWE-125/CWE-787).

## Build & run (code-level harness)

```
./build.sh        # builds tkip_harness (INVARIANTS) and tkip_harness_noinv
./run.sh          # runs both; INVARIANTS panics, NO_INVARIANTS SIGSEGVs
```

## Expected outcome

```
# INVARIANTS build:
panic: out of buffers with data_len 4294967292
cpuid = 0
Abort trap (core dumped)              # exit 134

# NO_INVARIANTS build:
[SIGSEGV at 0x...000 — OOB READ past mbuf data end]
RESULT: SIGSEGV in wep_decrypt ICV check — OOB READ CONFIRMED (CWE-125/CWE-787)
```

## Fix validation

`fix.diff` adds length guards in `tkip_decap` (reject < `hdrlen + ic_header +
ic_trailer`) and `tkip_demic` (reject < `hdrlen + ic_miclen`). `fix_check.c`
replicates the patched guards and confirms the 32-byte trigger frame is now
rejected before the vulnerable arithmetic, while legitimate (≥36 B decrypt,
≥32 B demic) frames pass with no underflow. Validated on a built-and-booted
single-fix kernel (`#1`). See `VERDICT.md` for the full before/after.

## Files

- `tkip_harness.c` — code-level reproduction (verbatim wep_decrypt + line-994 arithmetic)
- `fix_check.c` — fix-validation harness (patched-guard logic)
- `tkip_underflow.py` — original runtime scapy PoC scaffold (kept; needs real WiFi HW)
- `build.sh` / `run.sh` — build/run commands
- `fix.diff` — git-apply-able unified diff fixing the bug
- `VERDICT.md` — full narrative + fix before/after
- `build.log` / `run.log` — full unpatched-kernel logs
- `fix_build.log` / `fix_run.log` — full single-fix-kernel logs
- `env.txt` — guest environment + patched-kernel sha256
- `manifest.json` — artifact catalog
