# DF-3960 — VERDICT

**REPRODUCED at the function level** (impact: `none` — OOB reads feed PHY
registers only), confidence: certain.

## Root cause — three unvalidated EEPROM-derived edge cases

`ar5416SetPowerCalTable()` (sys/dev/netif/ath/ath_hal/ar5416/ar5416_reset.c:2337-2405)
takes `xpdMask` straight from EEPROM
(`pEepData->modalHeader[].xpdGain`, :2354) and computes
`numXpdGain = ar5416GetXpdGainValues(ah, xpdMask, ...)` (:2371). For an
empty mask the function returns **0** with no validation (:2247-2265),
and `ar5416GetGainBoundariesAndPdadcs(..., numXpdGains = 0)` is called
(:2387-2391). Inside (:2413-2576):

**(a)** With `numXpdGains == 0` both `for (i = 0; i < numXpdGains; i++)`
loops (:2462, :2497) leave `i == 0` and `k == 0`, so:

```c
while (i < AR5416_PD_GAINS_IN_MASK) {              /* :2566 */
        pPdGainBoundaries[i] = pPdGainBoundaries[i-1];   /* reads [-1] */
        i++;
}
while (k < AR5416_NUM_PDADC_VALUES) {              /* :2571 */
        pPDADCValues[k] = pPDADCValues[k-1];   /* reads [-1] */
        k++;
}
```

`pPdGainBoundaries` is the caller's stack array
`uint16_t gainBoundaries[AR5416_PD_GAINS_IN_MASK]` (:2345) — a 1-element
stack OOB read below the frame object; `pPDADCValues` is the driver
static `uint8_t pdadcValues[128]` (:2344) — a 1-byte static OOB read.
The garbage values are then written into PHY registers
(`ar5416SetGainBoundariesClosedLoop` :2220-2237, `ar5416WritePdadcValues`
:2302-2328).

**(b)** When `maxPwrT4[i] == minPwrT4[i]` (possible in the `match` branch
directly from `pwrPdg[i][0]`/`pwrPdg[i][4]`, :2463-2464; the non-match
branch only has a debug-only `HALASSERT`, :2480),
`sizeCurrVpdTable = (max-min)/2 + 1 == 1` (:2541) and the vpdStep read
at :2549 indexes `vpdTableI[i][-1]` and `vpdTableI[i][-2]` (static
.bbs OOB reads).

**(c)** When `maxPwrT4[i] < minPwrT4[i]` (inverted range, e.g. 0 and 255),
`(uint8_t)((0-255)/2 + 1)` truncates to **130**, larger than the
`AR5416_MAX_PWR_RANGE_IN_HALF_DB == 64` row dimension, so
`maxIndex` (:2543) can reach 129 and the copy loop (:2545-2547) reads
`vpdTableI[i][64..129]` — past the row (and for `i == 3` past the whole
2-D object into adjacent statics).

All three fire on every channel change once the EEPROM holds the
degenerate values. Attacker model: malicious/corrupted NIC EEPROM — the
same model as the knowns DF-1308/DF-1309 (do not re-report those; this
is a different function and different arrays).

## Impact

Kernel OOB reads (stack + static) whose values are programmed into radio
registers. No data reaches userspace and no kernel state is corrupted —
functional impact is radio misprogramming; worst case reads adjacent
static memory (case (c)) into PHY registers. Severity Low.

## Verification

- No ath hardware on the audit guest → kernel-side trigger proven by the
  instrumented index-arithmetic replica built and run on the guest
  (run.log / run.2.log): (a),(b),(c) all flagged, control case 0.
- Source trace confirms reachability: EEPROM → `ar5416SetPowerCalTable`
  → `ar5416GetGainBoundariesAndPdadcs` on every `ar5416Reset()` /
  channel change.

## Fix validation

- `fix.diff`: (1) `ar5416SetPowerCalTable` rejects `numXpdGain == 0`
  (`return AH_FALSE` — the caller `ar5416SetTransmitPower` :1098 already
  fails the reset gracefully); (2) clamp `sizeCurrVpdTable` to
  [2, AR5416_MAX_PWR_RANGE_IN_HALF_DB] before `maxIndex` is derived.
- Applies cleanly (fixapply2.log); kernel rebuilt `-Werror` rc=0
  (fix_build.log). The ath driver is not instantiated on the guest
  (compile-level validation, hardware absent — same standard as the
  pass-1 EEPROM findings).

## Kernel references

- sys/dev/netif/ath/ath_hal/ar5416/ar5416_reset.c:2354 (xpdMask from EEPROM)
- sys/dev/netif/ath/ath_hal/ar5416/ar5416_reset.c:2371 (numXpdGain==0 unvalidated)
- sys/dev/netif/ath/ath_hal/ar5416/ar5416_reset.c:2462,2497 (skipped loops)
- sys/dev/netif/ath/ath_hal/ar5416/ar5416_reset.c:2541-2549 (size/vpdStep reads)
- sys/dev/netif/ath/ath_hal/ar5416/ar5416_reset.c:2566-2574 ([-1] backfills)
- sys/dev/netif/ath/ath_hal/ar5416/ar5416_reset.c:2344,2345 (victim arrays)

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