ar5416GetGainBoundariesAndPdadcs OOB reads on degenerate EEPROM calibration data (empty xpdGain mask, zero/inverted pier power ranges)
Summary
Three unvalidated EEPROM-derived edge cases index before the start of kernel arrays: (a) xpdGain mask==0 leaves both fill loops skipped so the backfill loops read gainBoundaries[-1] and pdadcValues[-1]; (b) maxPwrT4==minPwrT4 gives sizeCurrVpdTable==1 so the vpdStep read indexes vpdTableI[i][-1]/[-2]; (c) inverted range truncates to 130>64 so the copy loop walks off the 64-entry row. Values land in PHY registers - no userspace exposure, no kernel-state corruption. VERIFIED: instrumented replica flags OOB reads on all three cases; control case 0. Crafted/corrupted NIC EEPROM - same attacker model as DF-1308/1309; fires on every channel change. Distinct function and arrays from both knowns.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3960 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| trigger.c | trigger-source | instrumented index-arithmetic replica of the three degenerate cases | 5.2 KB | view raw |
| build.sh | build-script | cc -O2 -Wall -o trigger trigger.c | 65 B | view raw |
| run.sh | run-script | ./trigger | 89 B | view raw |
| build.log | build-log | guest compile output | 168 B | view raw |
| run.log | run-log | baseline decisive run | 578 B | view raw |
| run.2.log | run-log | patched-kernel re-run | 578 B | view raw |
| fix.diff | suggested-fix | reject empty xpdGain mask + clamp sizeCurrVpdTable (git-apply-able) | 1.6 KB | view raw |
| fix_build.log | build-log | nativekernel with fix, -Werror, rc=0 | 5.3 KB | view raw |
| env.txt | environment | guest uname, cc version | 213 B | view raw |
| VERDICT.md | verdict | full narrative | 4.4 KB | β raw |
| README.md | readme | reproduce instructions | 1.4 KB | β raw |
DF-3960 β ar5416GetGainBoundariesAndPdadcs degenerate-EEPROM OOB reads
Unvalidated EEPROM calibration edge cases index before the start of
kernel arrays in ar5416GetGainBoundariesAndPdadcs() /
ar5416SetPowerCalTable():
- (a)
xpdGainmask == 0 β both fill loops are skipped, the backfill loops then readgainBoundaries[-1](caller's stack array) andpdadcValues[-1](driver-static array); - (b)
maxPwrT4[i] == minPwrT4[i]βsizeCurrVpdTable == 1β thevpdStepcomputation readsvpdTableI[i][-1]/[-2]; - (c) inverted pier range β the
(uint8_t)truncation yieldssizeCurrVpdTableup to 130 > 64 β the copy loop reads past the 64-entryvpdTableIrow.
Reproduce (on the DragonFly guest)
./build.sh
./run.sh # instrumented replica reports every OOB index; control
# case (sane EEPROM values) reports zero
Verified
- Index arithmetic replicated and run on the audit guest (baseline and patched kernels): cases (a), (b), (c) all flag OOB; control case 0.
- No ath(4) hardware exists on the audit guest, so the kernel-side trigger (crafted NIC EEPROM) is proven by the exact-arithmetic replica plus the source trace (same standard as DF-1308/DF-1309).
fix.diff(reject empty xpdGain mask; clampsizeCurrVpdTableto [2, AR5416_MAX_PWR_RANGE_IN_HALF_DB] before derivingmaxIndex) applies cleanly; kernel builds-Werrorrc=0 (fix_build.log).
DF-3960 β VERDICT
REPRODUCED at the function level (impact: none β OOB reads feed PHY
registers only), confidence: certain.
Root cause β three unvalidated EEPROM-derived edge cases
ar5416SetPowerCalTable() (sys/dev/netif/ath/ath_hal/ar5416/ar5416_reset.c:2337-2405)
takes xpdMask straight from EEPROM
(pEepData->modalHeader[].xpdGain, :2354) and computes
numXpdGain = ar5416GetXpdGainValues(ah, xpdMask, ...) (:2371). For an
empty mask the function returns 0 with no validation (:2247-2265),
and ar5416GetGainBoundariesAndPdadcs(..., numXpdGains = 0) is called
(:2387-2391). Inside (:2413-2576):
(a) With numXpdGains == 0 both for (i = 0; i < numXpdGains; i++)
loops (:2462, :2497) leave i == 0 and k == 0, so:
while (i < AR5416_PD_GAINS_IN_MASK) { /* :2566 */
pPdGainBoundaries[i] = pPdGainBoundaries[i-1]; /* reads [-1] */
i++;
}
while (k < AR5416_NUM_PDADC_VALUES) { /* :2571 */
pPDADCValues[k] = pPDADCValues[k-1]; /* reads [-1] */
k++;
}
pPdGainBoundaries is the caller's stack array
uint16_t gainBoundaries[AR5416_PD_GAINS_IN_MASK] (:2345) β a 1-element
stack OOB read below the frame object; pPDADCValues is the driver
static uint8_t pdadcValues[128] (:2344) β a 1-byte static OOB read.
The garbage values are then written into PHY registers
(ar5416SetGainBoundariesClosedLoop :2220-2237, ar5416WritePdadcValues
:2302-2328).
(b) When maxPwrT4[i] == minPwrT4[i] (possible in the match branch
directly from pwrPdg[i][0]/pwrPdg[i][4], :2463-2464; the non-match
branch only has a debug-only HALASSERT, :2480),
sizeCurrVpdTable = (max-min)/2 + 1 == 1 (:2541) and the vpdStep read
at :2549 indexes vpdTableI[i][-1] and vpdTableI[i][-2] (static
.bbs OOB reads).
(c) When maxPwrT4[i] < minPwrT4[i] (inverted range, e.g. 0 and 255),
(uint8_t)((0-255)/2 + 1) truncates to 130, larger than the
AR5416_MAX_PWR_RANGE_IN_HALF_DB == 64 row dimension, so
maxIndex (:2543) can reach 129 and the copy loop (:2545-2547) reads
vpdTableI[i][64..129] β past the row (and for i == 3 past the whole
2-D object into adjacent statics).
All three fire on every channel change once the EEPROM holds the degenerate values. Attacker model: malicious/corrupted NIC EEPROM β the same model as the knowns DF-1308/DF-1309 (do not re-report those; this is a different function and different arrays).
Impact
Kernel OOB reads (stack + static) whose values are programmed into radio registers. No data reaches userspace and no kernel state is corrupted β functional impact is radio misprogramming; worst case reads adjacent static memory (case (c)) into PHY registers. Severity Low.
Verification
- No ath hardware on the audit guest β kernel-side trigger proven by the instrumented index-arithmetic replica built and run on the guest (run.log / run.2.log): (a),(b),(c) all flagged, control case 0.
- Source trace confirms reachability: EEPROM β
ar5416SetPowerCalTableβar5416GetGainBoundariesAndPdadcson everyar5416Reset()/ channel change.
Fix validation
fix.diff: (1)ar5416SetPowerCalTablerejectsnumXpdGain == 0(return AH_FALSEβ the callerar5416SetTransmitPower:1098 already fails the reset gracefully); (2) clampsizeCurrVpdTableto [2, AR5416_MAX_PWR_RANGE_IN_HALF_DB] beforemaxIndexis derived.- Applies cleanly (fixapply2.log); kernel rebuilt
-Werrorrc=0 (fix_build.log). The ath driver is not instantiated on the guest (compile-level validation, hardware absent β same standard as the pass-1 EEPROM findings).
Kernel references
- sys/dev/netif/ath/ath_hal/ar5416/ar5416_reset.c:2354 (xpdMask from EEPROM)
- sys/dev/netif/ath/ath_hal/ar5416/ar5416_reset.c:2371 (numXpdGain==0 unvalidated)
- sys/dev/netif/ath/ath_hal/ar5416/ar5416_reset.c:2462,2497 (skipped loops)
- sys/dev/netif/ath/ath_hal/ar5416/ar5416_reset.c:2541-2549 (size/vpdStep reads)
- sys/dev/netif/ath/ath_hal/ar5416/ar5416_reset.c:2566-2574 ([-1] backfills)
- sys/dev/netif/ath/ath_hal/ar5416/ar5416_reset.c:2344,2345 (victim arrays)
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 applies cleanly to /usr/src; kernel rebuilt with -Werror rc=0 (fix_build.log); ath hardware absent on the guest so fix validation is compile-level plus harness-level (the guards make all three OOB index computations unreachable); patched kernel booted for the vtnet findings in the same batched build.
fixapply2.log (patch -p1 ok, ar5416_reset.c hunks at 2369/2550); fix_build.log (nativekernel rc=0, -Werror); run.2.log on patched kernel
Confirmed kernel references
- sys/dev/netif/ath/ath_hal/ar5416/ar5416_reset.c:2354
- sys/dev/netif/ath/ath_hal/ar5416/ar5416_reset.c:2371
- sys/dev/netif/ath/ath_hal/ar5416/ar5416_reset.c:2541
- sys/dev/netif/ath/ath_hal/ar5416/ar5416_reset.c:2549
- sys/dev/netif/ath/ath_hal/ar5416/ar5416_reset.c:2566
- sys/dev/netif/ath/ath_hal/ar5416/ar5416_reset.c:2571
Detail
Evidence (decisive lines)
run.log/run.2.log: '(a) ... YES', '(b) ... YES', '(c) ... YES', 'control ... 0 (expect 0)'; fixapply2.log; fix_build.log rc=0; VERDICT.md full trace
PoC changes
n/a (first-generation pack)
Verified recommended fix
In ar5416SetPowerCalTable, fail calibration when numXpdGain == 0; in ar5416GetGainBoundariesAndPdadcs, clamp sizeCurrVpdTable to [2, AR5416_MAX_PWR_RANGE_IN_HALF_DB] before deriving maxIndex.
Verdict
REPRODUCED at the function level on the audit guest: three unvalidated EEPROM-derived edge cases in ar5416SetPowerCalTable/ar5416GetGainBoundariesAndPdadcs produce OOB reads β (a) xpdGain mask 0 makes numXpdGain 0, so the backfill loops read gainBoundaries[-1] (caller stack array, ar5416_reset.c:2567) and pdadcValues[-1] (driver static, :2572); (b) zero pier range makes sizeCurrVpdTable 1 so the vpdStep read indexes vpdTableI[i][-1]/[-2] (:2549); (c) inverted range truncates sizeCurrVpdTable to up to 130 > 64 so the copy loop walks off the vpd row (:2545-2547). Values land in PHY registers only β no userspace exposure, no kernel-state corruption (impact none, severity Low). Index arithmetic proven by instrumented replica on the guest (all three flagged, control case 0); no ath hardware exists on the guest, so the kernel-side trigger is the replica + line trace, same standard as DF-1308/DF-1309. Fix (reject empty xpd mask + clamp size to [2,64]) applies cleanly and builds -Werror rc=0.
No comments yet.