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

ar2413: numPdGainsUsed==0 (EEPROM xpd_mask==0) β†’ uint32-wrapped wild reads at array+0x1FFFFFFFE (+8.59 GB) + uninitialized Pmin_t2[0] (twin of DF-4620/ar5413, DF-4649/ar2316)

Summary

xpd_mask==0 leaves numPdGainsUsed 0 but execution continues past the compiled-out HALASSERT: uninit stack Pmin_t2[0] feeds pMinCalPower, and the backfills [ii-1]/[kk-1] with uint32 counters at zero wrap to 0xFFFFFFFF - wild reads at +8.59 GB whose garbage propagates into TPCRG5 and the 32 pdadc PHY writes. Harness REPRODUCES (mapped wild pages -> propagation observed; minPower varies run-to-run proving the uninit read). Early-return-when-zero + caller-check fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4661 Β· 13 files
FileTypeDescriptionSize
README.md β€” 2.2 KB ↓ raw
VERDICT.md β€” 3.1 KB ↓ raw
ar2413_harness.c β€” 12.4 KB view raw
build.sh β€” 85 B view raw
run.sh β€” 99 B view raw
fix_validate.sh β€” 1.3 KB view raw
fix.diff β€” 1.0 KB view raw
build.log β€” 632 B view raw
run.log β€” 2.0 KB view raw
fix_run.log β€” 3.1 KB view raw
env.txt β€” 946 B view raw
manifest.json β€” 922 B view raw
verdict.json β€” 3.9 KB view raw

DF-4661 β€” ar2413: numPdGainsUsed==0 β†’ uint32-wrapped wild reads + uninit Pmin_t2[0]

Twin of DF-4620 (ar5413) / DF-4649 (ar2316). If the EEPROM supplies no pd gain data for a channel (xpd_mask==0 β‡’ every numVpd==0 β€” the exact shape ar2413SetupRawDataset produces, ah_eeprom_v3.c:656-673), then in ar2413getGainBoundariesAndPdadcsForPowers():

  • numPdGainsUsed stays 0; HALASSERT(1 <= numPdGainsUsed && ...) (ar2413.c:536) is compiled out in release (AH_ASSERT undefined, ah_internal.h:665-676) and the caller never checks the return;
  • ar2413.c:424 *pMinCalPower = Pmin_t2[0] reads uninitialized stack (Pmin_t2[] is only written when numVpd>0);
  • ar2413.c:484 pPdGainBoundaries[ii-1] with ii==0 β€” ii is uint32_t β‡’ element index 0xFFFFFFFF β‡’ read at gb + 0x1FFFFFFFE bytes (+8.59 GB), a WILD read, not a 2-byte under-read;
  • ar2413.c:488 pPDADCValues[kk-1] with kk==0 β€” same wrap from the pcdacTable base.

In-kernel the wild read lands in the direct map (garbage propagates into AR_PHY_TPCRG5 via ar2413SetPowerTable:595-600 and into the 32 PHY register writes at ar2413.c:586-593) or in a KVA hole (panic).

Build

On the DragonFly guest: cd /root/df2413 && sh build.sh (see DF-4660 pack / shared harness; verbatim kernel bodies + faithful struct).

Run

./ar2413_harness 2        # mode 2 = this finding

Expected

gainBoundaries[0] = 0x5a5a   (wild read at gb+0x1FFFFFFFE propagated)
pcdacTable[0]     = 0x5a5a   (wild read at pcdacTable+0x1FFFFFFFE propagated)
minPower out      = 0xd…0    (uninitialized stack; varies run to run:
                              observed 0xd2e0/0xd3b0/0xd620/0xd370)
DF-4661 REPRODUCED: uint32-underflow wild reads at ar2413.c:484/:488
                    + uninit stack read at :424

The harness emulates the kernel direct map (mmapping the exact wild pages +8.59 GB above the arrays and seeding 0x5A5A) so the call completes and the propagation is observable; the identical wrap arithmetic is proven to fault in mode 3 (SIGSEGV si_addr == expected wild address).

Fix validation

sh fix_validate.sh (fixA+B+C): patched mode 2 returns numPdGainsUsed=0 early, minPower keeps its 0x7c7c sentinel (no uninit read), no 0x5A5A propagation.

VERDICT.md
↓ download raw

DF-4661 β€” VERDICT

REPRODUCED (impact: dos β€” wild KVA reads + uninitialized stack read; hardware-gated trigger)

Method

Same verbatim-extraction harness as DF-4660 (real ar2413.c:245-496 function bodies from the guest's pristine /usr/src, faithful struct ar2413State).

Mode 2 crafts the EEPROM-shaped zero-gain dataset: xpd_mask=0 β‡’ every numVpd=0 β€” exactly what ar2413SetupRawDataset produces for such an EEPROM (ah_eeprom_v3.c:656-673) β‡’ numPdGainsUsed==0.

Result (run.log; variance across runs in the "repeat" section)

  1. Wild reads at +0x1FFFFFFFE bytes. To emulate the kernel direct map (where a +8.59 GB offset still hits mapped RAM), the harness mmaps the exact wild pages and seeds 0x5A5A: * pPdGainBoundaries[ii-1] at ar2413.c:484 with ii==0 (uint32_t) β‡’ element 0xFFFFFFFF β‡’ read at gb+0x1FFFFFFFE β€” value 0x5A5A observed propagating into gainBoundaries[0], which ar2413SetPowerTable writes to AR_PHY_TPCRG5 (ar2413.c:595-600); * pPDADCValues[kk-1] at ar2413.c:488 with kk==0 β‡’ read at pcdacTable+0x1FFFFFFFE β€” 0x5A5A propagates into pcdacTable[0] and is replicated across the whole pdadc table β†’ 32 PHY register writes (ar2413.c:586-593). The identical uint32-wrap arithmetic is separately proven to fault in mode 3 (SIGSEGV si_addr == expected wild address, DF-4662 pack): in the kernel the same wrap lands in the direct map (garbage radio programming) or a KVA hole (panic on every channel change).
  2. Uninitialized stack read. *pMinCalPower = Pmin_t2[0] (ar2413.c:424) with Pmin_t2[] never written: minPower varies run-to-run (0xd2e0 / 0xd3b0 / 0xd620 / 0xd370) β€” flows into ah_txPowerIndexOffset (ar2413.c:579-582).
  3. The HALASSERT(1 <= numPdGainsUsed && numPdGainsUsed <= 3) at ar2413.c:536 is a no-op in release builds (AH_ASSERT undefined, ah_internal.h:665-676), and the caller (ar2413.c:533-535) ignores the count's zero-ness; switch (numPdGainsUsed) then matches no case (SM(numPdGainsUsed-1, …) writes a wrapped -1 to the register mask).

Exploitation ceiling

Crafted EEPROM on AR2413 hardware (physical/supply-chain). Wild kernel read β†’ panic (KVA hole) or garbage radio programming; uninit stack byte-pair feeds a hardware offset. No exfil to userspace, no uid0 route.

Fix validation (fix_run.log)

fix.diff: early return 0 when numPdGainsUsed==0 before the Pmin_t2[0] read (ar2413.c:424) + caller-side if (numPdGainsUsed == 0) return AH_FALSE; in ar2413SetPowerTable. Patched harness mode 2: ret=0, minPower keeps the 0x7c7c sentinel (no uninit read), no 0x5A5A propagation. git-apply verified.

NOT re-reported

DF-1796 (numChannels==0, pass 1) and DF-4662 (flat-curve wrap β€” same uint32-underflow class, different trigger/guard) are distinct findings.

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

Validated on the guest: fixA+B+C applied to a copy of the pristine ar2413.c, function bodies re-extracted verbatim from the patched copy, harness rebuilt and re-run. Baseline reproduces the defect; patched build shows the defect GONE (mode 1: no overflow / mode 2: early return, sentinel minPower, no wild-read propagation / mode 3: completes without the wild-address SIGSEGV). fix.diff is git-apply-able against the pristine tree (git apply --check clean) and was never applied to sys/.

fix_run.log (full patched 3-mode run) vs run.log (baseline); fix.diff in pack; PATCHES_APPLIED + FIXED_BUILD_OK in fix_run.log.
↓ fix.diffper-fix-DF-4661

Confirmed kernel references

Detail

Exploit chain

crafted v3 EEPROM (xpd_mask=0) on AR2413 card -> numPdGainsUsed==0 -> uint32-wrapped wild reads at gb+0x1FFFFFFFE and pcdacTable+0x1FFFFFFFE (direct-map garbage or KVA-hole panic) + uninit Pmin_t2[0] stack read -> garbage TPCRG5/pdadc programming or kernel panic. No uid0 route.

Evidence (decisive lines)

run.log mode 2: 'gainBoundaries[0] = 0x5a5a', 'pcdacTable[0] = 0x5a5a', 'DF-4661 REPRODUCED'; repeat section: minPower 0xd3b0/0xd620/0xd370 (uninit variance); fix_run.log patched mode 2: ret=0, sentinel minPower 0x7c7c, no propagation.

PoC changes

Same harness as DF-4660 (mode 2); no kernel-code changes.

Verified recommended fix

Early-return 0 when numPdGainsUsed==0 before the Pmin_t2[0] read, plus a caller-side numPdGainsUsed==0 -> AH_FALSE check in ar2413SetPowerTable (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:656-673) the function's backfill loops index with uint32 counters at 0: pPdGainBoundaries[ii-1] (ar2413.c:484) and pPDADCValues[kk-1] (ar2413.c:488) 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 ar2413SetPowerTable:595-600) and pcdacTable[0] (-> 32 PHY reg writes, ar2413.c:586-593); the identical wrap arithmetic proven to fault in mode 3 (SIGSEGV si_addr == expected wild address). Plus uninitialized-stack read of Pmin_t2[0] at ar2413.c:424 (minPower varies run-to-run: 0xd2e0/0xd3b0/0xd620/0xd370). In-kernel this is a wild KVA read -> panic (KVA hole) or garbage radio programming; HALASSERT(:536) is compiled out in release and the caller (:533-535) never checks the return. Twin of DF-4620 (ar5413) / DF-4649 (ar2316). Fix validated (early return + caller check): no wild reads, no uninit read.