# DF-3748 — VERDICT

**status: not_reproduced (hardware absent on audit guest) — source-certain, every link proven from code**

## The bug, end to end

1. **Attacker controls `ni_associd` in STA mode.**
   `sys/netproto/802_11/wlan/ieee80211_sta.c:1688`:
   `ni->ni_associd = associd;` where `associd` is the raw 16-bit AID field of the
   AP's (re)association response (`ieee80211_sta.c:1627`). There is **no range
   validation anywhere** (only `status != 0` at :1620 and rate negotiation are checked).
   An AID of 254 (or any value ≡254 mod 256, and the 0xC000-bits are ignored by
   `IEEE80211_AID(b) ((b) &~ 0xc000)`, ieee80211.h:1314) is fully IEEE-legal and is
   simply accepted.

2. **The driver truncates it into an 8-bit index.**
   `sys/dev/netif/ral/rt2860.c:883-887` (rt2860_newassoc):
   ```c
   uint8_t wcid;
   wcid = IEEE80211_AID(ni->ni_associd);   /* 14-bit value truncated to 8 bits */
   if (isnew && ni->ni_associd != 0) {
       sc->wcid2ni[wcid] = ni;             /* OOB WRITE when AID&0xff == 254 or 255 */
   ```
   `sc->wcid2ni` is `struct ieee80211_node *wcid2ni[RT2860_WCID_MAX]` with
   `RT2860_WCID_MAX = 254` (rt2860var.h:148, :28) → valid indices 0..253.
   The comment at rt2860.c:493 ("HW supports up to 255 STAs (0-254)") and
   `vap->iv_max_aid = min(IEEE80211_AID_MAX, RT2860_WCID_MAX)` (rt2860.c:494) only
   protect the **hostap-mode AID allocator** (`for (aid = 1; aid < vap->iv_max_aid; aid++)`,
   wlan/ieee80211_node.c:2539 → max AID 253). In STA mode the AID is *received from
   the AP*, never passed through `iv_max_aid` — the guard does not apply.

3. **Reachability of newassoc in STA mode.**
   `ieee80211_sta.c:453-454`: on the S_ASSOC→S_RUN transition (i.e. right after the
   assoc response was parsed and `ni_associd` set at :1688) net80211 calls
   `ic->ic_newassoc(vap->iv_bss, ostate != IEEE80211_S_RUN)` → `rt2860_newassoc`
   with `isnew=1`. Pure remote: victim only has to *associate* to the attacker's AP
   (open/WEP/WPA2-PSK all work — the AID is inside the encrypted-after-auth assoc
   exchange, attacker is the AP so it sees everything).

4. **What the stray pointer write corrupts.**
   softc layout (rt2860var.h:148-151):
   ```c
   struct ieee80211_node *wcid2ni[254];   /* ends exactly at … */
   struct rt2860_tx_ring txq[6];           /* … &txq[0].txd (first member) */
   ```
   - AID ≡ 254 (mod 256) → `wcid2ni[254] = ni` overwrites `txq[0].txd` (kernel
     virtual address of ring-0's 64-entry `struct rt2860_txd` DMA block) with a
     pointer to the live BSS `ieee80211_node`.
   - AID ≡ 255 (mod 256) → `wcid2ni[255] = ni` overwrites `txq[0].paddr`.
   - Every subsequent `rt2860_tx()`/`rt2860_tx_raw()` on AC-BE (qid 0) does
     `txd = &ring->txd[ring->cur]` (rt2860.c:1715) and writes
     sdp0 (kernel-physical TXWI addr), sdl0/sdl1 (lengths — attacker-traffic-sized),
     flags at `node + cur*16`, walking `cur` 0..63 (rt2860.c:1726,1756) — a
     ≥1 KB, 16-byte-stride semi-controlled write across the node (which embeds
     `ni_vap`, `ni_ic`, refcount, crypto key pointers) and past it into adjacent
     M_80211_NODE heap. `bus_dmamap_sync`/`RAL_WRITE(TX_CTX_IDX)` still use the
     real ring, so nothing detects the corruption.

5. **Secondary sink of the same truncation (OOB read).**
   `rt2860_tx` rt2860.c:1586-1587 `txwi->wcid = IEEE80211_AID(ni->ni_associd);`
   (uint8 truncation again) → the MAC latches it into TX_STAT_FIFO →
   `rt2860_drain_stats_fifo` rt2860.c:1136-1137
   `wcid = (stat >> 8) & 0xff; ni = sc->wcid2ni[wcid];` — the only guard is
   `wcid == 0xff` (rt2860.c:1140), **254 is not filtered** → `wcid2ni[254]` is
   read (== the already-corrupted `txq[0].txd`/node pointer — after (4) this
   returns the node and "works"; if the newassoc write somehow did not alias yet,
   it yields the `txq[0].txd` DMA pointer which is then dereferenced as a node:
   `ni->ni_vap` at :1157/:1160/:1162 → `ieee80211_ratectl_tx_complete`
   (ieee80211_ratectl.h:99-103, indirect call `vap->iv_rate->ir_tx_complete`) →
   wild pointer chase).
   Same family: `rt2860_ampdu_rx_start/stop` rt2860.c:919/:934 feed an unbounded
   `wcid` into `RT2860_WCID_ENTRY(wcid)` (MMIO 0x1800+8*wcid; wcid≤255 stays inside
   the register aperture, so MMIO-only).

## Why not reproduced on this guest
`dfbsd-qemu/vm.sh run_root 'pciconf -lv | grep ralink'` → no device (env.txt);
interfaces are `vtnet0 lo0` only. No Ralink RT2860/3090/3390/3562/5390/5392 NIC can
be attached to this VM, so the trigger (associate to rogue AP) cannot execute.
Source chain above is fully proven; the sole hardware-dependent step is step 3's
radio exchange.

## Exploitability assessment
- **Crash**: near-certain (first ring-0 TX after association scribbles descriptors
  into the node; ni_vap/ni_ic corruption → panic on next rx/tx/stats).
- **Controlled corruption → RCE**: plausible but not demonstrated. The descriptor
  fields written into the node are partially attacker-influenced (packet lengths →
  sdl0/sdl1 16-bit values; sdp0 = physical address of TXWI pool blocks; flags =
  qsel). A `cur`-steering + heap-grooming path (spray M_80211_NODE-adjacent objects,
  overwrite a function-pointer-bearing victim at node+0x2xx) is conceivable on this
  no-SMAP/no-KASLR-class target but requires hardware iteration — recorded as the
  follow-up on any machine with the NIC.

## Fix
`fix.diff` in this directory (grow `wcid2ni` to the full 8-bit WCID space [256]
so the truncation can no longer escape the array, plus guard the stats-drain read).
A belt-and-braces net80211 validation of the AP-provided AID
(`ieee80211_sta.c:1688`: reject/mask AIDs > IEEE80211_AID_MAX) is recommended
upstream but not required to close this bug.

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