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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3960 Β· 11 files
FileTypeDescriptionSize
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
README.md readme reproduce instructions
↓ download raw

DF-3960 β€” ar5416GetGainBoundariesAndPdadcs degenerate-EEPROM OOB reads

Unvalidated EEPROM calibration edge cases index before the start of kernel arrays in ar5416GetGainBoundariesAndPdadcs() / ar5416SetPowerCalTable():

  • (a) xpdGain mask == 0 β†’ both fill loops are skipped, the backfill loops then read gainBoundaries[-1] (caller's stack array) and pdadcValues[-1] (driver-static array);
  • (b) maxPwrT4[i] == minPwrT4[i] β†’ sizeCurrVpdTable == 1 β†’ the vpdStep computation reads vpdTableI[i][-1] / [-2];
  • (c) inverted pier range β†’ the (uint8_t) truncation yields sizeCurrVpdTable up to 130 > 64 β†’ the copy loop reads past the 64-entry vpdTableI row.

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; clamp sizeCurrVpdTable to [2, AR5416_MAX_PWR_RANGE_IN_HALF_DB] before deriving maxIndex) applies cleanly; kernel builds -Werror rc=0 (fix_build.log).
VERDICT.md verdict full narrative
↓ download raw

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 β†’ ar5416GetGainBoundariesAndPdadcs on every ar5416Reset() / channel change.

Fix validation

  • fix.diff: (1) ar5416SetPowerCalTable rejects numXpdGain == 0 (return AH_FALSE β€” the caller ar5416SetTransmitPower :1098 already fails the reset gracefully); (2) clamp sizeCurrVpdTable to [2, AR5416_MAX_PWR_RANGE_IN_HALF_DB] before maxIndex is derived.
  • Applies cleanly (fixapply2.log); kernel rebuilt -Werror rc=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

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 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
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #1: Thu Sep 17 05:23:00 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

Confirmed kernel references

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.