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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4315 Β· 8 files
FileTypeDescriptionSize
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_flags are copied into the per-frame radiotap header)
  • the debug dump path β€” if_ath_rx.c:900-903
  • ath_led_event() via if_ath_rx.c:990 β†’ if_ath_led.c:194 (sc->sc_hwmap[rix].ledon/.ledoff, rix passed as int = 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)

  1. ar5416_recv.c:200 rs->rs_rate = RXSTATUS_RATE(ah, ads) β€” MS(ds_rxstatus0, AR_RxRate) with AR_RxRate = 0xff000000 (8 bits, ar5416desc.h:336); ar5212_recv.c:276 equivalent for older parts. Fully device-fed.
  2. ah.c:571-583: rateCodeToIndex[] initialised to (uint8_t)-1 for all 256 codes, then only the codes of the current mode's table are mapped.
  3. if_athvar.h:716: sc_hwmap[32].
  4. if_ath_rx.c:577: rix = rt->rateCodeToIndex[rs->rs_rate]; (uint8_t) β€” no rix == 0xff check β†’ sc->sc_hwmap[255] at 578/579.
  5. if_ath_rx.c:990 / if_ath_led.c:185-194: same sentinel reaches ath_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

VERDICT.md
↓ download raw

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; keyix is 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_testable
baseline no→ patch + rebuild →patched clean

fix.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.
↓ fix.diffper-fix-DF-4315

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.