Device-fed rs_rate through rateCodeToIndex[] 0xFF sentinel indexes sc_hwmap[32] out of bounds in ath_rx_tap/dump/led paths (OOB read exported into radiotap)
Summary
rs_rate is an 8-bit hardware-reported rate code copied verbatim from the RX descriptor. The HAL back-map is pre-initialized to 0xFF for every code not in the current mode's rate table. ath_rx_tap uses that value unchecked as index into sc_hwmap[32]: rix=255 reads ~1.8KB past the array (still inside the softc - no fault), and the garbage bytes become the per-frame radiotap wr_rate/wr_flags copied to BPF listeners. Same sentinel reaches the debug dump and the LED event path (rix=255 as int). Triggered by over-the-air frames whose hw rate code is outside the loaded table: XR-coded frames, MCS above the loaded chain count, scan/mode-change windows. Unauthenticated-wifi-peer -> 2 attacker-timed bytes/frame to privileged BPF readers. Clamp-the-sentinel fix x3 (+2 cross-file sites).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4315 Β· 8 files| File | Type | Description | Size | |
|---|---|---|---|---|
| trigger.sh | β | 869 B | view raw | |
| run.log | β | 49 B | view raw | |
| env.txt | β | 496 B | view raw | |
| fix.diff | β | 1.4 KB | view raw | |
| README.md | β | 3.9 KB | β raw | |
| VERDICT.md | β | 4.0 KB | β raw | |
| manifest.json | β | 1.1 KB | view raw | |
| verdict.json | β | 3.0 KB | view raw |
DF-4315 β ath_rx_tap: device-fed rs_rate β rateCodeToIndex[] 0xFF sentinel β sc_hwmap[255] OOB read
What
The ath(4) RX path trusts the hardware-reported rate code rs->rs_rate
(8 bits, straight from the RX status descriptor) and passes it through the
HAL back-mapping rt->rateCodeToIndex[rs->rs_rate], whose unmapped entries
are pre-initialised to (uint8_t)-1 = 0xFF (ah.c:573-574). The result is
used, unchecked, as an index into sc->sc_hwmap[32]:
ath_rx_tap()βif_ath_rx.c:577-579(wr_rate/wr_flagsare copied into the per-frame radiotap header)- the debug dump path β
if_ath_rx.c:900-903 ath_led_event()viaif_ath_rx.c:990βif_ath_led.c:194(sc->sc_hwmap[rix].ledon/.ledoff, rix passed asint= 255, no check)
sc_hwmap[255] reads an 8-byte record 2040 bytes into a 256-byte array
(~1.8 KB past its end). The read stays inside the (very large) ath_softc
allocation β no fault β but the garbage bytes are exported, two per frame,
into the radiotap header (wr_rate, wr_flags) delivered to BPF listeners
on monitor-mode VAPs, and the OOB ledon/ledoff values become LED blink
timings.
Root cause chain (path:line)
ar5416_recv.c:200rs->rs_rate = RXSTATUS_RATE(ah, ads)βMS(ds_rxstatus0, AR_RxRate)withAR_RxRate = 0xff000000(8 bits,ar5416desc.h:336);ar5212_recv.c:276equivalent for older parts. Fully device-fed.ah.c:571-583:rateCodeToIndex[]initialised to(uint8_t)-1for all 256 codes, then only the codes of the current mode's table are mapped.if_athvar.h:716:sc_hwmap[32].if_ath_rx.c:577:rix = rt->rateCodeToIndex[rs->rs_rate];(uint8_t) β norix == 0xffcheck βsc->sc_hwmap[255]at 578/579.if_ath_rx.c:990/if_ath_led.c:185-194: same sentinel reachesath_led_event(sc, 255)βsc->sc_hwmap[255].ledon/ledoff.
When is a received code unmapped? The current table (sc_currates) covers
only the current PHY mode. Monitor-mode radios receive whatever is on air:
XR-coded frames (AR5212 proprietary codes), HT MCS rates above the loaded
table's chain count (e.g. 3-stream MCS 16-23 arriving at a 1/2-stream table),
or rates of a different mode during scan/channel-change windows.
Threat
Unauthenticated wifi peer (or any RF environment on a monitor-mode box) β
2 bytes/frame of out-of-bounds softc memory exported into the radiotap header.
BPF capture requires privilege on DragonFly, and the leaked bytes come from
adjacent ath_softc members, so the practical impact ceiling is low: a
correctness bug + minor privileged-only info exposure. No write primitive
(led timings are clamped by hardware callout semantics to u16 garbage).
Severity: Low. CWE-125. Confidence: certain for the code mechanics, likely for over-the-air reachability (mode-mismatch frames).
Run (exact, on the audit guest)
vm.sh run_root 'sh /tmp/trigger.sh' # -> vtnet0 lo0 ; no Atheros PCI device (vendor 0x168c) [see run.log]
The audit QEMU guest has no Atheros NIC (run.log, env.txt), so the driver
cannot attach and the RX path is unreachable at runtime there β recorded as
not_reproduced / impact=none for the observable-on-this-guest result.
On real hardware the observation recipe is: monitor VAP + tcpdump -i wlan0 -w
cap.pcap, inject/air frames at a rate outside the loaded mode table, then
inspect wr_rate/wr_flags bytes of captured frames for values inconsistent
with any valid rate (e.g. wr_rate byte matching softc bytes ~2 KB past
sc_hwmap, varying with softc state rather than PHY rate).
Fix
fix.diff (git-apply-clean): clamp the 0xFF sentinel to a valid index (0) at
the three use sites in if_ath_rx.c. (Twin sites elsewhere β
if_ath_tdma.c:407, ath_rate/sample/sample.c:872 β follow the same pattern
but are out of this file's scope.)
Artifacts
trigger.sh, run.log, env.txt, fix.diff, VERDICT.md,
manifest.json, verdict.json
DF-4315 VERDICT β not_reproduced on the audit guest (no ath NIC); OOB-read mechanics proven in source
Reproduced?
No β impossible on this guest by construction. ifconfig -l shows only
vtnet0 lo0; pciconf -l has no vendor-0x168c (Atheros) device (run.log,
env.txt). The ath driver never attaches, ath_rx_pkt/ath_rx_tap never
execute. Recorded as not_reproduced / reproduced=0 / impact=none for the
observable-on-this-guest result (the audit vocabulary has no separate
"device absent" status).
Why the finding stands (source-level proof)
Sentinel creation β sys/dev/netif/ath/ath_hal/ah.c:571-583
(ath_hal_setupratetable equivalent): every one of the 256
rateCodeToIndex[] entries starts as (uint8_t)-1; only the current mode's
codes are overwritten with real indices.
Device-fed index β ar5416_recv.c:200 (RXSTATUS_RATE, 8-bit
AR_RxRate, ar5416desc.h:336-337), ar5212_recv.c:276
(MS(ds_rxstatus0, AR_RcvRate)). No masking/validating in between; the HAL
copies the descriptor word into rs->rs_rate.
OOB use β if_ath_rx.c:577-579:
rix = rt->rateCodeToIndex[rs->rs_rate]; /* uint8_t; 0xff when unmapped */
sc->sc_rx_th.wr_rate = sc->sc_hwmap[rix].ieeerate; /* sc_hwmap[32]! */
sc->sc_rx_th.wr_flags = sc->sc_hwmap[rix].rxflags;
sc_hwmap is declared ... sc_hwmap[32] at if_athvar.h:716 and populated
only for i < 32 (if_ath.c:6211-6233). Index 255 reads an 8-byte record at
byte offset 2040 of a 256-byte array β roughly 1.8 KB past its end. The
ath_softc struct continues well past sc_hwmap (rx/tx queues, task
structures, locks...), so the read does not fault; it returns bytes of
unrelated softc state.
Leak surface β sc_rx_th is the radiotap template; for every received
frame while a monitor VAP is up, wr_rate/wr_flags are copied into the
radiotap header seen by BPF listeners (ieee80211_radiotap_rx_all,
if_ath_rx.c:874-879). Two attacker-influenced-timing bytes of intra-softc
memory per frame β visible only to privileged BPF readers on DFly.
Second consumer β if_ath_rx.c:988-990 passes the sentinel to
ath_led_event(sc, rix); if_ath_led.c:185-194 indexes
sc->sc_hwmap[rix].ledon/.ledoff with rix == 255 and no check β OOB read
feeding LED blink timings (no further memory-safety consequence). The debug
dump path if_ath_rx.c:900-903 is a third consumer.
Reachability β requires a received frame whose hw rate code is absent from
the currently-loaded mode table. Realistic instances: monitor mode on
AR5212-class parts receiving XR-coded frames; monitor mode on 11n parts
receiving MCS codes above the loaded table's chain count; scan/mode-change
windows where sc_curmode no longer matches the frames still in flight.
Everything hinges on RF/hardware state, which is exactly why this is Low
severity: no write primitive, privileged-only visibility, weak content.
Fix validation
fix.diff clamps the 0xFF sentinel to index 0 at all three sites in this
file (git apply --check clean). Runtime validation on this guest is
impossible (no ath hardware β fix_status: not_testable); the clamp is
trivially sound by inspection: index 0 is always populated
(if_ath.c:6211-6233 initialises all 32 entries), so behaviour degrades to
"report base rate" instead of reading OOB softc bytes.
Notes / exclusions
- DF-1517 (rs_datalen OOB) is a different root cause in the same function; not re-reported.
- DF-1667 covers the EDMA m_rxpending double-free (different file,
if_ath_rx_edma.c). - The RX antenna field, the classic sibling of this bug, is already clamped
in-tree (
if_ath_rx.c:828-835);ast_rx_phy[]is guarded at:696-697;keyixis bounds-checked in net80211 (ieee80211_node.c:1679).
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_testablefix.diff is git-apply-clean and trivially sound by inspection (index 0 is always populated per if_ath.c:6211-6233); runtime validation impossible on this guest because no ath hardware can attach.
fix.diff in this pack; VERDICT.md Fix validation section.
Confirmed kernel references
Detail
Evidence (decisive lines)
VERDICT.md (sentinel creation at ah.c:573, device-fed field at ar5416_recv.c:200, OOB consumers at if_ath_rx.c:577/900/990 and if_ath_led.c:194, array bound at if_athvar.h:716); run.log (guest precondition check: no ath device); fix.diff (git apply --check clean, sentinel clamp at 3 sites)
PoC changes
No userspace trigger exists for an RX-path bug fed by hardware descriptors; shipped a precondition probe (trigger.sh) that documents and checks the hardware requirement honestly.
Verified recommended fix
Clamp the rateCodeToIndex 0xFF sentinel to index 0 at the three if_ath_rx.c use sites (fix.diff); same clamp advisable in if_ath_tdma.c and ath_rate/sample.
Verdict
OOB-read mechanics proven in source (device-fed 8-bit rs_rate -> rateCodeToIndex[] 0xFF sentinel for codes not in the current mode table -> unchecked sc_hwmap[255] read ~1.8KB past the 32-entry array, at if_ath_rx.c:577-579, 900-903, and via ath_led_event at 990/if_ath_led.c:194), but NOT runtime-reproducible on the audit guest: it has no Atheros NIC (vtnet0/lo0 only, no vendor-0x168c PCI device - run.log/env.txt), so the ath driver cannot attach. Two garbage bytes per frame are exported into the radiotap header visible only to privileged BPF listeners; no write primitive; Low severity.
No comments yet.