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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4624 Β· 16 files
FileTypeDescriptionSize
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]; evaluates pdadcValues[-1] β€” a 1-byte OOB read below the caller's static uint8_t pdadcValues[AR5416_NUM_PDADC_VALUES] (ar9285_reset.c:570) β€” and replicates that byte into all 128 PDADC values, which ar5416WritePdadcValues() (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

VERDICT.md
↓ download raw

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)

  1. ar9285SetPowerCalTable (ar9285_reset.c:563) reads xpdMask = pEepData->modalHeader.xpdGain (:580) straight from the v4k EEPROM (uint8_t xpdGain, ah_eeprom_v4k.h:93). Nothing between v4kEepromAttach's checksum check and here rejects xpdGain == 0.
  2. The xpd-gain loop (:593-602) only increments numXpdGain for set bits; xpdGain == 0 leaves numXpdGain == 0. The HALASSERT(0) + break at :595-598 guards the > 2 case only β€” and HALASSERT is a no-op in release builds anyway (ah_internal.h:665-676) β€” the break itself is dead text for the ==0 case.
  3. 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-798 while (k < 128) pPDADCValues[k] = pPDADCValues[k-1]; runs with k == 0 β†’ first iteration reads pPDADCValues[-1] β€” one byte below static uint8_t pdadcValues[128] (:570) β€” and replicates it into all 128 entries.
  4. 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-1 exactly. run.log
  • Mode 3 (uninit): -O0 build; a 128 KB stack spill of 0xA7 produces *pMinCalPower == 0x0053, of 0x12 produces 0x0009 β€” exactly minPwrT4[0]/2 for 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 + sizeCurrVpdTable uint8 wrap at :766 β†’ far-OOB reads. DF-4624's trigger is xpdGain == 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_CHAINS on a 1-chain struct. Not involved here.
  • The ar5413-family DF-4619 pattern (unbounded kk writes) is absent in ar9285: all three write loops are k < AR5416_NUM_PDADC_VALUES - 1 bounded (: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

fixed
baseline reproduced→ patch + rebuild →patched clean

fix.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'
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT #0 x86_64 (harness-level validation of the patched extraction; full kernel-build validation hardware-gated: no AR9285 on the guest)

Confirmed kernel references

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.