β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-4649

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4649 Β· 15 files
FileTypeDescriptionSize
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).

VERDICT.md
↓ download raw

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.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

fix.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
↓ fix.diffper-fix-DF-4649

Confirmed kernel references

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.