ar9285: xpdGain==0 (numXpdGain==0) executes PDADC tail backfill with k==0 β pdadcValues[-1] 1-byte OOB read below the static array replicated into all 128 PHY PDADC values, plus uninitialized minPwrT4[0] stack read
Summary
A v4k EEPROM with xpdGain==0 leaves numXpdGain==0 - the mask loop never runs and the HALASSERT only guards >2 gains, never 0. The function then reads uninitialized stack minPwrT4[0] into pMinCalPower, skips both gain loops with counters at zero, and the tail backfill evaluates pPDADCValues[k-1] with k==0 - a 1-byte OOB read below the static array - replicating that byte into all 128 PDADC entries programmed into the PHY. The ar9285/v4k sibling of ar5413 DF-4620 (int counters, so index -1 rather than the wrapped wild read). VERIFIED on guest (verbatim-extraction harness: marker at [-1] propagates into all 128 entries; SIGSEGV si_addr exactly base-1; uninit-stack flows confirmed at -O0; fix validated). Early-bail-when-zero fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4624 Β· 16 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 3.2 KB | β raw | |
| VERDICT.md | β | 4.3 KB | β raw | |
| ar9285_harness.c | β | 8.7 KB | view raw | |
| build.sh | β | 114 B | view raw | |
| run.sh | β | 90 B | view raw | |
| build_fixed.sh | β | 853 B | view raw | |
| fix.diff | β | 1.0 KB | view raw | |
| build.log | β | 552 B | view raw | |
| run.log | β | 706 B | view raw | |
| run_O0_mode3.log | β | 354 B | view raw | |
| fix_build.log | β | 262 B | view raw | |
| env.txt | β | 188 B | view raw | |
| ar9285_extracted.inc | β | 6.7 KB | β download | |
| ah_extracted.inc | β | 2.5 KB | β download | |
| ar9285_fixed.inc | β | 7.1 KB | β download | |
| verdict.json | β | 5.9 KB | view raw |
DF-4624 β ar9285 numXpdGain==0 β pdadcValues[-1] OOB read + uninit stack read
What
ar9285GetGainBoundariesAndPdadcs() (sys/dev/netif/ath/ath_hal/ar9002/ar9285_reset.c:636-801)
executed with numXpdGains == 0 β which a v4k EEPROM with
modalHeader.xpdGain == 0 produces, because the xpd-gain mask loop at
ar9285_reset.c:593-602 never increments numXpdGain (the HALASSERT+break
at :595-598 only guards > 2 gains, never 0 gains) β
- reads uninitialized stack
minPwrT4[0]at ar9285_reset.c:719 (*pMinCalPower = (int16_t)(minPwrT4[0] / 2)β GCC itself warns on the verbatim extraction), and - runs the PDADC tail backfill at ar9285_reset.c:796-798 with
k == 0:pPDADCValues[k] = pPDADCValues[k-1];evaluatespdadcValues[-1]β a 1-byte OOB read below the caller'sstatic uint8_t pdadcValues[AR5416_NUM_PDADC_VALUES](ar9285_reset.c:570) β and replicates that byte into all 128 PDADC values, whichar5416WritePdadcValues()(ar9285_reset.c:628) programs into the PHY power-detector ADC table.
This is the AR9285/v4k sibling of the AR5413 finding DF-4620 (same family,
same trigger shape xpd_mask == 0, smaller blast radius: the ar9285
counters are int, so the index is -1, not a wrapped 0xFFFFFFFF).
Threat model
Same as the already-filed DF-1768/DF-1769 for this file: the v4k EEPROM
contents (including xpdGain, uint8_t, ah_eeprom_v4k.h:93) come off the
card/dongle verbatim after a non-cryptographic checksum; a malicious or
corrupt AR9285/AR9271 dongle EEPROM with xpdGain == 0 reaches this on the
first channel set (ifconfig ath0 up β ar9285SetTransmitPower β
ar9285SetPowerCalTable β ar9285GetGainBoundariesAndPdadcs). No in-kernel
validation rejects xpdGain == 0.
Build / run (DragonFly guest)
sh build.sh # seds ar9285_reset.c:636-801 + ah.c helpers out of
# /usr/src VERBATIM, compiles with HALASSERT compiled
# out (release kernel shape), -O2
cc -O0 -o ar9285_harness_O0 ar9285_harness.c # for mode 3
sh run.sh # modes 1, 2, 3
sh build_fixed.sh # applies fix.diff to a scratch copy (sys/ untouched),
# rebuilds, validates (mode 4 + mode 2 under fix)
Expected output (baseline)
mode 1: ... DF-4624 REPRODUCED: all 128 PDADC values sourced from
pdadcValues[-1] (ar9285_reset.c:797 with k==0)
mode 2: SIGSEGV si_addr=0x800473fff expected=0x800473fff (base-1)
DF-4624 REPRODUCED: fault at exactly pdadcValues[-1]
mode 3 (O0): poison=0xA7 -> minPower=0x0053 (=0xA7/2)
poison=0x12 -> minPower=0x0009 (=0x12/2)
Fix validation
build_fixed.sh output: minPower=0x7c7c ... pdadc[0]=0x00 ... (non-marker entries: 128) DF-4624 FIX VALIDATED: pdadc zeroed, no [-1] read, minPower sentinel 0x7c7c preserved mode2 under fix: "no fault -- not reproduced"
Files
ar9285_harness.c harness (kernel function compiled verbatim via .inc) build.sh / run.sh exact build/run build_fixed.sh scratch-tree patch + rebuild + validation fix.diff the fix (early bail when numXpdGains == 0) .inc the verbatim extractions (build artifacts, kept) .log, env.txt full untrimmed runs
DF-4624 VERDICT β REPRODUCED (harness-level, verbatim extraction)
Status: reproduced / impact: dos (OOB read + uninit read, hardware-gated trigger) / confidence: certain.
Root cause (traced line by line)
ar9285SetPowerCalTable(ar9285_reset.c:563) readsxpdMask = pEepData->modalHeader.xpdGain(:580) straight from the v4k EEPROM (uint8_t xpdGain, ah_eeprom_v4k.h:93). Nothing betweenv4kEepromAttach's checksum check and here rejectsxpdGain == 0.- The xpd-gain loop (:593-602) only increments
numXpdGainfor set bits;xpdGain == 0leaves numXpdGain == 0. TheHALASSERT(0)+breakat :595-598 guards the > 2 case only β andHALASSERTis a no-op in release builds anyway (ah_internal.h:665-676) β thebreakitself is dead text for the ==0 case. ar9285GetGainBoundariesAndPdadcs(:636) then runs with numXpdGains==0: * :683/:692 table loops never execute βminPwrT4[]/maxPwrT4[]stay uninitialized stack; :719*pMinCalPower = (int16_t)(minPwrT4[0] / 2)reads them. * :722 gain loop never executes βi == 0,k == 0. * :791-794 boundary backfill is in-bounds (gainBoundaries[4], fills 4). * :796-798while (k < 128) pPDADCValues[k] = pPDADCValues[k-1];runs with k == 0 β first iteration reads pPDADCValues[-1] β one byte belowstatic uint8_t pdadcValues[128](:570) β and replicates it into all 128 entries.ar5416WritePdadcValues(:628) then programs those 128 values into the AR9285 PHY PDADC table.
How it was proven (guest, DragonFly 6.5-DEVELOPMENT)
build.sh seds the verbatim function (ar9285_reset.c:636-801) plus its
ah.c helpers out of the guest's /usr/src and compiles them into the
harness with HALASSERT compiled out (release-kernel shape). GCC itself
emits -Wuninitialized for minPwrT4[0] on this extraction (build.log).
- Mode 1 (adjacency): marker byte 0x5A at
pdadcValues[-1]β after the call all 128 PDADC entries are 0x5A (non-marker entries: 0). run.log - Mode 2 (fault): array at a fresh page with a PROT_NONE guard page
below β SIGSEGV with
si_addr == base-1exactly. run.log - Mode 3 (uninit): -O0 build; a 128 KB stack spill of 0xA7 produces
*pMinCalPower == 0x0053, of 0x12 produces0x0009β exactlyminPwrT4[0]/2for each poison; the value flows out to the caller (there:tMinCalPower). run_O0_mode3.log
Why not uid0 / not a bigger impact
The sink is the radio register table (ar5416WritePdadcValues), not
userspace-visible memory; the [-1] byte cannot be read back. The trigger
needs an AR9285/AR9271 device whose EEPROM image is attacker-supplied
(same precondition as the already-filed DF-1768/DF-1769). Ceiling: radio
misprogramming / RF compliance / correctness β classified dos-grade with
Low severity, memcorrupt bucket, mirroring the sibling DF-4620 (Low).
Distinctness from the known findings on this file
- DF-1768 (lines 650-782): unbounded pwrPdg delta β FillVpdTable
overflow +
sizeCurrVpdTableuint8 wrap at :766 β far-OOB reads. DF-4624's trigger isxpdGain == 0(no curve needed at all) and its lines are :719/:796-798, outside DF-1768's cited range. - DF-1769 (lines 607-612): chain loop
AR5416_MAX_CHAINSon a 1-chain struct. Not involved here. - The ar5413-family DF-4619 pattern (unbounded
kkwrites) is absent in ar9285: all three write loops arek < AR5416_NUM_PDADC_VALUES - 1bounded (:760, :770, :781). The DF-4622 flat-curve[size-1]/[size-2]reads exist at :774 but are DF-1768's :766 truncation family (same line family, same missing-validation root cause) β not re-filed.
Fix validation
build_fixed.sh applies fix.diff (early bail when numXpdGains == 0,
zeroing the PDADC table) to a scratch copy β sys/ is never touched β
re-extracts, rebuilds, reruns: pdadc zeroed, marker never read, minPower
sentinel 0x7c7c preserved, guard-page flavor no longer faults
(fix_build.log). fix_status: fixed, baseline reproduced / patched clean.
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 inserts an early bail when numXpdGains==0 (zero the pdadc table and return) right after ar5416GetChannelCenters in ar9285GetGainBoundariesAndPdadcs; on the patched extraction mode 4 shows pdadc zeroed with the 0x5A marker never read, minPower sentinel 0x7c7c preserved (the :719 uninit read never executes), and the guard-page flavor no longer faults.
fix_build.log: 'minPower=0x7c7c gainBoundaries[0]=0 pdadc[0]=0x00 pdadc[127]=0x00 (non-marker entries: 128)' + 'DF-4624 FIX VALIDATED' + mode2-under-fix 'no fault -- not reproduced'
Confirmed kernel references
- sys/dev/netif/ath/ath_hal/ar9002/ar9285_reset.c:570
- sys/dev/netif/ath/ath_hal/ar9002/ar9285_reset.c:580
- sys/dev/netif/ath/ath_hal/ar9002/ar9285_reset.c:593
- sys/dev/netif/ath/ath_hal/ar9002/ar9285_reset.c:607
- sys/dev/netif/ath/ath_hal/ar9002/ar9285_reset.c:612
- sys/dev/netif/ath/ath_hal/ar9002/ar9285_reset.c:628
- sys/dev/netif/ath/ath_hal/ar9002/ar9285_reset.c:636
- sys/dev/netif/ath/ath_hal/ar9002/ar9285_reset.c:683
- sys/dev/netif/ath/ath_hal/ar9002/ar9285_reset.c:719
- sys/dev/netif/ath/ath_hal/ar9002/ar9285_reset.c:722
- sys/dev/netif/ath/ath_hal/ar9002/ar9285_reset.c:791
- sys/dev/netif/ath/ath_hal/ar9002/ar9285_reset.c:796
- sys/dev/netif/ath/ath_hal/ar9002/ar9285_reset.c:797
- sys/dev/netif/ath/ath_hal/ah_eeprom_v4k.h:93
- sys/dev/netif/ath/ath_hal/ah_internal.h:665
Detail
Exploit chain
crafted v4k EEPROM xpdGain==0 -> ar9285SetPowerCalTable :580/:593-602 numXpdGain=0 -> ar9285GetGainBoundariesAndPdadcs :719 uninit minPwrT4[0] read and :796-798 backfill with k==0 -> pdadcValues[-1] 1-byte OOB read (kernel static adjacent data) replicated into all 128 PDADC values -> ar5416WritePdadcValues :628 programs the OOB byte into the AR9285 PHY power table; also gainBoundaries backfilled with defaults (58). Hardware-gated (needs an AR9285/AR9271 device whose EEPROM says xpdGain==0); no uid0 route.
Evidence (decisive lines)
run.log mode 1 'DF-4624 REPRODUCED: all 128 PDADC values sourced from pdadcValues[-1] (ar9285_reset.c:797 with k==0)'; run.log mode 2 'SIGSEGV si_addr=0x800473fff expected=0x800473fff (base-1)'; run_O0_mode3.log 'poison=0xA7: minPower=0x0053 ... poison=0x12: minPower=0x0009'; build.log contains GCC's own '-Wuninitialized minPwrT4[0]' warning on the verbatim extraction; fix_build.log 'DF-4624 FIX VALIDATED: pdadc zeroed, no [-1] read, minPower sentinel 0x7c7c preserved'
PoC changes
no seed PoC; written from scratch as a verbatim-extraction harness (build.sh seds ar9285_reset.c:636-801 and the ah.c helpers out of the guest's /usr/src); HALASSERT compiled out to mirror release kernels; pdadcValues adjacency modeled by page-placing the array with a marker byte / PROT_NONE guard below; uninit-stack proof needs the -O0 build (the -O2 constprop folds minPwrT4[0] to 0 -- GCC's warning is kept as corroboration)
Verified recommended fix
Early-return in ar9285GetGainBoundariesAndPdadcs when numXpdGains==0 (zero the PDADC table); optionally fail ar9285SetPowerCalTable when the EEPROM xpdMask==0 (see fix.diff).
Verdict
REPRODUCED on the DragonFly guest (verbatim-extraction harness): with a v4k EEPROM shape of xpdGain==0 (ar9285_reset.c:580 xpdMask, loop :593-602 leaves numXpdGain==0; the :595 HALASSERT only guards >2 gains, never 0), ar9285GetGainBoundariesAndPdadcs executes the tail backfill with k==0 -- ar9285_reset.c:797 'pPDADCValues[k] = pPDADCValues[k-1]' reads pdadcValues[-1], 1 byte below the caller's static pdadcValues[128] (:570). Proven three ways: (1) adjacency flavor, a 0x5A marker placed at pdadc[-1] propagates into ALL 128 PDADC entries (run.log mode 1, non-marker entries: 0); (2) fault flavor, SIGSEGV si_addr == array base-1 exactly (run.log mode 2); (3) the byte is then programmed to the PHY via ar5416WritePdadcValues (:628). Additionally ar9285_reset.c:719 reads uninitialized stack minPwrT4[0]: GCC warns on the verbatim extraction, and at -O0 a 128KB stack spill of 0xA7 yields *pMinCalPower==0x0053 and 0x12 yields 0x0009 (exactly poison/2, run_O0_mode3.log) -- uninit stack data flows out to the caller. In-kernel the [-1] read hits whatever precedes the .bss/.data static (adjacent kernel data), so a crafted-EEPROM AR9285 dongle programs the PHY pdadc table from an out-of-bounds kernel byte on every channel set; no exfil channel (register-only sink), realistic ceiling is radio misprogramming/correctness, not privilege escalation. Fix validated (early bail when numXpdGains==0 + pdadc zeroing): no [-1] read (marker untouched, pdadc zeroed), no uninit read (minPower sentinel preserved), guard-page flavor faults no longer occur.
No comments yet.