# DF-4649 — ar2316: numPdGainsUsed==0 → uint32 index-wrap wild reads + uninitialized stack (twin of DF-4620/ar5413)

## Verdict: REPRODUCED (harness, verbatim kernel code) — impact: dos (hardware-gated)

## Root cause

`sys/dev/netif/ath/ath_hal/ar5212/ar2316.c` —
`ar2316getGainBoundariesAndPdadcsForPowers()`:

When the EEPROM raw cal dataset carries no pd-gain data for the channel
(`xpd_mask == 0` → `ar2413SetupRawDataset` sets every
`pDataPerPDGain[j].numVpd = 0`, ah_eeprom_v3.c:670-671), the gain-selection
loop (ar2316.c:388-425) never increments `numPdGainsUsed`. `HALASSERT(1 <=
numPdGainsUsed && numPdGainsUsed <= 3)` at ar2316.c:538 is compiled out in
release builds (`AH_ASSERT` undefined, ah_internal.h:665-676), and the
caller (ar2316.c:535-537) never checks the return value, so execution
falls through to:

- ar2316.c:427 — `*pMinCalPower = Pmin_t2[0];` reads **uninitialized
  stack** (`Pmin_t2[]` only written when `numVpd > 0`); propagates into
  `ahp->ah_txPowerIndexOffset` (ar2316.c:581-584).
- ar2316.c:486-489 — backfill `pPdGainBoundaries[ii] =
  pPdGainBoundaries[ii-1];` with `ii == 0`: `ii` is `uint32_t` →
  element index 0xFFFFFFFF → **read at array + 0x1FFFFFFFE bytes
  (+8.59 GB)** — a wild KVA read, not a 2-byte under-read.
- ar2316.c:490-493 — backfill `pPDADCValues[kk] = pPDADCValues[kk-1];`
  with `kk == 0`: same uint32 wrap from the `pcdacTable` base.

The garbage boundary then goes to `AR_PHY_TPCRG5` (ar2316.c:597-602) and
the whole pdadc table (fed by the wild read) to 32 PHY registers
(ar2316.c:588-595). In the kernel the +8.6 GB read lands either in the
direct map (garbage survives and is programmed to the radio) or in a KVA
hole → panic.

## Trigger (threat model)

Crafted EEPROM on an AR2316-bearing card: `xpd_mask == 0` for the 11b/11g
raw dataset (exactly the shape `ar2413SetupRawDataset` produces). First
channel set after attach reaches the broken path.

## How it was verified

Verbatim-extraction harness (`ar2316_harness.c` mode 2) — same method as
DF-4648 (function bodies ar2316.c:251-499 compiled verbatim against the
faithful struct). The two wild addresses are emulated as kernel
direct-map survivors: the exact pages at `gb + 0x1FFFFFFFE` and
`pcdacTable + 0x1FFFFFFFE` are mmap'd (MAP_FIXED) and seeded with
0x5A5A; the fault flavor (unmapped wild page) is covered by mode 3's
SIGSEGV proof for the identical wrap arithmetic.

`run.log` (decisive lines):

```
mode=2 numPdGainsUsed(ret)=0 minPower=0xd390
gainBoundaries[0] = 0x5a5a   (wild read at gb+0x1FFFFFFFE, ar2316.c:487)
pcdacTable[0] = 0x5a5a       (wild read at pcdacTable+0x1FFFFFFFE, ar2316.c:491)
minPower out = 0xd390        (uninitialized Pmin_t2[0] stack read, ar2316.c:427)
DF-4649 REPRODUCED: uint32-underflow wild reads at ar2316.c:487/:491 + uninit stack read at :427
```

The uninitialized-stack read varies run-to-run (0xd2d0 / 0xd360 / 0xd5a0
across `run.mode2.3.log`, `run.mode2.4.log`, `run.mode2.5.log`) — proof
it is live stack garbage, not dataset data.

## Exploitation ceiling

Hardware-gated (crafted EEPROM). Wild KVA read → panic (KVA hole) or
garbage radio programming; the uninit stack value flows only into a tx
power offset. No exfiltration channel, no uid0 route.

## Fix validation

`fix.diff` (mirrors the accepted DF-4620 fix): early `return 0` when
`numPdGainsUsed == 0` before the `Pmin_t2[0]` read and the backfill
loops, plus `if (numPdGainsUsed == 0) return AH_FALSE;` in
`ar2316SetPowerTable`. `fix_run.log` (PATCHED mode 2): ret=0,
gainBoundaries[0]=0x0000, pcdacTable[0]=0xaaaa (canary intact),
minPower=0x7c7c (sentinel preserved — the :427 uninit read never
executes).

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