⬢ DragonFlyBSD Kernel Audit
← triage · dashboard
DF-4686

ar2425: FillVpdTable/interpolate span never clamped — descending curve → (uint16_t)(Pmax−Pmin) wrap → ~131 KB sequential OOB write past the function-static VpdTable_* block in kernel static storage; valid curve spanning >63 half-dB → OOB write with no wrap (ar2425 twin of DF-4683, flagged at ar2425.c:311)

Summary

Neither loop clamps the span to the 64-wide row or rejects Pmax<Pmin. ar2425 twist: the scratch is FUNCTION-SCOPE STATICS (.bss) - the descending-curve wrap marches ~128KB through the whole statics block into adjacent kernel statics; a valid ascending curve >63 half-dB spills rows and escapes with no wrap. REACHABILITY refinement: the v3/v5 EEPROM decoder uses unsigned 4-bit cumulative deltas, so legal pier curves are non-descending with span<=60 - latent today (decoder change or divergent caller re-arms it); filed per the DF-4683 family-mirror directive. Harness modes 4/5 fault exactly at the block end (static-storage emulation). Skip-inverted/oversized-spans fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4686 · 16 files
FileTypeDescriptionSize
ar2425_harness.c — 19.2 KB view raw
build.sh — 85 B view raw
run.sh — 109 B view raw
fixA.diff — 1.2 KB view raw
fixB.diff — 921 B view raw
fixC.diff — 1.2 KB view raw
fixD.diff — 1009 B view raw
fix_validate.sh — 3.3 KB view raw
build.log — 773 B view raw
run.log — 3.8 KB view raw
fix_run.log — 10.2 KB view raw
env.txt — 435 B view raw
fix.diff — 1009 B view raw
VERDICT.md — 3.9 KB ↓ raw
manifest.json — 1019 B view raw
verdict.json — 5.1 KB view raw
VERDICT.md
↓ download raw

DF-4686 -- verdict narrative

What was claimed

FillVpdTable/interpolate span never clamped (ar2425 twin of DF-4683): descending curve -> (uint16_t)(Pmax-Pmin) wrap -> ~131 KB sequential OOB write past the function-static VpdTable_* block in kernel static storage; valid curve spanning >63 half-dB -> OOB write past the block with NO wrap

Reproduced?

YES (mode 4 and 5 of the shared verbatim-extraction harness, run on the DragonFly guest).

REPRODUCED on the DragonFly guest (verbatim-extraction harness, modes 4 and 5) -- the ar2425 twin of DF-4683 with an ar2425-specific twist: ar2425.c keeps VpdTable_L/R/I + Pmin_t2/Pmax_t2 as FUNCTION-SCOPE STATICS (ar2425.c:367-379), not struct members like the ar2317/ar2316/ar2413/ar5413 twins, so the span overflow marches through KERNEL STATIC STORAGE (adjacent .bss symbols), not a heap object. Neither ar2425FillVpdTable's loop (bound = (uint16_t)(Pmax-Pmin) at ar2425.c:311, store at :325) nor the interpolate loop (:419-424) clamps the span to the 64-wide row or rejects Pmax<Pmin. (Mode 4) a DESCENDING pwr_t4 curve yields Pmax_t2=84 < Pmin_t2=90 (the :398-408 min() across idxL/idxR performs no monotonicity check), the bound wraps to 65530 -> 131060 bytes of uint16 stores from VpdTable_L[0][0] through the whole 1552-byte statics block and off its end; the harness faulted on the STORE at exactly the block end (guard page, exact match). (Mode 5) a VALID ascending curve spanning 102 half-dB needs no wrap: FillVpdTable spills rows L[3]/R[3][64..] into neighboring statics, and the interpolate loop writes VpdTable_I[3][0..101]; I[3][64] lands on Pmin_t2 (kernel: adjacent .bss), clobbers the loop's own re-evaluated bound to a uint16-wrapped ~65535 and the march continues off the block end (fault exact-match). Values written are EEPROM-Vpd-derived. REACHABILITY (honest, refines DF-4683's framing): through the ONLY constructor (readEepromRawPowerCalInfo2413 -> ar2413EepromToRawDataset, ah_eeprom_v3.c:703-747) pwr deltas are UNSIGNED 4-bit fields (dbm_delta_mask=0xF, ah_eeprom_v3.c:450) summed cumulatively, so every pier curve is provably non-descending with span <= 4x15=60 half-dB: neither variant is constructible from EEPROM bits TODAY -- the missing clamp is latent/defense-in-depth (any decoder change, a divergent caller, or an eeprom v>5 re-arm it). Filed as the family twin DF-4683 explicitly flagged at ar2425.c:311. Fix validated: fixD (reject inverted/oversized spans) alone completes mode 5; mode 4 completes with fixD+fixB (skipping the inverted gain leaves zero gains -- DF-4688's early return covers the residual).

Fix validation

Harness-level validation (fix_validate.sh): baseline modes 4/5 fault at the statics-block guard on the unpatched build; with fixD applied, mode 5 completes; mode 4 completes with fixD+fixB (residual zero-gain path after skipping the inverted gain). No kernel rebuild: no AR2425 hardware on the guest, the fix is validated against the verbatim-extracted function bodies per the family methodology (DF-4619..DF-4683).

Decisive evidence: run.log: 'mode 4 ... si_addr is 1552 bytes past block start (sizeof=1552) ... MATCH' and 'mode 5 ... MATCH'; fix_run.log: 'PATCHED mode 5 with fixD only ... no SIGSEGV' and 'PATCHED mode 4 with fixB+fixD ... no SIGSEGV'.

Why no in-kernel run

The guest has no AR2425 NIC and no EEPROM-injection path; the family methodology (DF-4619..DF-4683, all accepted with the same constraint) compiles the real function bodies against faithful struct/static layouts and proves the violation with exact-address faults / canaries / propagation -- equivalent strength for a hardware-gated defect, and it validates the fix the same way.

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

Harness-level validation (fix_validate.sh): baseline modes 4/5 fault at the statics-block guard on the unpatched build; with fixD applied, mode 5 completes; mode 4 completes with fixD+fixB (residual zero-gain path after skipping the inverted gain). No kernel rebuild: no AR2425 hardware on the guest, the fix is validated against the verbatim-extracted function bodies per the family methodology (DF-4619..DF-4683).

run.log: 'mode 4 ... si_addr is 1552 bytes past block start (sizeof=1552) ... MATCH' and 'mode 5 ... MATCH'; fix_run.log: 'PATCHED mode 5 with fixD only ... no SIGSEGV' and 'PATCHED mode 4 with fixB+fixD ... no SIGSEGV'.
↓ fix.diffper-fix-DF-4686

Confirmed kernel references

Detail

Exploit chain

crafted-EEPROM AR2425 card -> ath attach -> ar2425SetPowerTable (channel set/reset) -> ar2425getGainBoundariesAndPdadcsForPowers -> OOB write/wild read; ceiling radio misprogramming/panic (DoS), not uid0

Evidence (decisive lines)

run.log: 'mode 4 ... si_addr is 1552 bytes past block start (sizeof=1552) ... MATCH' and 'mode 5 ... MATCH'; fix_run.log: 'PATCHED mode 5 with fixD only ... no SIGSEGV' and 'PATCHED mode 4 with fixB+fixD ... no SIGSEGV'.

PoC changes

ar2425-specific harness (vs DF-4680 template): the 5 function-scope static declarations (ar2425.c:367-379) are stripped from the verbatim extraction (asserted count) and re-bound to a guarded mmap block emulating kernel static storage -- ar2425 keeps the VpdTable/Pmin/Pmax scratch in .bss, not in ar2425State like the family twins; first build attempt had a macro/field name collision (offsetof) fixed by m_-prefixing the emulation fields; guest tar transfer ordering bug fixed (extract before overwrite)

Verified recommended fix

Reject inverted (Pmax= MAX_PWR_RANGE_IN_HALF_DB) pd-gain curves before FillVpdTable (fixD; pair with fixB for the zero-gain residual).

Verdict

REPRODUCED on the DragonFly guest (verbatim-extraction harness, modes 4 and 5) -- the ar2425 twin of DF-4683 with an ar2425-specific twist: ar2425.c keeps VpdTable_L/R/I + Pmin_t2/Pmax_t2 as FUNCTION-SCOPE STATICS (ar2425.c:367-379), not struct members like the ar2317/ar2316/ar2413/ar5413 twins, so the span overflow marches through KERNEL STATIC STORAGE (adjacent .bss symbols), not a heap object. Neither ar2425FillVpdTable's loop (bound = (uint16_t)(Pmax-Pmin) at ar2425.c:311, store at :325) nor the interpolate loop (:419-424) clamps the span to the 64-wide row or rejects Pmax<Pmin. (Mode 4) a DESCENDING pwr_t4 curve yields Pmax_t2=84 < Pmin_t2=90 (the :398-408 min() across idxL/idxR performs no monotonicity check), the bound wraps to 65530 -> 131060 bytes of uint16 stores from VpdTable_L[0][0] through the whole 1552-byte statics block and off its end; the harness faulted on the STORE at exactly the block end (guard page, exact match). (Mode 5) a VALID ascending curve spanning 102 half-dB needs no wrap: FillVpdTable spills rows L[3]/R[3][64..] into neighboring statics, and the interpolate loop writes VpdTable_I[3][0..101]; I[3][64] lands on Pmin_t2 (kernel: adjacent .bss), clobbers the loop's own re-evaluated bound to a uint16-wrapped ~65535 and the march continues off the block end (fault exact-match). Values written are EEPROM-Vpd-derived. REACHABILITY (honest, refines DF-4683's framing): through the ONLY constructor (readEepromRawPowerCalInfo2413 -> ar2413EepromToRawDataset, ah_eeprom_v3.c:703-747) pwr deltas are UNSIGNED 4-bit fields (dbm_delta_mask=0xF, ah_eeprom_v3.c:450) summed cumulatively, so every pier curve is provably non-descending with span <= 4x15=60 half-dB: neither variant is constructible from EEPROM bits TODAY -- the missing clamp is latent/defense-in-depth (any decoder change, a divergent caller, or an eeprom v>5 re-arm it). Filed as the family twin DF-4683 explicitly flagged at ar2425.c:311. Fix validated: fixD (reject inverted/oversized spans) alone completes mode 5; mode 4 completes with fixD+fixB (skipping the inverted gain leaves zero gains -- DF-4688's early return covers the residual).