DF-3749 / verdict.json
{ "finding_id": "DF-3749", "status": "not_reproduced", "reproduced": 0, "impact": "none", "confidence": "likely", "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_completes 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).", "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": "VERDICT.md (static proof + interleaving analysis), README.md (deauth-under-load recipe), fix.diff (clears wcid2ni[wcid] in node_free; git apply --check passes)", "kernel_refs": [ "sys/dev/netif/ral/rt2860.c:887", "sys/dev/netif/ral/rt2860.c:897", "sys/dev/netif/ral/rt2860.c:1133", "sys/dev/netif/ral/rt2860.c:1137", "sys/dev/netif/ral/rt2860.c:1157", "sys/dev/netif/ral/rt2860.c:1162", "sys/dev/netif/ral/rt2860.c:1174", "sys/netproto/802_11/wlan/ieee80211_amrr.c:363" ], "poc_changes": "No PoC source: needs 802.11 radio + Ralink NIC in hostap mode. Recipe documented in README.md.", "attempts": 1, "guest_uname": "DragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64", "runtime_sec": 10, "guest_dirty": 0, "build_cmd": "", "run_cmd": "dfbsd-qemu/vm.sh run_root pciconf -lv | grep ralink; ifconfig -l", "code_hash": "", "notes": "Pass-2 depth finding (lifetime across teardown vs interrupt state machine). fix.diff composes with DF-3748s array widening (with wcid2ni[256] the node_free clear is index-safe for any truncated wcid).", "recommended_fix": "Clear sc->wcid2ni[wcid] = NULL in rt2860_node_free next to the existing hardware WCID entry clear.", "fix_status": "not_testable", "fix_baseline_reproduced": 0, "fix_patched_reproduced": 0, "fix_verdict": "One-line lifetime fix, compile-consistent, mirrors the OpenBSD rt2860-family fix shape; runtime validation impossible on this guest (no NIC).", "fix_evidence": "fix.diff (git apply --check passes against pristine sys/)" } |