ar2316: numPdGainsUsed==0 (EEPROM xpd_mask==0) -> uint32-wrapped wild reads at array+0x1FFFFFFFE + uninitialized Pmin_t2[0] (twin of DF-4620/ar5413)
Summary
xpd_mask==0 leaves numPdGainsUsed 0 but execution continues past the compiled-out HALASSERT: uninit stack Pmin_t2[0] flows into pMinCalPower, and the backfills [ii-1]/[kk-1] with uint32 counters at zero wrap to 0xFFFFFFFF - wild reads at +8.59 GB whose garbage lands in TPCRG5 and the 32 pdadc PHY writes. Harness REPRODUCES (mapped wild pages -> 0x5a5a propagation, minPower varies across runs proving the uninit read; fix validated). Early-return-when-zero + caller-check fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4649 Β· 15 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 1.1 KB | β raw | |
| VERDICT.md | β | 3.9 KB | β raw | |
| ar2316_harness.c | β | 12.4 KB | view raw | |
| build.sh | β | 191 B | view raw | |
| run.sh | β | 99 B | view raw | |
| build.log | β | 632 B | view raw | |
| run.log | β | 1.4 KB | view raw | |
| run.2.log | β | 1.4 KB | view raw | |
| run.mode2.3.log | β | 674 B | view raw | |
| run.mode2.4.log | β | 674 B | view raw | |
| run.mode2.5.log | β | 674 B | view raw | |
| env.txt | β | 346 B | view raw | |
| fix.diff | β | 1.1 KB | view raw | |
| fix_validate.sh | β | 1.3 KB | view raw | |
| fix_run.log | β | 3.3 KB | view raw |
DF-4649 β ar2316 numPdGainsUsed==0 uint32-wrap wild reads (twin of DF-4620/ar5413)
PoC: verbatim-extraction harness, mode 2. Same build as DF-4648 (see that README; the packs are self-contained copies).
Build (guest, root)
sh build.sh # verbatim-extract ar2316.c:251-499 + cc -O2 -Wno-pointer-sign
Run
./ar2316_harness 2
Expected output
mapped wild pages 0x... 0x..., seeded 0x5A5A mode=2 numPdGainsUsed(ret)=0 minPower=0xd3xx gainBoundaries[0] = 0x5a5a pcdacTable[0] = 0x5a5a minPower out = 0xd3xx (varies per run: uninit stack) DF-4649 REPRODUCED: uint32-underflow wild reads at ar2316.c:487/:491 + uninit stack read at :427
Exit 0 = reproduced. With fix.diff (early return) mode 2 prints
ret=0, gainBoundaries[0]=0x0000, pcdacTable[0]=0xaaaa, minPower=0x7c7c
(sentinel preserved). run.mode2.{3,4,5}.log show the uninit-stack
value varying across runs (0xd2d0/0xd360/0xd5a0).
Threat model
Crafted EEPROM (xpd_mask == 0 β all numVpd == 0) on an AR2316 card;
first channel set after attach executes the wrapped backfill reads at
array +0x1FFFFFFFE (wild KVA read β panic or garbage radio program).
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 whennumVpd > 0); propagates intoahp->ah_txPowerIndexOffset(ar2316.c:581-584). - ar2316.c:486-489 β backfill
pPdGainBoundaries[ii] = pPdGainBoundaries[ii-1];withii == 0:iiisuint32_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];withkk == 0: same uint32 wrap from thepcdacTablebase.
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.
Fix verification
fixedfix.diff returns early when numPdGainsUsed==0 and makes ar2316SetPowerTable fail; patched run shows no wild-read propagation (gainBoundaries[0]=0x0000, pcdacTable[0]=0xaaaa) and minPower sentinel 0x7c7c preserved (the :427 uninit read never executes). Harness-level validation of the patched function bodies; kernel-build validation hardware-gated (no AR2316 on guest).
fix_run.log '--- PATCHED mode 2' section: gainBoundaries[0]=0x0000, pcdacTable[0]=0xaaaa, minPower=0x7c7c
Confirmed kernel references
- sys/dev/netif/ath/ath_hal/ar5212/ar2316.c:388
- sys/dev/netif/ath/ath_hal/ar5212/ar2316.c:427
- sys/dev/netif/ath/ath_hal/ar5212/ar2316.c:486
- sys/dev/netif/ath/ath_hal/ar5212/ar2316.c:487
- sys/dev/netif/ath/ath_hal/ar5212/ar2316.c:490
- sys/dev/netif/ath/ath_hal/ar5212/ar2316.c:491
- sys/dev/netif/ath/ath_hal/ar5212/ar2316.c:535
- sys/dev/netif/ath/ath_hal/ar5212/ar2316.c:538
- sys/dev/netif/ath/ath_hal/ar5212/ar2316.c:597
- sys/dev/netif/ath/ath_hal/ah_eeprom_v3.c:670
Detail
Exploit chain
crafted EEPROM xpd_mask=0 -> numPdGainsUsed==0 at ar2316.c:422 -> :487/:491 uint32 index wrap reads at +0x1FFFFFFFE from kernel-stack gainBoundaries[4] and kernel-heap pcdacTable -> value programmed to AR_PHY_TPCRG5 / replicated across the pdadc table + PHY regs; :427 uninit stack read -> ah_txPowerIndexOffset. Panic-or-garbage; no exfil channel, no uid0 route (hardware-gated).
Evidence (decisive lines)
run.log mode 2: 'gainBoundaries[0] = 0x5a5a', 'pcdacTable[0] = 0x5a5a', 'minPower out = 0xd390', 'DF-4649 REPRODUCED'; run.mode2.{3,4,5}.log: minPower 0xd2d0/0xd360/0xd5a0 (uninit stack, run-varying); fix_run.log PATCHED mode 2: ret=0, gainBoundaries[0]=0x0000, pcdacTable[0]=0xaaaa, minPower=0x7c7c (untouched sentinel).
PoC changes
no seed PoC; mode 2 of the shared verbatim-extraction harness (same build.sh as DF-4648): wild pages mmap'd at the exact wrapped addresses with 0x5A5A to emulate kernel direct-map survival; gainBoundaries passed via a heap struct whose pre-array bytes carry a marker; three extra solo runs captured to prove the :427 read is run-varying uninitialized stack.
Verified recommended fix
Early-return when numPdGainsUsed==0 before the Pmin_t2[0] read and backfill loops; fail ar2316SetPowerTable on 0 gains (see fix.diff).
Verdict
REPRODUCED on the DragonFly guest (verbatim-extraction harness, mode 2): with an EEPROM-shaped zero-gain dataset (xpd_mask=0 -> all numVpd=0, the exact shape ar2413SetupRawDataset produces at ah_eeprom_v3.c:670-671) the function's backfill loops index with uint32 counters at 0: pPdGainBoundaries[ii-1] (ar2316.c:487) and pPDADCValues[kk-1] (:491) wrap to element index 0xFFFFFFFF and read at array+0x1FFFFFFFE bytes (+8.59 GB) -- the direct-map survival case proven by mapping the exact wild pages and observing both 0x5A5A values propagate into gainBoundaries[0] (-> AR_PHY_TPCRG5 by ar2316SetPowerTable:597-602) and pcdacTable[0] (-> 32 PHY reg writes, :588-595); the identical wrap arithmetic is additionally proven to fault in mode 3 (SIGSEGV si_addr == expected wild address). Plus uninitialized-stack read of Pmin_t2[0] at :427 (minPower varies run-to-run: 0xd2d0/0xd360/0xd5a0). In-kernel this is a wild KVA read -> panic (KVA hole) or garbage radio programming; HALASSERT(:538) is compiled out in release and the caller (:535-537) never checks the return. Fix validated (early return + caller check): no wild reads, no uninit read.
No comments yet.