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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3749 Β· 6 files
FileTypeDescriptionSize
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.

  1. Attacker station associates β†’ rt2860_newassoc stores a borrowed (unrefcounted) pointer: sc->wcid2ni[wcid] = ni; (rt2860.c:887).
  2. 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.
  3. 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).
  4. Any subsequent TX interrupt on the AP drains the stats FIFO (rt2860_drain_stats_fifo runs from every rt2860_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) yields ni = sc->wcid2ni[wcid] β†’ freed node β†’ ieee80211_ratectl_tx_complete(ni->ni_vap, ni, …) (rt2860.c:1157-1163) β†’ amrr_tx_complete reads ni->ni_rctls from freed memory and writes amn->amn_txcnt++ / amn->amn_retrycnt += … (wlan/ieee80211_amrr.c:363-370), plus if_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).

VERDICT.md
↓ download raw

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_newassoc rt2860.c:885-892: sc->wcid2ni[wcid] = ni; β€” no ieee80211_ref_node, no lifetime note. Borrowed pointer.
  • rt2860_node_free rt2860.c:897-911: clears the hardware WCID entry (RAL_SET_REGION_4(RT2860_WCID_ENTRY(wcid), 0, 2)) and calls through to sc->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). Neither rt2860_stop_locked (4141-4174) nor any other path clears the array.
  • rt2860_drain_stats_fifo rt2860.c:1133-1165 runs under RAL_LOCK from (a) MAC_INT_2 (rt2860_intr rt2860.c:1475-1476) and (b) the head of every rt2860_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; vap itself 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) then amn->amn_txcnt++; amn->amn_retrycnt += retries; β€” write through a freed-memory-derived pointer; also if_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 β€” wcid2ni is 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_ring dropping all data->ni refs (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_testable
baseline no→ patch + rebuild →patched clean

One-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/)
↓ fix.diffper-fix-DF-3749

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).