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

ar5212InvalidGainReadback reads rfBuf[4..5] past the 3-word RF bank 7 of 2413/2316/2317/5413/2425/2417 radios (gain-readback OOB read, UB)

Summary

The generic gain-step extraction indexes words 4-5 of bank 7, but every RF backend of that generation defines bank 7 with only 3 rows (ar5212.ini:1870/1878/1886/2158/2166) - only 5111 (8 rows) and 5112 (13, other branch) are big enough. Runs every long calibration cycle on 5GHz; on ar5413 the 8 bytes land inside vpdTable_L of the same malloc'd priv struct (C-array UB, no heap violation - RF gain misbehavior); dormant true-heap-OOB on ar2425/2417 (Bank7Data final member) gated off by 2.4GHz-only. Bounds demo verified on guest; pad-to-6-words fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5032 Β· 9 files
FileTypeDescriptionSize
README.md β€” 4.1 KB ↓ raw
VERDICT.md β€” 2.8 KB ↓ raw
oob_demo.c β€” 3.1 KB view raw
oob_demo.log β€” 1.2 KB view raw
build.sh β€” 66 B view raw
run.sh β€” 90 B view raw
env.txt β€” 197 B view raw
verdict.json β€” 3.5 KB view raw
manifest.json β€” 1.4 KB view raw

DF-5032 β€” AR5212 RF gain readback reads 2 words past the 3-word RF bank 7 (2413/2316/2317/5413/2425/2417 radios)

What

ar5212InvalidGainReadback() (ar5212_rfgain.c:123-154) takes the non-5112/5111 branch for the newer AR5212-family radios and extracts the 6-bit gain step from RF bank 7:

gStep = ar5212GetRfField(ar5212GetRfBank(ah, 7), 6, 37, 0);   /* :140 */

ar5212GetRfField() (ar5212_rfgain.c:92-120) walks the bank as bytes: firstBit=37, numBits=6 β†’ rfBuf[4] then rfBuf[5] (see the walk in oob_demo.c, output in oob_demo.log).

But for these radios, bank 7 has only 3 rows in the shared ini tables (sys/dev/netif/ath/ath_hal/ar5212/ar5212.ini):

radio bank7 rows struct holding it what follows Bank7Data
ar5111 8 ar5111.c:44 (in bounds: 4,5 < 8)
ar5112 13 ar5112.c:43 (takes the IS_RAD5112_ANY branch β€” index 4 only)
ar2413 3 (ar5212.ini:1870) ar2413.c:43 vpdTable_L (intra-struct)
ar2316 3 (:1878) ar2316.c vpdTable_L (intra-struct)
ar5413 3 (:1886) ar5413.c:43 vpdTable_L (intra-struct)
ar2317 3 (:2158) ar2317.c vpdTable_L (intra-struct)
ar2425/2417 3 (:2166) ar2425.c:44 nothing β€” last member

Reachability gate: the probe state is only armed on 5 GHz channels (or 5112+OFDM) β€” ar5212PerCalibrationN(), ar5212_reset.c:1085-1094 β†’ ar5212RequestRfgain(); the driver polls ath_hal_getrfgain() each calibration tick (if_ath.c:5401) β†’ ar5212GetRfgain() β†’ ar5212InvalidGainReadback().

Consequently:

  • ar5413 (a/b/g, 5 GHz-capable, common card silicon): the OOB read is live on every long calibration cycle on a 5 GHz channel. The 8 bytes past Bank7Data[3] land inside vpdTable_L in the same ath_hal_malloc()'d RF private struct (ar5413.c:35-57,777) β€” undefined behavior / array OOB in C, but stays inside the allocation.
  • ar2425/ar2417: Bank7Data is the final member of ar2425State (ar2425.c:36-45), so the same two reads would be a heap OOB read of 8 bytes past the allocation β€” but these radios are 2.4 GHz-only, the channel gate (ar5212_reset.c:1090) never arms the probe on them, so the path is dormant unless a 5 GHz channel is ever configured on such a part (e.g. by future band-capability changes).
  • ar2413/2316/2317: same struct shape as 5413 (vpd tables follow) and same 2.4 GHz-only gating.

Impact: gStep is garbage derived from adjacent fields β†’ L2/L3/L4, gv->loTrig/hiTrig (ar5212_rfgain.c:143-149) are wrong β†’ the gain-ladder adjust decisions (ar5212AdjustGain) misfire β†’ RF gain calibration misbehaves (link-quality degradation; worst case the radio walks the gain ladder to a rail). The value never leaves the HAL; no disclosure or corruption is reachable from it. Severity: Low (hardware-triggered, no attacker control, functional impact).

Files

  • oob_demo.c β€” replicates ar5212GetRfField's index walk (ar5212_rfgain.c:102-117) for all four call sites in the file and prints the touched word indices against the real N(ar5212Bank7_*) row counts.
  • oob_demo.log β€” its output on the audit guest (exit 1: 5 radios OOB).
  • env.txt β€” guest uname / cc.
  • verdict.json / manifest.json.

Build / run (any UNIX with a C compiler; ran on the DragonFly guest)

cc -O2 -o oob_demo oob_demo.c && ./oob_demo

Expected

InvalidGainReadback/else GetRfField(bank7, 6, 37, 0)
  words touched: [4] [5]  (max index 5)
...
ar5413  (else path)   N(ar5212Bank7_5413)    =  3 rows -> index 5 *** OUT OF BOUNDS (reads 2 words past) ***

Kernel-level verification (panic/corruption) is intentionally skipped: Low severity, no memory-safety consequence beyond the intra-struct read on the reachable path, and no AR5412/5413 hardware exists on the audit guest.

Fix

Extend the bank-7 shadow arrays for the affected radios (or clamp the read): simplest correct fix is to make ar5212InvalidGainReadback() use a bank-7 field layout matching each radio family; minimal hardening is to size Bank7Data[] to 6 words minimum in the 2413/2316/2317/5413/2425/2417 RF private structs so the byte-walk stays inside the array. See verdict.json: recommended_fix.

VERDICT.md
↓ download raw

DF-5032 VERDICT β€” AR5212 rfgain readback OOB read past 3-word bank 7

Status: untested (kernel impact), defect certain (static proof + arithmetic demo run on the guest).

Reproduced or not, and why

  • The bounds defect is proven statically and by the index-walk demo (oob_demo.log, run on the DragonFly guest): ar5212GetRfField(bank7, 6, 37, 0) β€” ar5212_rfgain.c:140 β€” touches rfBuf[4] and rfBuf[5], while every non-5112/5111 RF backend's bank 7 shadow array has only 3 words (ar5212.ini:1870/1878/1886/2158/2166).
  • The kernel path was not executed: the audit guest has no Atheros hardware (and no 5 GHz-capable 5413 card can exist in it), so ar5212GetRfgain() cannot run. Per the audit contract, PoC/Phase-V work is skipped for Low-severity, hardware-only findings.

Reachability (path:line)

ar5212PerCalibrationN (ar5212_reset.c:1085-1094) arms the probe on 5 GHz channels β†’ ar5212RequestRfgain (ar5212_rfgain.c:160-172) β†’ driver polls ath_hal_getrfgain (if_ath.c:5401) β†’ ar5212GetRfgain (ar5212_rfgain.c:279-333) β†’ ar5212InvalidGainReadback:140 β†’ OOB read. Radio gates:

  • ar5413: 5 GHz-capable β†’ live path on every calibration cycle; the read lands in vpdTable_L inside the same allocation (ar5413.c:35-57; priv from ath_hal_malloc, ar5413.c:777) β†’ C-level array OOB, no heap violation.
  • ar2425/ar2417: Bank7Data is the last member (ar2425.c:36-45) β†’ would be an 8-byte heap OOB read β€” but 2.4 GHz-only radios never pass the 5 GHz gate (ar5212_reset.c:1090-1091). Dormant.
  • ar2413/2316/2317: vpd tables follow Bank7Data (ar2413.c:35-57); 2.4 GHz only β†’ dormant, and intra-struct anyway.

Impact ceiling

gStep (garbage) β†’ wrong loTrig/hiTrig (ar5212_rfgain.c:147-149) β†’ misfiring gain-ladder adjustments (ar5212AdjustGain, :187-237) β†’ RF performance degradation / potential link loss. The read value is never exported (no info leak) and never used as an index/pointer (no corruption). Low severity, CWE-125.

Fix sketch

Keep the byte-walk inside the shadow arrays: bump the affected RF structs' Bank7Data[] to β‰₯6 words (harmless extra 12 bytes per radio priv), or gate ar5212InvalidGainReadback's gStep extraction to radios whose bank-7 layout actually places bits 33-43 there (5111 only, 8 rows) and treat the newer radios as "never adjust" (gStep == 0x3f semantics already encode that).

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.

(No fix.diff for this item: documented recommendation only - see Fix above.)

Fix verification

not_testable
baseline no→ patch + rebuild →patched clean

Fix validation not applicable: memory-corruption reproduction did not occur (no hardware), and the finding is Low severity / functional RF impact only.

n/a
per-fix-DF-5032

Confirmed kernel references

Detail

Evidence (decisive lines)

oob_demo.log: 'InvalidGainReadback/else GetRfField(bank7, 6, 37, 0) words touched: [4] [5] (max index 5)' followed by 'ar5413 (else path) N(ar5212Bank7_5413) = 3 rows -> index 5 *** OUT OF BOUNDS (reads 2 words past) ***' (x5 radios), exit 1; static refs ar5212_rfgain.c:140 + ar5212.ini bank7 row counts

PoC changes

No prior PoC; oob_demo.c authored this pass (pure-arithmetic replica of ar5212GetRfField's index walk vs the real bank-7 row counts).

Verified recommended fix

Size Bank7Data[] >= 6 words in the 2413/2316/2317/5413/2425/2417 RF priv structs (keeps the generic byte-walk in bounds), or restrict the gStep extraction at ar5212_rfgain.c:140 to radios whose bank-7 layout actually holds bits 33-43 (RF5111).

Verdict

Bounds defect is certain: ar5212InvalidGainReadback (ar5212_rfgain.c:140) extracts gStep via ar5212GetRfField(bank7, 6, 37, 0), whose byte walk (ar5212_rfgain.c:102-117) reads rfBuf[4] and rfBuf[5], but the RF bank-7 shadow arrays of the 2413/2316/2317/5413/2425/2417 backends have only 3 words (ar5212.ini:1870,1878,1886,2158,2166; only 5111 has 8, 5112 has 13 and takes the other branch). Kernel-path execution was not possible: the probe is armed only on 5 GHz channels (ar5212_reset.c:1085-1094; driver polls ath_hal_getrfgain, if_ath.c:5401) and the audit guest has no Atheros hardware. Impact on the reachable path (ar5413, 5 GHz): the 8 bytes land in vpdTable_L inside the same ath_hal_malloc'd priv struct (ar5413.c:35-57) - C-level array OOB / UB, no heap violation, value never exported nor used as an index - so the practical effect is miscalculated loTrig/hiTrig (ar5212_rfgain.c:143-149) and misfiring gain-ladder adjustments: RF performance degradation only. Dormant variant: ar2425/2417 where Bank7Data is the final struct member (ar2425.c:36-45) would make the same read an 8-byte heap OOB read, gated off because those radios are 2.4 GHz-only. Severity Low; verification skipped per contract (hardware-only trigger). The index-walk arithmetic demo (oob_demo.c, byte-for-byte replica of the walk) ran on the DragonFly guest and prints the OOB for all five 3-row radios (oob_demo.log, exit 1).