# DF-4315 — ath_rx_tap: device-fed rs_rate → rateCodeToIndex[] 0xFF sentinel → sc_hwmap[255] OOB read

## What

The ath(4) RX path trusts the hardware-reported rate code `rs->rs_rate`
(8 bits, straight from the RX status descriptor) and passes it through the
HAL back-mapping `rt->rateCodeToIndex[rs->rs_rate]`, whose unmapped entries
are pre-initialised to `(uint8_t)-1` = 0xFF (`ah.c:573-574`). The result is
used, **unchecked**, as an index into `sc->sc_hwmap[32]`:

- `ath_rx_tap()` — `if_ath_rx.c:577-579`
  (`wr_rate`/`wr_flags` are copied into the per-frame radiotap header)
- the debug dump path — `if_ath_rx.c:900-903`
- `ath_led_event()` via `if_ath_rx.c:990` → `if_ath_led.c:194`
  (`sc->sc_hwmap[rix].ledon/.ledoff`, rix passed as `int` = 255, no check)

`sc_hwmap[255]` reads an 8-byte record 2040 bytes into a 256-byte array
(~1.8 KB past its end). The read stays inside the (very large) `ath_softc`
allocation — no fault — but the garbage bytes are exported, two per frame,
into the radiotap header (`wr_rate`, `wr_flags`) delivered to BPF listeners
on monitor-mode VAPs, and the OOB `ledon/ledoff` values become LED blink
timings.

## Root cause chain (path:line)

1. `ar5416_recv.c:200` `rs->rs_rate = RXSTATUS_RATE(ah, ads)` —
   `MS(ds_rxstatus0, AR_RxRate)` with `AR_RxRate = 0xff000000` (8 bits,
   `ar5416desc.h:336`); `ar5212_recv.c:276` equivalent for older parts.
   Fully device-fed.
2. `ah.c:571-583`: `rateCodeToIndex[]` initialised to `(uint8_t)-1` for all
   256 codes, then only the codes of the *current mode's* table are mapped.
3. `if_athvar.h:716`: `sc_hwmap[32]`.
4. `if_ath_rx.c:577`: `rix = rt->rateCodeToIndex[rs->rs_rate];` (uint8_t) —
   no `rix == 0xff` check → `sc->sc_hwmap[255]` at 578/579.
5. `if_ath_rx.c:990` / `if_ath_led.c:185-194`: same sentinel reaches
   `ath_led_event(sc, 255)` → `sc->sc_hwmap[255].ledon/ledoff`.

When is a received code unmapped? The current table (`sc_currates`) covers
only the current PHY mode. Monitor-mode radios receive *whatever is on air*:
XR-coded frames (AR5212 proprietary codes), HT MCS rates above the loaded
table's chain count (e.g. 3-stream MCS 16-23 arriving at a 1/2-stream table),
or rates of a different mode during scan/channel-change windows.

## Threat

Unauthenticated wifi peer (or any RF environment on a monitor-mode box) →
2 bytes/frame of out-of-bounds softc memory exported into the radiotap header.
BPF capture requires privilege on DragonFly, and the leaked bytes come from
adjacent `ath_softc` members, so the practical impact ceiling is low: a
correctness bug + minor privileged-only info exposure. No write primitive
(led timings are clamped by hardware callout semantics to u16 garbage).

Severity: Low. CWE-125. Confidence: certain for the code mechanics,
likely for over-the-air reachability (mode-mismatch frames).

## Run (exact, on the audit guest)

```
vm.sh run_root 'sh /tmp/trigger.sh'
# -> vtnet0 lo0 ; no Atheros PCI device (vendor 0x168c)   [see run.log]
```

The audit QEMU guest has no Atheros NIC (`run.log`, `env.txt`), so the driver
cannot attach and the RX path is unreachable at runtime there — recorded as
`not_reproduced / impact=none` for the observable-on-this-guest result.
On real hardware the observation recipe is: monitor VAP + `tcpdump -i wlan0 -w
cap.pcap`, inject/air frames at a rate outside the loaded mode table, then
inspect `wr_rate`/`wr_flags` bytes of captured frames for values inconsistent
with any valid rate (e.g. `wr_rate` byte matching softc bytes ~2 KB past
`sc_hwmap`, varying with softc state rather than PHY rate).

## Fix

`fix.diff` (git-apply-clean): clamp the 0xFF sentinel to a valid index (0) at
the three use sites in `if_ath_rx.c`. (Twin sites elsewhere —
`if_ath_tdma.c:407`, `ath_rate/sample/sample.c:872` — follow the same pattern
but are out of this file's scope.)

## Artifacts

`trigger.sh`, `run.log`, `env.txt`, `fix.diff`, `VERDICT.md`,
`manifest.json`, `verdict.json`
