wcid2ni[] never cleared on node free β TX-status drain use-after-free (hostap remote)
Summary
rt2860_newassoc stores an unrefcounted node pointer in sc->wcid2ni[wcid]; rt2860_node_free wipes only the hardware WCID entry and no path in the driver ever clears the array. rt2860_drain_stats_fifo runs on every TX interrupt and dereferences the stale entry - ni->ni_vap and amrr_tx_complete's write through ni->ni_rctls read from freed memory - whenever a TX_STAT_FIFO entry for a torn-down station is drained after the free. Remote 802.11 station vs a ral(4) hostap/IBSS AP: associate, exchange traffic (stats latch), deauth - repeated cycles; TXD-done writeback and MCU stat-FIFO latch are unordered, so a late-drained stat derefs the freed node. UAF write (amrr increments at fixed offsets) with chunk-reclaim grooming via new association nodes -> crash at minimum, groomed indirect call at worst. Three-line fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3749 Β· 6 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 2.5 KB | β raw | |
| VERDICT.md | β | 4.5 KB | β raw | |
| fix.diff | β | 455 B | view raw | |
| verdict.json | β | 3.3 KB | view raw | |
| env.txt | β | 275 B | view raw | |
| manifest.json | β | 1.1 KB | view raw |
DF-3749 β rt2860 wcid2ni[] dangling-pointer UAF: TX-status drain dereferences freed nodes
Severity / class
High (remote-reachable use-after-free write in hostap/IBSS operation). CWE-416.
Reproduce
Precondition: Ralink RT2860/RT3xxx NIC (if_ral.ko), interface in hostap mode
(ifconfig wlan0 create wlandev ral0 mode 11ng hostap), attacker as an associated
802.11 station in range.
- Attacker station associates β
rt2860_newassocstores a borrowed (unrefcounted) pointer:sc->wcid2ni[wcid] = ni;(rt2860.c:887). - Attacker exchanges some traffic so the AP transmits data frames to it
(
txwi->wcid = IEEE80211_AID(ni_associd), rt2860.c:1587) β the MAC latches a per-frame entry into the 16-deep TX_STAT_FIFO on completion. - Attacker sends a deauth frame. RX processing (
rt2860_rx_intrβieee80211_input, RAL_UNLOCKed window at rt2860.c:1382-1393) βieee80211_node_leaveβ the node is freed once the last reference drops.rt2860_node_free(rt2860.c:897-911) clears only the hardware WCID table βsc->wcid2ni[wcid]is never cleared anywhere in the driver (grep: set at :887, read at :1137, no other references). - Any subsequent TX interrupt on the AP drains the stats FIFO
(
rt2860_drain_stats_fiforuns from everyrt2860_tx_intr, rt2860.c:1174, and from MAC_INT_2, rt2860.c:1475-1476): a TX-status entry for the dead station's wcid that was latched late (or left in the FIFO around teardown β TXD-done writeback and the MCU's stat-FIFO write are independent hardware paths with no ordering guarantee; stats for retried frames land long after the ring slot completed) yieldsni = sc->wcid2ni[wcid]β freed node βieee80211_ratectl_tx_complete(ni->ni_vap, ni, β¦)(rt2860.c:1157-1163) βamrr_tx_completereadsni->ni_rctlsfrom freed memory and writesamn->amn_txcnt++ / amn->amn_retrycnt += β¦(wlan/ieee80211_amrr.c:363-370), plusif_inc_counter(ni->ni_vap->iv_ifp,β¦)(rt2860.c:1162).
Expected observation on hardware: kernel panic or heap corruption shortly after a station that was actively transmitting traffic disassociates/deauths under AP load. Use-after-free is deterministic at the pointer level (every teardown leaves a dangling entry); the deref depends on the stats-FIFO vs node-free interleaving, so repeat deauth-under-load cycles.
Status on this audit's guest
No Ralink NIC on the QEMU guest (env.txt) β runtime reproduction impossible; the lifetime defect itself is source-certain (see VERDICT.md).
DF-3749 β VERDICT
status: not_reproduced (hardware absent) β dangling-pointer defect source-certain; deref window requires on-air timing, assessed likely
Proof of the dangling reference (static, indisputable)
rt2860_newassocrt2860.c:885-892:sc->wcid2ni[wcid] = ni;β noieee80211_ref_node, no lifetime note. Borrowed pointer.rt2860_node_freert2860.c:897-911: clears the hardware WCID entry (RAL_SET_REGION_4(RT2860_WCID_ENTRY(wcid), 0, 2)) and calls through tosc->sc_node_free(ni)which frees the node.wcid2ni[wcid]is left pointing at freed memory.- Whole-driver grep for
wcid2ni: only rt2860.c:887 (store), rt2860.c:1137 (load). Neitherrt2860_stop_locked(4141-4174) nor any other path clears the array. rt2860_drain_stats_fifort2860.c:1133-1165 runs under RAL_LOCK from (a) MAC_INT_2 (rt2860_intr rt2860.c:1475-1476) and (b) the head of everyrt2860_tx_intr(rt2860.c:1174) β i.e. after any TX activity, including traffic to other stations on a busy AP.- Consumer chain at rt2860.c:1157-1163:
ieee80211_ratectl_tx_complete(ni->ni_vap, ni, IEEE80211_RATECTL_TX_{SUCCESS,FAILURE}, &retrycnt, NULL)βvap->iv_rate->ir_tx_complete(...)(ieee80211_ratectl.h:99-103;vapitself was read from the freed node) βamrr_tx_complete(wlan/ieee80211_amrr.c:359-370):amn = ni->ni_rctls(pointer read out of freed memory) thenamn->amn_txcnt++; amn->amn_retrycnt += retries;β write through a freed-memory-derived pointer; alsoif_inc_counter(ni->ni_vap->iv_ifp, β¦)at rt2860.c:1162.
Why the window is real (not merely theoretical)
- TX descriptor completion (TXD DDONE writeback) and the MCU latching a TX_STAT_FIFO
entry are independent hardware paths; the driver drains the FIFO before releasing
the ring slot's node reference (rt2860_tx_intr:1174 drains, then :1188
ieee80211_tx_complete drops the ring's ref), so a stat latched after that drain but
before/at TXD completion is processed in a later interrupt with zero references
held β
wcid2niis the only "reference" and it is not one. - Frames with long retry sequences (up to long-retry-limit at low rates, hundreds of ms) complete long after the ring slot's TXD for earlier frames; a deauth received in between (rt2860_intr processes RX at :1481 after the first stats drain at :1475 but before tx_intr's for rings 0-4 at :1484-1497) frees the node mid-interrupt while a stat for it may still be in flight.
- Concrete single-interrupt interleaving: RX (deauth β node freed once the RX path's own ref drops at rt2860.c:1390) followed by TX_DONE_INT0 β drain at rt2860.c:1174 picking up the just-latched stat for the dead station.
- Also reachable via
rt2860_stop_lockedβrt2860_reset_tx_ringdropping alldata->nirefs (rt2860.c:587-590) while a stat sits in the FIFO β narrowed by the following MAC reset, but the FIFO and the reset are racy on real silicon.
The pointer dangles 100% of the time after every station teardown; only the deref needs the timing above. Attacker cost: repeat (associate, ping flood, deauth) cycles against a hostap ral(4) AP β cheap.
Exploitability
ni->ni_rctls is read from freed M_80211_NODE memory; the attacker can reclaim the
chunk (same-size allocations: new association nodes with attacker-influenced fields
via assoc IEs β MAC, rates, HT caps β land in ieee80211_node members) and thereby
steer amn (write target: amn_txcnt++, amn_retrycnt += n β increment primitives
at fixed offsets) and ni->ni_vap (indirect call target vap->iv_rate->ir_tx_complete
β full control of the called pointer if vap is groomed). On this guest's
no-SMAP/no-SMEP kernel class that is a plausible code-execution path; not demonstrable
without the NIC.
Why not reproduced
Guest has no Ralink NIC (env.txt: pciconf shows none; only vtnet0/lo0 exist). The trigger is an on-air 802.11 exchange; cannot be synthesized on this VM.
Fix
fix.diff: clear sc->wcid2ni[wcid] = NULL in rt2860_node_free (paired with the
hardware-entry clear that is already there). This is the same shape OpenBSD used for
the rt2860-family wcid2ni lifetime fix.
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_testableOne-line lifetime fix, compile-consistent, mirrors the OpenBSD rt2860-family fix shape; runtime validation impossible on this guest (no NIC).
fix.diff (git apply --check passes against pristine sys/)
Confirmed kernel references
Detail
Exploit chain
attacker station associates (wcid2ni[w]=ni) -> traffic so AP transmits to it (stats latched) -> attacker deauths; RX intr frees node (node_free clears HW WCID but not wcid2ni[w]) -> a late-drained TX_STAT_FIFO entry for w yields freed ni -> ni->ni_vap deref + amrr write through ni->ni_rctls read from freed M_80211_NODE memory -> reclaim chunk via new association nodes (attacker-influenced fields) to steer amn/vap -> increment-style writes at fixed offsets / indirect call vap->iv_rate->ir_tx_complete; crash at minimum, groomed code-execution plausible on the no-SMAP class guest. Undemonstrated without hardware.
Evidence (decisive lines)
VERDICT.md (static proof + interleaving analysis), README.md (deauth-under-load recipe), fix.diff (clears wcid2ni[wcid] in node_free; git apply --check passes)
PoC changes
No PoC source: needs 802.11 radio + Ralink NIC in hostap mode. Recipe documented in README.md.
Verified recommended fix
Clear sc->wcid2ni[wcid] = NULL in rt2860_node_free next to the existing hardware WCID entry clear.
Verdict
rt2860_newassoc (rt2860.c:887) stores an unrefcounted node pointer in sc->wcid2ni[] and nothing ever clears it: rt2860_node_free (rt2860.c:897-911) wipes only the hardware WCID table, stop/reset never touch the array (whole-driver grep: set at :887, read at :1137 only). rt2860_drain_stats_fifo (rt2860.c:1133-1165) runs on every TX interrupt and dereferences the entry -- ni->ni_vap at :1157/:1160/:1162 and amrr_tx_complete's write through ni->ni_rctls (ieee80211_amrr.c:363-370) -- so any TX-status FIFO entry for a torn-down station processed after the free is a use-after-free read+write. The dangling pointer is 100% guaranteed on every station teardown (source-certain); the deref needs a stat latched/drained across the teardown window (TXD-done vs MCU stat-FIFO ordering, deauth mid-interrupt), which an attacker drives by repeated associate/traffic/deauth cycles against a ral(4) hostap. Not reproduced only because the audit guest has no Ralink NIC (env.txt).
No comments yet.