# DF-5032 — AR5212 RF gain readback reads 2 words past the 3-word RF bank 7 (2413/2316/2317/5413/2425/2417 radios)

## What

`ar5212InvalidGainReadback()` (ar5212_rfgain.c:123-154) takes the
non-5112/5111 branch for the newer AR5212-family radios and extracts the
6-bit gain step from RF bank 7:

	gStep = ar5212GetRfField(ar5212GetRfBank(ah, 7), 6, 37, 0);   /* :140 */

`ar5212GetRfField()` (ar5212_rfgain.c:92-120) walks the bank as bytes:
firstBit=37, numBits=6 → `rfBuf[4]` then `rfBuf[5]` (see the walk in
`oob_demo.c`, output in `oob_demo.log`).

But for these radios, bank 7 has only **3 rows** in the shared ini tables
(sys/dev/netif/ath/ath_hal/ar5212/ar5212.ini):

| radio | bank7 rows | struct holding it | what follows Bank7Data |
|---|---|---|---|
| ar5111 | 8 | ar5111.c:44 | (in bounds: 4,5 < 8) |
| ar5112 | 13 | ar5112.c:43 | (takes the IS_RAD5112_ANY branch — index 4 only) |
| ar2413 | 3 (ar5212.ini:1870) | ar2413.c:43 | vpdTable_L (intra-struct) |
| ar2316 | 3 (:1878) | ar2316.c | vpdTable_L (intra-struct) |
| ar5413 | 3 (:1886) | ar5413.c:43 | vpdTable_L (intra-struct) |
| ar2317 | 3 (:2158) | ar2317.c | vpdTable_L (intra-struct) |
| ar2425/2417 | 3 (:2166) | ar2425.c:44 | **nothing — last member** |

Reachability gate: the probe state is only armed on 5 GHz channels (or
5112+OFDM) — `ar5212PerCalibrationN()`, ar5212_reset.c:1085-1094 →
`ar5212RequestRfgain()`; the driver polls `ath_hal_getrfgain()` each
calibration tick (if_ath.c:5401) → `ar5212GetRfgain()` →
`ar5212InvalidGainReadback()`.

Consequently:

* **ar5413** (a/b/g, 5 GHz-capable, common card silicon): the OOB read is
  *live on every long calibration cycle on a 5 GHz channel*.  The 8 bytes
  past `Bank7Data[3]` land inside `vpdTable_L` in the same
  `ath_hal_malloc()`'d RF private struct (ar5413.c:35-57,777) — undefined
  behavior / array OOB in C, but stays inside the allocation.
* **ar2425/ar2417**: `Bank7Data` is the *final* member of `ar2425State`
  (ar2425.c:36-45), so the same two reads would be a **heap OOB read of 8
  bytes past the allocation** — but these radios are 2.4 GHz-only, the
  channel gate (ar5212_reset.c:1090) never arms the probe on them, so the
  path is dormant unless a 5 GHz channel is ever configured on such a
  part (e.g. by future band-capability changes).
* ar2413/2316/2317: same struct shape as 5413 (vpd tables follow) and same
  2.4 GHz-only gating.

Impact: `gStep` is garbage derived from adjacent fields → `L2/L3/L4`,
`gv->loTrig/hiTrig` (ar5212_rfgain.c:143-149) are wrong → the gain-ladder
adjust decisions (`ar5212AdjustGain`) misfire → RF gain calibration
misbehaves (link-quality degradation; worst case the radio walks the gain
ladder to a rail).  The value never leaves the HAL; no disclosure or
corruption is reachable from it.  Severity: **Low** (hardware-triggered,
no attacker control, functional impact).

## Files

* `oob_demo.c` — replicates `ar5212GetRfField`'s index walk
  (ar5212_rfgain.c:102-117) for all four call sites in the file and prints
  the touched word indices against the real `N(ar5212Bank7_*)` row counts.
* `oob_demo.log` — its output on the audit guest (exit 1: 5 radios OOB).
* `env.txt` — guest uname / cc.
* `verdict.json` / `manifest.json`.

## Build / run (any UNIX with a C compiler; ran on the DragonFly guest)

    cc -O2 -o oob_demo oob_demo.c && ./oob_demo

## Expected

    InvalidGainReadback/else GetRfField(bank7, 6, 37, 0)
      words touched: [4] [5]  (max index 5)
    ...
    ar5413  (else path)   N(ar5212Bank7_5413)    =  3 rows -> index 5 *** OUT OF BOUNDS (reads 2 words past) ***

Kernel-level verification (panic/corruption) is intentionally skipped:
Low severity, no memory-safety consequence beyond the intra-struct read on
the reachable path, and no AR5412/5413 hardware exists on the audit guest.

## Fix

Extend the bank-7 shadow arrays for the affected radios (or clamp the
read): simplest correct fix is to make `ar5212InvalidGainReadback()` use a
bank-7 field layout matching each radio family; minimal hardening is to
size `Bank7Data[]` to 6 words minimum in the 2413/2316/2317/5413/2425/2417
RF private structs so the byte-walk stays inside the array.  See
`verdict.json: recommended_fix`.
