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

ar5212GetLowerUpperValues with an empty pier list: uninitialized stack read (lp[0]) and 2-byte OOB stack read (ep[-1]) below tempChannelList, garbage pier selection can program MAX_RATE_POWER

Summary

With listSize==0 (blanked EEPROM target-power group or an all-zero CTL edge block), GetLowerUpperValues reads the never-written tempChannelList[0] and ep[-1] one uint16 below the array; garbage lower/upper flow into walks whose HALASSERT guard is compiled out in release, letting an empty CTL's entry-0 power - or MAX_RATE_POWER (63) - be selected for the channel (regulatory TX-power bypass). Confined to two stack reads in a mapped frame - no disclosure, no write primitive. All other ar5416-checklist families driven to verified negatives (ctlEdges counts bounded per-EEPROM-rev, spur masks <=31 shifts, gain-delta range analysis).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3977 Β· 5 files
FileTypeDescriptionSize
README.md β€” 3.9 KB ↓ raw
VERDICT.md β€” 2.6 KB ↓ raw
verdict.json β€” 2.6 KB view raw
manifest.json β€” 893 B view raw
fix.diff β€” 910 B view raw

DF-3977 β€” ar5212GetLowerUpperValues with an empty pier list: uninitialized stack reads and a 2-byte OOB stack read (ep[-1]) below tempChannelList

Build / Run / Expected

Hardware-gated: requires an AR5212/AR5213/AR5413-family card whose EEPROM has:

  • a target-power group with every testChannel == 0 (β†’ ee_numTargetPwr_11a/11b/11g == 0; the v3 parser computes counts by counting non-zero testChannels β€” ah_eeprom_v3.c:964-1002), or
  • a CTL with a non-zero index byte but an all-zero edge block (β†’ numEdges == 0 in ar5212GetMaxEdgePower β€” the rdEdge==0 break fires at i=0, ar5212_reset.c:2500-2505).

Both are plain zeroed/corrupted-EEPROM states β€” no precise crafting needed.

Root cause

void ar5212GetLowerUpperValues(uint16_t v, uint16_t *lp, uint16_t listSize, ...)
{
    uint32_t target = v * EEP_SCALE;
    uint16_t *ep = lp+listSize;                 /* == lp when listSize==0 */
    ...
    if (target < (uint32_t)(lp[0] * EEP_SCALE - EEP_DELTA)) {   /* UNINIT read: tempChannelList never filled */
    if (target > (uint32_t)(ep[-1] * EEP_SCALE + EEP_DELTA)) {  /* OOB read 2 bytes BELOW the array */

With listSize == 0 the caller's tempChannelList[] was never written (both callers fill it in a loop bounded by the count), so lp[0] is uninitialized stack and ep[-1] reads one uint16 below the array. The returned *vlo/*vhi (garbage or 0) then drive index selection:

  • ar5212GetMaxEdgePower:2510-2513: the "find lower index" loop never runs (numEdges==0), the HALASSERT(i != numEdges) is compiled out in production builds, and pRdEdgesPower[0].flag / twice_rdEdgePower of a CTL that was declared empty decide the edge power β€” worst case MAX_RATE_POWER (63) is programmed for the channel (regulatory-limit bypass on the radio), or a garbage-derived value.
  • ar5212GetTargetPowers:2586-2608: ixlo/ixhi default to 0 β†’ the zeroed (M_ZERO-allocated) EEPROM target powers are used β†’ TX power collapses to 0.

Memory-safety exposure is confined to two stack reads (one uninitialized, one 2 bytes below a 16-byte local array); no disclosure to userspace and no write primitive β€” hence Low, AV:P, consistent with DF-1308/1309/3960.

Verified NOT vulnerable (pass-2 negative results for the checklist)

  • (a) ctlEdges/regdomain walks: ee_numCtls is never raw EEPROM data β€” the v3 parser hardcodes {16,32} (ah_eeprom_v3.c:1147-1152) against ee_ctl[32]/ee_rdEdgesPower[8*32] (ah_eeprom_v3.h:416,445); the v14/v4k/9287 backends compute ee_numCtls = i bounded by AR5416(4K/9287)_NUM_CTLS with a sizeof-fit assert (ah_eeprom_v14.c:283-305 etc.).
  • target-power counts are computed (11a≀8, 11g≀3, 11b≀2 β€” ah_eeprom_v3.c:939/947/955) and tempChannelList[NUM_TEST_FREQUENCIES=8] can never overflow.
  • (b) GetGainBoundariesAndPdadcs does not exist in this file (RF backends ar2413/ar5413/ar2316/ ar2317/ar2425 own it) β€” out of scope here.
  • (d) spur mitigation (1750-1922): all mask shifts provably ≀ 31; binWidth assigned in every branch; callers guarantee OFDM (non-B) channels.
  • ar5212CorrectGainDelta (2416-2486): cckIndex and ratesIndex[] clamps hold for every EEPROM-reachable value of ee_cckOfdmPwrDelta (0..255) and ee_cckOfdmGainDelta (0..255) β€” proved by range analysis; ah_pcdacTable writes stay in [0,63].
  • the lp[1] past-the-end read in the walk loop is unreachable for both callers because target = freq100 ≑ 0 (mod 100) can never sit in the (last100-10, last*100+10) non-return band β€” the final iteration always returns at the "close" check.
  • FreeBSD/OpenBSD CVE family for this HAL (beacon/DMA/timer bugs) does not intersect this file's code paths; the EEPROM OOB family (DF-1308/1309/3960-era fixes) is bounded here as shown above.
VERDICT.md
↓ download raw

VERDICT β€” DF-3977

status: inconclusive (hardware-gated; source-level certainty: certain for the ep[-1] read shape, likely for end-to-end reachability which additionally requires the zeroed-EEPROM condition)

Analysis performed

Read ar5212_reset.c end-to-end (2802 lines) plus the data-feeding layers: ah_eeprom_v3.c/h (target powers, CTL edges, spur channels, gain deltas), ah_eeprom_v14/v4k/9287 ctl backends, ar5212.h (ah_pcdacTable/ah_ratesArray), ar5212_attach.c (M_ZERO allocation), ah_osdep.c (ath_hal_malloc = kmalloc M_ZERO).

Every checklist item was driven to a conclusion (see README "Verified NOT vulnerable"). The single surviving memory-safety defect is ar5212GetLowerUpperValues (ar5212_reset.c:2639/2643) reading lp[0] (uninitialized stack β€” the caller's fill loop never ran) and ep[-1] (2 bytes below the caller's tempChannelList[]) when listSize == 0. Reachable from:

  1. ar5212GetTargetPowers with ee_numTargetPwr_11b/11g == 0 (all testChannels zero in the EEPROM group β€” ah_eeprom_v3.c:973-999 counts only non-zero entries; ar5212_reset.c:2303/2218 pass the count unguarded), and
  2. ar5212GetMaxEdgePower with numEdges == 0 (a CTL whose index byte is non-zero but whose edge block is all zeros β€” ar5212_reset.c:2500-2505).

Consequences: uninitialized stack values select the lower/upper "pier" channels; the subsequent index walk in GetMaxEdgePower silently uses entry 0 of the empty CTL (HALASSERT is a no-op in release), which can program MAX_RATE_POWER for the channel β€” a regulatory TX-power bypass β€” while GetTargetPowers fails toward 0 power. No kernel-memory disclosure, no write primitive, no panic expected (both reads land in the mapped kernel stack frame).

Why not reproduced

The QEMU guest has no Atheros NIC (env.txt: no 0x168c device, no ath module); the trigger data is in the card's serial EEPROM, read at if_ath attach. Following the DF-1308/1309/3960 convention (all rated Low, AV:P, hardware-gated) this is recorded inconclusive with source-level certainty.

Fix validation

not_testable (no hardware). fix.diff: early-out in both callers when the count is zero (fail-safe: MAX_RATE_POWER is already the initialized twiceMaxEdgePower; target powers zeroed by M_ZERO), plus a defensive listSize==0 guard in GetLowerUpperValues.

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

not_testable

fix.diff authored (three guards); not runtime-testable without an Atheros NIC.

findings/poc/DF-3977/fix.diff
↓ fix.diffper-fix-DF-3977

Confirmed kernel references

Detail

Evidence (decisive lines)

README.md (reachable-empty-list conditions + the full negative-result list); VERDICT.md; fix.diff (three guards)

PoC changes

No PoC: hardware-gated (needs an AR5212-family card with zeroed EEPROM target-power/CTL blocks).

Verified recommended fix

Guard listSize==0 in ar5212GetLowerUpperValues and early-out in ar5212GetMaxEdgePower/ar5212GetTargetPowers when the pier list is empty.

Verdict

ar5212GetLowerUpperValues (ar5212_reset.c:2639/2643) dereferences lp[0] and ep[-1] before any listSize check; with the empty pier lists reachable from a zeroed/crafted EEPROM (ee_numTargetPwr_*==0 via ah_eeprom_v3.c:973-999, or a CTL with non-zero index and all-zero edges via ar5212_reset.c:2500-2505) this performs an uninitialized stack read and a 2-byte OOB stack read below the callers' tempChannelList[], and lets garbage select CTL edge power β€” worst case MAX_RATE_POWER (63) programmed for the channel (regulatory TX-power bypass). No disclosure/write primitive; Low, AV:P, consistent with the ar5416 EEPROM family (DF-1308/1309/3960). All other pass-2 checklist items for this file were driven to verified-negative conclusions (ctl walks bounded by construction in all EEPROM backends; gain-boundary code lives in the RF backends; spur-mitigation shifts provably in range; CorrectGainDelta clamps hold by range analysis).