STA-mode AIDβWCID truncation: rt2860_newassoc writes node pointer past wcid2ni[254] into txq[0] (remote OOB write via rogue AP)
Summary
In STA mode net80211 stores the AP-supplied 16-bit association ID verbatim and unvalidated. rt2860_newassoc truncates IEEE80211_AID(ni_associd) (14 bits) into a uint8_t wcid and executes sc->wcid2ni[wcid] = ni against a 254-entry array. A rogue AP assigning AID 254 (or 510, 0xC0FE, ...) makes the store land on &txq[0].txd, replacing the Tx ring-0 descriptor base with a pointer to the live ieee80211_node; every subsequent AC-BE transmit then writes 16-byte TXDs across the node and adjacent heap. The same truncation reaches rt2860_tx's txwi->wcid and the TX-status drain read where only wcid==0xff is filtered. Unauthenticated remote (802.11-adjacent) attacker running an AP the victim associates with; any DFBSD host with a Ralink RT2860/RT3xxx NIC (if_ral.ko ships in /boot/kernel). Crash near-certain on first post-association TX; 16-byte-stride semi-controlled heap corruption with a plausible grooming path to code execution. Hostap is safe only because its own allocator caps AID at 253. Hardware-gated on this guest: source-certain with full recipe.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3748 Β· 6 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 2.2 KB | β raw | |
| VERDICT.md | β | 6.0 KB | β raw | |
| fix.diff | β | 1.5 KB | view raw | |
| verdict.json | β | 4.1 KB | view raw | |
| env.txt | β | 275 B | view raw | |
| manifest.json | β | 1.3 KB | view raw |
DF-3748 β rt2860 STA-mode AIDβWCID truncation: OOB write of node pointer into txq[0] (remote, rogue-AP)
Severity / class
High (remote-adjacent unauthenticated kernel heap OOB write; potential RCE-class primitive). CWE-787 / CWE-125 / CWE-704 (integer truncation feeding array index).
Reproduce
Precondition: a machine with a Ralink RT2860/RT3xxx PCI(/e) card driven by if_ral.ko,
and an attacker-controlled 802.11 AP (hostapd with a patched AID allocator, or a raw
802.11 TX setup) in range of the victim.
- Victim:
kldload if_ral, createwlan0in STA mode (ifconfig wlan0 create wlandev ral0 mode 11ng), scan. - Attacker AP: answer the victim's association request with an ASSOC/REASSOC Response
whose AID field is
254(0x00FE β a fully IEEE-legal AID;0xC0FEalso works because net80211 only clears 0xC000). - Victim completes association β
rt2860_newassoc()runs withwcid = (uint8_t)(AID & 0x3fff) = 254βsc->wcid2ni[254] = ni;writes one entry past the 254-element array (rt2860var.h:148), aliasing&sc->txq[0].txd(first member of the immediately-followingtxq[6], rt2860var.h:150). The Tx ring-0 descriptor base pointer is replaced by a pointer to the liveieee80211_node. - Any subsequent data frame queued on AC BE (ring 0) makes
rt2860_tx()write TX descriptors (struct rt2860_txd, 16 bytes: sdp0/sdl0/sdl1/sdp1/flags, rt2860reg.h:771-783) attxq[0].txd[ring->cur]=node + cur*16,cur β [0,63]β ~1 KB of 16-byte-stride semi-controlled writes across the node and adjacent heap. Expect: immediate kernel panic on next TX (or, with grooming, controlled corruption β see VERDICT.md).
Expected observation on hardware: panic in rt2860_tx / net80211 rx path right after
association + first data frame; txq[0].txd != DMA block address.
Status on this audit's guest
Guest dfbsd 6.5-DEVELOPMENT (X86_64_GENERIC, Jul 2026) has no Ralink NIC
(pciconf -lv shows none; interfaces: vtnet0 lo0) β runtime reproduction impossible.
if_ral.ko exists and pci/ral ids are registered, so the module is only one
kldload away on any machine with the hardware. Analysis is source-certain; see
VERDICT.md for the line-by-line proof.
DF-3748 β VERDICT
status: not_reproduced (hardware absent on audit guest) β source-certain, every link proven from code
The bug, end to end
-
Attacker controls
ni_associdin STA mode.sys/netproto/802_11/wlan/ieee80211_sta.c:1688:ni->ni_associd = associd;whereassocidis the raw 16-bit AID field of the AP's (re)association response (ieee80211_sta.c:1627). There is no range validation anywhere (onlystatus != 0at :1620 and rate negotiation are checked). An AID of 254 (or any value β‘254 mod 256, and the 0xC000-bits are ignored byIEEE80211_AID(b) ((b) &~ 0xc000), ieee80211.h:1314) is fully IEEE-legal and is simply accepted. -
The driver truncates it into an 8-bit index.
sys/dev/netif/ral/rt2860.c:883-887(rt2860_newassoc):c uint8_t wcid; wcid = IEEE80211_AID(ni->ni_associd); /* 14-bit value truncated to 8 bits */ if (isnew && ni->ni_associd != 0) { sc->wcid2ni[wcid] = ni; /* OOB WRITE when AID&0xff == 254 or 255 */sc->wcid2niisstruct ieee80211_node *wcid2ni[RT2860_WCID_MAX]withRT2860_WCID_MAX = 254(rt2860var.h:148, :28) β valid indices 0..253. The comment at rt2860.c:493 ("HW supports up to 255 STAs (0-254)") andvap->iv_max_aid = min(IEEE80211_AID_MAX, RT2860_WCID_MAX)(rt2860.c:494) only protect the hostap-mode AID allocator (for (aid = 1; aid < vap->iv_max_aid; aid++), wlan/ieee80211_node.c:2539 β max AID 253). In STA mode the AID is received from the AP, never passed throughiv_max_aidβ the guard does not apply. -
Reachability of newassoc in STA mode.
ieee80211_sta.c:453-454: on the S_ASSOCβS_RUN transition (i.e. right after the assoc response was parsed andni_associdset at :1688) net80211 callsic->ic_newassoc(vap->iv_bss, ostate != IEEE80211_S_RUN)βrt2860_newassocwithisnew=1. Pure remote: victim only has to associate to the attacker's AP (open/WEP/WPA2-PSK all work β the AID is inside the encrypted-after-auth assoc exchange, attacker is the AP so it sees everything). -
What the stray pointer write corrupts. softc layout (rt2860var.h:148-151):
c struct ieee80211_node *wcid2ni[254]; /* ends exactly at β¦ */ struct rt2860_tx_ring txq[6]; /* β¦ &txq[0].txd (first member) */- AID β‘ 254 (mod 256) βwcid2ni[254] = nioverwritestxq[0].txd(kernel virtual address of ring-0's 64-entrystruct rt2860_txdDMA block) with a pointer to the live BSSieee80211_node. - AID β‘ 255 (mod 256) βwcid2ni[255] = nioverwritestxq[0].paddr. - Every subsequentrt2860_tx()/rt2860_tx_raw()on AC-BE (qid 0) doestxd = &ring->txd[ring->cur](rt2860.c:1715) and writes sdp0 (kernel-physical TXWI addr), sdl0/sdl1 (lengths β attacker-traffic-sized), flags atnode + cur*16, walkingcur0..63 (rt2860.c:1726,1756) β a β₯1 KB, 16-byte-stride semi-controlled write across the node (which embedsni_vap,ni_ic, refcount, crypto key pointers) and past it into adjacent M_80211_NODE heap.bus_dmamap_sync/RAL_WRITE(TX_CTX_IDX)still use the real ring, so nothing detects the corruption. -
Secondary sink of the same truncation (OOB read).
rt2860_txrt2860.c:1586-1587txwi->wcid = IEEE80211_AID(ni->ni_associd);(uint8 truncation again) β the MAC latches it into TX_STAT_FIFO βrt2860_drain_stats_fifort2860.c:1136-1137wcid = (stat >> 8) & 0xff; ni = sc->wcid2ni[wcid];β the only guard iswcid == 0xff(rt2860.c:1140), 254 is not filtered βwcid2ni[254]is read (== the already-corruptedtxq[0].txd/node pointer β after (4) this returns the node and "works"; if the newassoc write somehow did not alias yet, it yields thetxq[0].txdDMA pointer which is then dereferenced as a node:ni->ni_vapat :1157/:1160/:1162 βieee80211_ratectl_tx_complete(ieee80211_ratectl.h:99-103, indirect callvap->iv_rate->ir_tx_complete) β wild pointer chase). Same family:rt2860_ampdu_rx_start/stoprt2860.c:919/:934 feed an unboundedwcidintoRT2860_WCID_ENTRY(wcid)(MMIO 0x1800+8*wcid; wcidβ€255 stays inside the register aperture, so MMIO-only).
Why not reproduced on this guest
dfbsd-qemu/vm.sh run_root 'pciconf -lv | grep ralink' β no device (env.txt);
interfaces are vtnet0 lo0 only. No Ralink RT2860/3090/3390/3562/5390/5392 NIC can
be attached to this VM, so the trigger (associate to rogue AP) cannot execute.
Source chain above is fully proven; the sole hardware-dependent step is step 3's
radio exchange.
Exploitability assessment
- Crash: near-certain (first ring-0 TX after association scribbles descriptors into the node; ni_vap/ni_ic corruption β panic on next rx/tx/stats).
- Controlled corruption β RCE: plausible but not demonstrated. The descriptor
fields written into the node are partially attacker-influenced (packet lengths β
sdl0/sdl1 16-bit values; sdp0 = physical address of TXWI pool blocks; flags =
qsel). A
cur-steering + heap-grooming path (spray M_80211_NODE-adjacent objects, overwrite a function-pointer-bearing victim at node+0x2xx) is conceivable on this no-SMAP/no-KASLR-class target but requires hardware iteration β recorded as the follow-up on any machine with the NIC.
Fix
fix.diff in this directory (grow wcid2ni to the full 8-bit WCID space [256]
so the truncation can no longer escape the array, plus guard the stats-drain read).
A belt-and-braces net80211 validation of the AP-provided AID
(ieee80211_sta.c:1688: reject/mask AIDs > IEEE80211_AID_MAX) is recommended
upstream but not required to close this bug.
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 a minimal, compile-consistent change (array width + drain guard) that removes both the OOB write and the OOB read sink of the truncation; it cannot be runtime-validated on the audit guest because baseline itself needs the Ralink NIC.
fix.diff (git apply --check passes against pristine sys/); no fix_build.log/fix_run.log possible without hardware.
Confirmed kernel references
- sys/dev/netif/ral/rt2860.c:885
- sys/dev/netif/ral/rt2860.c:887
- sys/dev/netif/ral/rt2860.c:1587
- sys/dev/netif/ral/rt2860.c:1136
- sys/dev/netif/ral/rt2860.c:1140
- sys/dev/netif/ral/rt2860.c:1715
- sys/dev/netif/ral/rt2860var.h:148
- sys/dev/netif/ral/rt2860var.h:28
- sys/netproto/802_11/wlan/ieee80211_sta.c:1688
- sys/netproto/802_11/wlan/ieee80211_sta.c:453
- sys/netproto/802_11/ieee80211.h:1314
Detail
Exploit chain
victim STA associates to attacker AP -> AP returns AID 254 (or 0xC0FE/510/0xC1FE...) in assoc response -> ieee80211_sta.c:1688 sets ni_associd=254 -> S_RUN transition calls ic_newassoc (sta.c:453) -> rt2860_newassoc:887 sc->wcid2ni[254]=ni aliases txq[0].txd -> victim sends/receives first data frame on ring 0 -> rt2860_tx writes txd at node+cur*16 for cur 0..63 (>=1KB 16-byte-stride semi-controlled writes: lengths attacker-sized, sdp0 = physical TXWI addr, flags) -> node ni_vap/ni_ic/refcnt/key pointers corrupted -> panic or, with heap grooming of M_80211_NODE neighbours, controlled corruption (undemonstrated, needs hardware).
Evidence (decisive lines)
VERDICT.md (full line-by-line chain), README.md (trigger recipe + expected observation), env.txt (guest hardware absence), fix.diff (applies clean via git apply --check)
PoC changes
No PoC source produced: trigger requires 802.11 radio + Ralink NIC (rogue AP assigns AID 254 in assoc response). Guest verified to lack the device; pack documents the hardware recipe instead.
Verified recommended fix
Size wcid2ni[] for the full 8-bit WCID space (RT2860_WCID_MAPSZ 256, rt2860var.h) and filter wcid >= RT2860_WCID_MAX in rt2860_drain_stats_fifo; optionally validate the AP-provided AID in ieee80211_sta.c:1688.
Verdict
Source-certain remote kernel OOB write: in STA mode net80211 stores the AP-supplied 16-bit AID verbatim (ieee80211_sta.c:1688, unvalidated), rt2860_newassoc truncates it into a uint8_t wcid and writes wcid2ni[wcid] (rt2860.c:885-887) into a 254-entry array (rt2860var.h:148); an attacker AP assigning AID 254 overwrites txq[0].txd with a live ieee80211_node pointer, and every later AC-BE transmit then writes 16-byte TX descriptors across the node object and adjacent heap (rt2860.c:1715-1741, cur up to 63) -- unauthenticated remote heap corruption with crash near-certain and a plausible grooming path to RCE. Same truncation also reaches the TX-status drain read at rt2860.c:1136-1140 (only wcid==0xff filtered). Not reproduced only because the QEMU audit guest has no Ralink NIC (pciconf shows none; env.txt); if_ral.ko is built, present in /boot/kernel and one kldload away on any host with the hardware.
No comments yet.