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

ar5413: numPdGainsUsed==0 path β€” uint32 counter underflows make pPdGainBoundaries[ii-1] / pPDADCValues[kk-1] read at array+0x1FFFFFFFE (+8.6 GB wild reads), plus uninitialized Pmin_t2[0] stack read

Summary

xpd_mask==0 in the raw dataset leaves numPdGainsUsed==0; the boundary/pdadc backfills then execute [ii-1]/[kk-1] with uint32 counters at zero - the index wraps to 0xFFFFFFFF giving element offset +0x1FFFFFFFE (~8.59 GB) wild reads from the caller's stack and the state struct, values programmed into PHY registers; :468 additionally reads uninitialized stack Pmin_t2[0] feeding txPowerIndexOffset. The HALASSERT is a no-op in release. Harness fault-flavor pinned the read (si_addr == base+0x1FFFFFFFE exactly); delivered run shows both values propagate. Early-bail + caller-check fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4620 Β· 12 files
FileTypeDescriptionSize
ar5413_harness.c β€” 11.9 KB view raw
build.sh β€” 66 B view raw
run.sh β€” 99 B view raw
build.log β€” 707 B view raw
run.log β€” 2.4 KB view raw
env.txt β€” 271 B view raw
VERDICT.md β€” 3.0 KB ↓ raw
README.md β€” 2.7 KB ↓ raw
fix.diff β€” 1.0 KB view raw
fix_build.log β€” 140 B view raw
fix_run.log β€” 1.3 KB view raw
verdict.json β€” 3.9 KB view raw

DF-4620 β€” ar5413 zero-gain path: uint32 index underflows β†’ +8.6 GB wild reads + uninitialized stack read

What

When the EEPROM provides no pd-gain data for the calibration channel (xpd_mask == 0 β†’ ar2413SetupRawDataset sets every numVpd = 0, ah_eeprom_v3.c:656-673), ar5413getGainBoundariesAndPdadcsForPowers() runs with numPdGainsUsed == 0 and:

  • ar5413.c:528 pPdGainBoundaries[ii] = pPdGainBoundaries[ii-1]; with ii == 0. ii is uint32_t β†’ ii-1 = 0xFFFFFFFF β†’ the array subscript reads at gainBoundaries + 0x1FFFFFFFE bytes (+8.59 GB) β€” not a 2-byte under-read. The value then fills gainBoundaries[0..3] and is programmed into AR_PHY_TPCRG5 by ar5413SetPowerTable (ar5413.c:639-644).
  • ar5413.c:532 pPDADCValues[kk] = pPDADCValues[kk-1]; with kk == 0 (same uint32 wrap from the pcdacTable base) β€” the wild value is replicated across the whole 128-entry pdadc table and written to 32 PHY registers (ar5413.c:630-637).
  • ar5413.c:468 *pMinCalPower = Pmin_t2[0]; reads uninitialized stack (Pmin_t2[] is only written inside the numVpd > 0 branch, :435-445); the garbage propagates into ah_txPowerIndexOffset (:623-626).

The HALASSERT(1 <= numPdGainsUsed && ...) at :580 is compiled out in release builds (AH_ASSERT undefined; HALASSERT is a no-op, ah_internal.h:665-676; option exists at sys/conf/options:549 but is not in GENERIC), so nothing stops this path.

In the kernel the +8.6 GB reads land in the direct map: either mapped RAM (garbage value survives and is programmed to hardware) or a KVA hole (kernel panic).

Reproduce

cd findings/poc/DF-4620
./build.sh && ./ar5413_harness 2    # maps the exact wild pages (kernel-survival emulation)
./ar5413_harness 2                  # again: uninit minPower differs run to run

Expected decisive lines (run.log):

gainBoundaries[0] = 0x5a5a (wild read at gb+0x1FFFFFFFE, ar5413.c:528 ...)
pcdacTable[0] = 0x5a5a (wild read at pcdacTable+0x1FFFFFFFE, ar5413.c:532 ...)
minPower out = 0xd320 (uninitialized Pmin_t2[0] stack read at ar5413.c:468 ...)
DF-4620 REPRODUCED: uint32-underflow wild reads at ar5413.c:528/:532 + uninit stack read at :468

(An earlier unmapped variant faulted at exactly gb + 0x1FFFFFFFE (si_addr 0xa004902c2 == 0x8004902c4 + 0x1FFFFFFFE) β€” same primitive, fault flavor.)

Fix validation

fix.diff returns early when numPdGainsUsed == 0 (and makes ar5413SetPowerTable fail). Patched run: gainBoundaries[0]=0x0000, pcdacTable[0]=0xaaaa (untouched), minPower=0x7c7c (sentinel intact β€” the uninit read never happens). See fix_run.log.

Threat model

Crafted EEPROM (xpd_mask = 0) on AR5413 hardware β†’ wild KVA reads and/or panic on every channel set. No unprivileged-host path.

VERDICT.md
↓ download raw

DF-4620 VERDICT

Finding

numPdGainsUsed == 0 path in ar5413getGainBoundariesAndPdadcsForPowers (ar5413.c:397-540). Because the backfill loops decrement uint32 counters past zero, two array subscripts wrap to element index 0xFFFFFFFF and read at array + 0x1FFFFFFFE bytes (+8.59 GB):

  • :528 pPdGainBoundaries[ii-1] (ii==0) β€” from the caller's stack array gainBoundaries[4] (ar5413.c:555, kernel stack of ar5413SetPowerTable); the value is later written to AR_PHY_TPCRG5 (:639-644).
  • :532 pPDADCValues[kk-1] (kk==0) β€” from priv->pcdacTable (kernel heap); replicated across all 128 pdadc entries and 32 PHY register writes (:630-637).

Plus :468 *pMinCalPower = Pmin_t2[0] β€” uninitialized kernel-stack read (Pmin_t2[] only written when numVpd>0, :435-445) feeding ahp->ah_txPowerIndexOffset (:623-626).

Release kernels reach this: HALASSERT(1 <= numPdGainsUsed && numPdGainsUsed <= 3) (:580) is a no-op without AH_ASSERT (ah_internal.h:665-676; not in GENERIC). Trigger: EEPROM with xpd_mask==0 for the mode's raw dataset (produced exactly by ar2413SetupRawDataset, ah_eeprom_v3.c:643-671 setting numVpd=0 for all gains) β€” i.e., crafted EEPROM, or a degenerately-programmed card.

Reproduction (guest)

Mode 2 of the same verbatim-extraction harness (see DF-4619 pack). The guest userspace fault flavor was first observed as SIGSEGV/SIGBUS at si_addr == gb + 0x1FFFFFFFE (0x8004902c4 + 0x1FFFFFFFE = 0xa004902c2, exact). To emulate the kernel's direct map (where the +8.6 GB read usually lands on mapped RAM instead of faulting), the harness mmaps the two exact wild pages seeded with 0x5A5A β€” the call then COMPLETES and both wild values are observed propagating into gainBoundaries[0] and pcdacTable[0]:

gainBoundaries[0] = 0x5a5a   (ar5413.c:528 β†’ TPCRG5)
pcdacTable[0]     = 0x5a5a   (ar5413.c:532 β†’ pdadc table / PHY regs)
minPower out      = 0xd320 / 0xd370 / 0xd300 across runs  (uninit stack, :468)

Impact

In-kernel: wild read at KVA +8.6 GB β€” mapped RAM β†’ garbage programmed into radio registers (malfunction); KVA hole β†’ page fault β†’ panic (DoS). The uninit stack read leaks nothing to userspace (values only feed PHY programming). Gated on attacker-supplied EEPROM β†’ physical/supply-chain precondition, same class as DF-1766. Recorded impact: dos.

Fix validation

fix.diff: early return 0 when numPdGainsUsed==0 (after the gain loop, before :468) + ar5413SetPowerTable returns AH_FALSE on 0 gains. Patched harness run: no wild-read propagation (gainBoundaries[0]=0x0000, pcdacTable[0]=0xaaaa untouched) and minPower stays at its 0x7c7c sentinel β€” the uninitialized read never executes. See fix_run.log.

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 ar5413SetPowerTable fail; patched run shows no wild-read propagation (0x0000/0xaaaa) and minPower sentinel 0x7c7c preserved (the :468 uninit read never executes).

fix_run.log '--- mode 2' section: gainBoundaries[0]=0x0000, pcdacTable[0]=0xaaaa, minPower=0x7c7c
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT #0 x86_64 (harness-level validation of patched function bodies; kernel-build validation hardware-gated)

Confirmed kernel references

Detail

Exploit chain

crafted EEPROM xpd_mask=0 -> numPdGainsUsed==0 at ar5413.c:463 -> :528/:532 uint32 index wrap reads at +0x1FFFFFFFE from kernel-stack array and kernel-heap pcdacTable -> value programmed to AR_PHY_TPCRG5 / replicated to pdadc table + PHY regs; :468 uninit stack read -> ah_txPowerIndexOffset. Panic-or-garbage; no exfil channel, no uid0 route (hardware-gated).

Evidence (decisive lines)

run.log mode 2 lines: 'gainBoundaries[0] = 0x5a5a', 'pcdacTable[0] = 0x5a5a', 'minPower out = 0xd320', 'DF-4620 REPRODUCED: uint32-underflow wild reads at ar5413.c:528/:532 + uninit stack read at :468'; fix_run.log: patched mode 2 shows 0x0000/0xaaaa/0x7c7c (untouched).

PoC changes

no seed PoC; mode 2 of the shared verbatim-extraction harness: wild pages mmap'd at the exact wrapped addresses with 0x5A5A to emulate kernel direct-map survival (fault flavor also captured: si_addr == gb+0x1FFFFFFFE exact); gainBoundaries passed via a heap struct whose pre-array bytes carry a marker

Verified recommended fix

Early-return when numPdGainsUsed==0 before the Pmin_t2[0] read and backfill loops; fail ar5413SetPowerTable 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) the function's backfill loops index with uint32 counters at 0: pPdGainBoundaries[ii-1] (ar5413.c:528) and pPDADCValues[kk-1] (:532) wrap to element index 0xFFFFFFFF and read at array+0x1FFFFFFFE bytes (+8.59 GB) β€” proven by SIGSEGV si_addr exactly equal to gb+0x1FFFFFFFE (0xa004902c2) in the fault flavor, and by mapping the exact wild pages (kernel direct-map emulation) and observing both 0x5A5A values propagate into gainBoundaries[0] (-> AR_PHY_TPCRG5) and pcdacTable[0] (-> 32 PHY reg writes). Plus uninitialized-stack read of Pmin_t2[0] at :468 (minPower varies run-to-run: 0xd320/0xd370/0xd300). In-kernel this is a wild KVA read -> panic (KVA hole) or garbage radio programming; HALASSERT(:580) is compiled out in release. Fix validated (early return): no wild reads, no uninit read.