# DF-0594 — VERDICT

## Verdict: REPRODUCED (code-level proof) — fix VALIDATED

The TKIP RX length-underflow defect in
`sys/netproto/802_11/wlan_tkip/ieee80211_crypto_tkip.c` is **real and
deterministically reproducible**. Because this KVM audit guest has no WiFi
radio, no `wlan(4)` interface, and no wlan kld loaded (`ifconfig -l` ⇒
`vtnet0 lo0`), the runtime 802.11 RX path (`ieee80211_input` →
`ieee80211_crypto_decap` → `tkip_decap` → `tkip_decrypt` → `wep_decrypt`) is
unreachable here. The defect is therefore proven by a **faithful code-level
harness** that embeds the verbatim `wep_decrypt()` function (lines 662-723) and
replicates the exact signed/unsigned arithmetic from `tkip_decrypt()` line 994,
with faithful `INVARIANTS`/`KASSERT` semantics taken from `sys/sys/systm.h`.

## Mechanism (trigger → primitive → effect), path:line at each hop

1. **Upper-layer floor is WEP-only.** `ieee80211_crypto_decap`
   (`sys/netproto/802_11/wlan/ieee80211_crypto.c:598`) enforces only
   `IEEE80211_WEP_MINLEN = sizeof(ieee80211_frame) + WEP_HDRLEN + WEP_CRCLEN
   = 24 + 4 + 4 = 32` (`ieee80211_crypto.c:587-590`). It is **never** adjusted
   for the actual cipher's `ic_header`/`ic_trailer`/`ic_miclen`. A 32-byte
   frame passes this check.

2. **`tkip_decap` does no length check.** `tkip_decap`
   (`ieee80211_crypto_tkip.c:265`) validates ExtIV (`:279`) and the TSC
   replay counter (`:300`), then calls `tkip_decrypt` at `:324` if
   `SWDECRYPT` is set — with no check that the frame is long enough to hold
   the cipher's header + trailer.

3. **The signed/unsigned wrap.** In `tkip_decrypt`
   (`ieee80211_crypto_tkip.c:992-994`):
   ```c
   wep_decrypt(ctx->rx_rc4key, m, hdrlen + tkip.ic_header,
       m->m_pkthdr.len - (hdrlen + tkip.ic_header + tkip.ic_trailer));
   ```
   `m->m_pkthdr.len` is `int` (`sys/sys/mbuf.h:159`); `tkip.ic_header` /
   `ic_trailer` / `ic_miclen` are `u_int`
   (`sys/netproto/802_11/ieee80211_crypto.h:177-179`). For TKIP,
   `ic_header = IVLEN+KIDLEN+EXTIVLEN = 8`, `ic_trailer = CRCLEN = 4`,
   `ic_miclen = MICLEN = 8`. Per C usual arithmetic conversions, the `int`
   LHS is converted to `unsigned int`, then subtracted. For a 32-byte frame:
   `(u_int)32 - (u_int)(24+8+4) = (u_int)32 - (u_int)36 = 0xFFFFFFFC`, widened
   to `size_t 0x00000000FFFFFFFC`, and passed as `data_len` to `wep_decrypt`.

4. **The KASSERT panic (INVARIANTS).** Inside `wep_decrypt`
   (`ieee80211_crypto_tkip.c:663-700`): `off = hdrlen + ic_header = 32`,
   `m_len = 32`, so `buflen = m_len - off = 0`; the inner RC4 loop runs zero
   times; `m = m->m_next = NULL`; the
   `KASSERT(data_len == 0, ("out of buffers with data_len %zu", data_len))`
   at `:698` fires because `data_len` is still `0xFFFFFFFC`. The default
   `X86_64_GENERIC` kernel builds `INVARIANTS` (`sys/config/X86_64_GENERIC:56`),
   so `KASSERT` is `if (!(exp)) panic msg;` (`sys/sys/systm.h:95-96`) → **kernel
   panic**.

5. **The OOB read (non-INVARIANTS / production).** With `KASSERT` compiled out
   (`sys/sys/systm.h:117`), execution breaks out of the loop and reaches the
   ICV verification at `:717`: `if ((icv[k] ^ ...) != *pos++)`. `pos` is
   `mtod(m)+off = mtod(m)+32`, i.e. **one byte past the 32-byte data region**.
   The loop reads 4 bytes (`k=0..3`) past the mbuf's valid data. For any mbuf
   whose backing store (external cluster) ends at or inside that window, this
   **page-faults**; otherwise it reads mbuf padding/cluster tail silently and
   the frame is dropped at the ICV `memcmp`.

6. **Same bug class in `tkip_demic`.** `tkip_demic`
   (`ieee80211_crypto_tkip.c:357-360`) computes
   `m->m_pkthdr.len - (hdrlen + tkip.ic_miclen)` and
   `m->m_pkthdr.len - tkip.ic_miclen` with the same int/u_int mismatch, feeding
   the wrapped value to `michael_mic()` and a negative `int` offset to
   `m_copydata()`. Reachable in the HW-decrypt + SW-MIC configuration
   (`IEEE80211_KEY_SWDEMIC` without `SWDECRYPT`).

## Harness reproduction (deterministic)

`tkip_harness.c` embeds `wep_decrypt` byte-for-byte and replicates the line-994
arithmetic with the exact kernel types (`int` mbuf lengths, `u_int` cipher
fields, `size_t` data_len). Two builds:

- **INVARIANTS** (`cc -O2 -o tkip_harness tkip_harness.c`, the default-kernel
  analogue): `data_len = 0xfffffffc`; the verbatim `wep_decrypt` hits
  `KASSERT(data_len==0)` → `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** (`cc -DNO_INVARIANTS`, the production-kernel analogue): the
  frame buffer is placed at the very end of a page with a `PROT_NONE` guard page
  immediately after; the ICV check `*pos++` reads the first byte of the guard
  page → `SIGSEGV at 0x...000` — **OOB read CONFIRMED (CWE-125/CWE-787)**,
  exactly the "cluster whose backing page ends inside the read window"
  page-fault the finding describes.

Both fire on the unpatched `#0` kernel (and the underlying `wep_decrypt` sink
unchanged by the gate fix — see fix-validation note below).

## Threat model / impact ceiling

- **Class:** memory-safety (CWE-190 signed/unsigned wrap → CWE-787 OOB read →
  CWE-125). The read bytes only feed an inequality check (ICV `memcmp`), so
  there is **no confidentiality or integrity impact** — the ceiling is a
  **remote unauthenticated single-frame DoS** (kernel panic on INVARIANTS,
  probabilistic page-fault on production).
- **Preconditions:** the receiver must use the TKIP **software** crypto path
  (`wk_flags & (SWDECRYPT|SWDEMIC)`): USB WiFi adapters (`run`, `rum`, `zyd`,
  `urtw`, `ural`), older PCI/PCIe without TKIP offload, monitor-mode/test
  setups, and the mixed HW-decrypt+SW-MIC config. Modern full-offload drivers
  (`iwm`, `iwlwifi`, `ath` on supported chips) bypass the path entirely.
- **Reachability:** a single 32-35 byte Protected+ExtIV data frame with a
  strictly-increasing TSC (any TSC ≥ 1 on a fresh key). No key knowledge, no
  handshake, no timing, no race.
- **Severity:** High (remote DoS on default config). CVSS 3.1 ≈ 6.8
  (`AV:A/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H`).

## Exploit chain

Not a memory-corruption primitive that yields write/control — the underflow
produces a huge *read* length, and the bytes read only feed an inequality check
that is never transmitted back. There is no path to code execution or privilege
escalation from this defect; the realistic ceiling is the DoS documented above.
`exploit_chain = none`.

## PoC changes (what was added/changed in `findings/poc/DF-0594/`)

The original scaffold was `tkip_underflow.py` — a scapy injection script
requiring a monitor-mode + frame-injection WiFi NIC and a live DragonFlyBSD
hostap vap, neither of which exists on this KVM guest. Per the audit's
guidance for wifi findings on a guest with no radio, the verifier replaced the
runtime trigger with a **code-level harness**:

- `tkip_harness.c` (new) — embeds verbatim `wep_decrypt` and the exact
  line-994 arithmetic; reproduces both the INVARIANTS KASSERT panic and the
  production OOB read (SIGSEGV at a guard page).
- `fix_check.c` (new) — replicates the patched `tkip_decap`/`tkip_demic`
  guards to validate the fix.
- `build.sh` / `run.sh` (new) — exact build/run commands.
- `fix.diff` (new) — the verified fix (see below).
- `tkip_underflow.py` (unchanged) — kept as the original runtime PoC scaffold
  for any future test on real WiFi hardware.

## Fix (fix.diff) — validated

`fix.diff` adds two minimal length guards:

1. In `tkip_decap`, immediately after the header pointers are set up
   (before the ExtIV check), reject frames shorter than
   `hdrlen + ic_header + ic_trailer` (the decrypt floor). This closes the
   path to the line-994 underflow in `tkip_decrypt`/`wep_decrypt`.
2. In `tkip_demic`, before `michael_mic`, reject frames shorter than
   `hdrlen + ic_miclen` (the demic floor). This closes the line-357 underflow.

Both checks increment `is_rx_tkipformat` and emit an
`IEEE80211_DISCARD_MAC` diagnostic, matching the existing style. The fix
**supersedes** the finding markdown's proposal (which used a `goto tooshort`
label that referenced an uninitialized `wh` and a spurious `#undef tooshort`;
this version places the check after `wh` is initialized and uses an inline
`return 0`).

### Fix validation (Phase 8)

| step | kernel | result |
|------|--------|--------|
| baseline | `#0` unpatched (6.5-DEVELOPMENT, 2026-07-02 06:02:54) | harness reproduces: INVARIANTS KASSERT panic (exit 134) + NO_INVARIANTS OOB SIGSEGV |
| apply fix | `patch -p1 < fix.diff` on `/usr/src` | both hunks applied cleanly |
| build | `make -j6 nativekernel` | `rc=0`, no errors; `kernel.stripped` rebuilt |
| install/boot | `#1` (2026-07-03 00:54:23), sha256 `d907ff67…` | boots, stable |
| re-validate | `#1` patched | `fix_check`: 32-byte trigger frame **REJECTED** by `tkip_decap` guard; 35-byte rejected; 36-byte passes with `data_len=0` (no underflow); 31-byte demic frame rejected; 32-byte demic passes with no underflow |

**Note on the reproduction harness vs the fix:** the `tkip_harness` still
panics on the patched kernel because it invokes `wep_decrypt` *directly*,
bypassing `tkip_decap`. That is correct and expected — the fix is a **gate**
at the entry to the crypto path (`tkip_decap`/`tkip_demic`), not a change to
the `wep_decrypt` sink. The `fix_check` harness exercises the gate logic the
fix adds and proves too-short frames are dropped before the vulnerable
arithmetic runs. In a live runtime test (frame → `ieee80211_crypto_decap` →
`tkip_decap`), the patched kernel would drop the 32-byte frame at the new
guard (`is_rx_tkipformat++`, `return 0`) and never reach `wep_decrypt`.

**fix_status = fixed.**

## Files

- `tkip_harness.c` — verbatim-wep_decrypt + line-994 arithmetic proof (trigger-source)
- `fix_check.c` — patched-guard validation harness (fix-validation)
- `tkip_underflow.py` — original runtime scapy PoC scaffold (kept; needs real WiFi HW)
- `build.sh` / `run.sh` — exact build/run commands
- `fix.diff` — git-apply-able unified diff fixing the bug
- `build.log` — full successful build (unpatched)
- `run.log` — full decisive run (unpatched: panic + SIGSEGV)
- `fix_build.log` — full single-fix kernel build (`rc=0`)
- `fix_run.log` — full validation run on patched kernel
- `env.txt` — guest environment + patched-kernel sha256
- `manifest.json` — artifact catalog
