# DF-0612 — VERDICT

**Finding:** `ieee80211_parse_tdma()` join path skips IE-length and `tdma_slot`
validation → heap OOB write via `setbit()` from a crafted TDMA vendor IE.
**File:** `sys/netproto/802_11/wlan/ieee80211_tdma.c:644-669`

## Verdict: REPRODUCED (code-level) — fix VALIDATED on a single-fix kernel

The bug is **real and present in the default GENERIC kernel** (`nm /boot/kernel/kernel`
shows `ieee80211_parse_tdma` as a `T` symbol, compiled in directly). The
sibling handler `tdma_process_params()` guards the same `setbit()` with an
IE-length check (`ieee80211_tdma.c:536`) and a `tdma_slot` range check
(`:555`); `ieee80211_parse_tdma()` has **neither**.

The **live 802.11 receive/join path is unreachable on this QEMU guest** (only
`vtnet0`/`lo0`, no WiFi radio, no wlan module loaded), so the bug cannot be
triggered end-to-end here. Per the per-PoC procedure, a deterministic
**code-level harness** that replicates the exact kernel struct layouts and the
exact buggy line is used to prove the primitive, and a **binary-level
before/after** on the actual shipped kernel proves the fix.

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

- `ieee80211_node.c:848` — join path: `if (ni->ni_ies.tdma_ie != NULL) ieee80211_parse_tdma(ni, ni->ni_ies.tdma_ie);` (guarded by `IEEE80211_SUPPORT_TDMA`).
- `ieee80211_tdma.c:650-652` — casts attacker IE directly to `const struct ieee80211_tdma_param *`, fetches `ts = vap->iv_tdma`.
- `ieee80211_tdma.c:657` — **the buggy line**: `setbit(ts->tdma_inuse, tdma->tdma_slot);` with no prior validation.
- `sys/param.h:390` — `#define setbit(a,i) ((a)[(i)/NBBY] |= 1<<((i)%NBBY))`, `NBBY=8`.
- `ieee80211_tdma.h:73` — `uint8_t tdma_inuse[1];` — a **1-byte** array.
- `ieee80211_tdma.h:54` — `tdma_slot` is `u_int8_t` (0..255).

`setbit` therefore writes at `&ts->tdma_inuse[0] + tdma_slot/8`, i.e. struct
offset `8 + slot/8`. Verified struct offsets (`__packed`-false, x86_64):
`tdma_inuse@8`, `tdma_active@9`, `tdma_count@12`, `tdma_peer@16`,
`tdma_lastprint@24`, `tdma_fails@40`, **function pointers (`tdma_newstate`
etc.)@48**.

- slot 0..7 → offset 8 (in-bounds)
- slot 8..15 → offset 9 (`tdma_active`)
- slot 32..39 → offset 12 (`tdma_count`)
- slot 64..71 → offset 16 (`tdma_peer` byte 0)
- slot 248..255 → offset 39 (last byte of `tdma_lastprint`)

Max reach is offset 39 (slot is u8 → 255/8=31). The function pointers at
offset 48 are **NOT reachable** — this bounds the primitive to TDMA **state
corruption / protocol-DoS**, with **no RIP control / no `uid0` path**.

Secondary bug: because the IE length is never checked, reading
`tdma->tdma_slot` (offset 8), `tdma_slotcnt`, `tdma_slotlen`, `tdma_bintval`,
`tdma_inuse` from a deliberately short IE (the `istdmaoui` accept test only
requires `ie[1] > 3`) is a heap OOB **read** within the `ies->data` blob.

## Harness proof (deterministic)

`harness.c` includes the verbatim kernel header and reproduces the exact
`setbit(ts->tdma_inuse, tdma->tdma_slot)` line. Decisive runs:

```
[Case 1] slot=64 -> setbit writes tdma_peer byte 0 (offset 16)
    [before] ... tdma_peer=0xdeadbeefcafebabe ...
    [UNFIXED] ... tdma_peer=0xdeadbeefcafebabf ...
    -> tdma_peer changed 0xdeadbeefcafebabe -> 0xdeadbeefcafebabf : OOB WRITE CONFIRMED
[Case 3] slot=8 -> tdma_active[0] = 0x01 : OOB WRITE CONFIRMED  (1 past the array)
[Case 4] short IE (len=6) -> tdma_slot read as 0xab (offset 8) : OOB READ CONFIRMED
[Case 5] FIXED rejects slot=64; tdma_peer UNCHANGED (0xdeadbeefcafebabe): FIX HOLDS
[Case 6] FIXED accepts slot=1 in-bounds; legit path preserved
```

## Binary-level before/after (the fix is in the shipped kernel)

`objdump -d --disassemble=ieee80211_parse_tdma`:

**#0 baseline** (`Thu Jul 2 06:02:54`, sha baseline):
```
movzbl 0x8(%rsi),%ecx          ; read tdma_slot, NO length check
shr    $0x3,%al ; and $0x1f,%eax
or     %dl,0x8(%rdi,%rax,1)    ; setbit at variable offset 8+(slot/8)  -> OOB
```
No `cmp` against `ie[1]`; no `cmp` against `TDMA_MAXSLOTS`.

**#1 single-fix** (`Wed Jul 8 20:37:20`, sha `8048ccf4…`, built from `fix.diff`):
```
movzbl 0x1(%rsi),%r8d ; cmp $0x15,%r8b ; jbe reject   ; len guard: ie[1]<22 -> reject
movzbl 0x8(%rsi),%ecx ; cmp $0x1,%cl   ; jbe accept    ; slot guard: slot>=2 -> reject
... accept path:
or     %al,0x8(%rdx)                                       ; setbit at FIXED offset 8 (slot provably 0..1)
```
Both guards compiled in; `setbit` now writes only at fixed offset 8 (the
compiler proved `slot/8 ≡ 0` for slot ∈ {0,1}). The OOB is eliminated.

## Exploit chain / escalation

**Not applicable** — two valid hard blockers:
1. **Reachability blocker:** the live path requires a WiFi radio + TDMA vap
   scanning/joining. This QEMU guest has only `vtnet0`/`lo0`; no wlan module
   is loaded; the path is unreachable here. (On a real DragonFlyBSD TDMA host
   — e.g. an `ath`/`ath9k` long-distance link — it is reachable by an
   unauthenticated adjacent-network attacker via a spoofed beacon carrying the
   forged TDMA vendor IE.)
2. **Primitive ceiling:** even on a real host, the OOB write is bounded to
   struct offsets 9..39 (slot is `u_int8_t`, max index 31). It **cannot reach
   the function pointers at offset 48+**, so there is no RIP control and no
   path to `uid=0`. The realistic impact is TDMA link desynchronization /
   state corruption (DoS) of the affected TDMA vap, plus a secondary OOB read
   from a short IE.

Severity Medium (matches the finding): unauthenticated adjacent-network OOB
write, deterministic once a forged beacon is selected for join, but bounded to
TDMA state and requiring the niche TDMA mode.

## PoC changes

- Added `harness.c` — a faithful userspace replica of the exact kernel
  `struct ieee80211_tdma_state`/`ieee80211_tdma_param` layouts (verbatim
  header) and the exact buggy `setbit(ts->tdma_inuse, tdma->tdma_slot)` line
  (`ieee80211_tdma.c:657`), plus a FIXED-function variant mirroring the
  proposed guards. Drives crafted IEs (slot=64/255/8 OOB; short IE OOB read)
  and demonstrates the corruption and that the fix blocks it.
- The original `inject_tdma_beacon.py` is kept as the live-frame injector for
  real WiFi hardware (not runnable on QEMU).
- Added `fix.diff` — standalone `git apply`-able unified diff adding the
  length check (`ie[1] < sizeof(*tdma)-2`) and slot range check
  (`tdma->tdma_slot >= TDMA_MAXSLOTS`) before the `setbit`, mirroring the
  sibling guards at `:536`/`:555`. `git apply --check` passes.
- Added `build.sh`/`run.sh` repro scripts.

## Fix validation (Phase 8) — VALIDATED

- **Baseline (#0):** harness Case 1 corrupts `tdma_peer` (`...babe`→`...babf`);
  `ieee80211_parse_tdma` in `/boot/kernel/kernel.debug` has no guards.
- Applied `fix.diff` to in-guest `/usr/src`, built
  `make -j6 nativekernel KERNCONF=X86_64_GENERIC` (`fix_build.log`,
  `NK_DONE rc=0`), `make installkernel`, rebooted.
- **Patched (#1, `Wed Jul  8 20:37:20`, sha `8048ccf4…`):** harness Case 5
  rejects slot=64, `tdma_peer` unchanged; `ieee80211_parse_tdma` in the booted
  `/boot/kernel/kernel.debug` now contains `cmp $0x15` (length guard) and
  `cmp $0x1` (slot guard) before a fixed-offset `or %al,0x8(%rdx)`.

`fix_status: fixed` — the OOB write present in the #0 kernel is gone in the
#1 single-fix kernel; legit in-range slots (0..1) still work.

## Recommended fix

The fix in `fix.diff` **matches** the finding markdown's proposal (same two
guards, same `IEEE80211_DISCARD_IE` style as the sibling). One minor
refinement over the finding's diff: the slot-range reject branch also emits an
`IEEE80211_DISCARD_IE` (matching `tdma_process_params:556-558`) rather than a
bare `return`, for observability.

## Environment

- Guest: DragonFly 6.5-DEVELOPMENT x86_64, gcc 8.3 (DragonFly).
- Interfaces: `vtnet0 lo0` only — **no WiFi radio**; wlan module not loaded.
- `ieee80211_parse_tdma` is compiled directly into `/boot/kernel/kernel`
  (default `X86_64_GENERIC`), so the vulnerable code ships by default; only
  the live trigger path is absent on this guest.
