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

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)

No comments yet.

PoC verification

Evidence pack

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

  1. Victim: kldload if_ral, create wlan0 in STA mode (ifconfig wlan0 create wlandev ral0 mode 11ng), scan.
  2. Attacker AP: answer the victim's association request with an ASSOC/REASSOC Response whose AID field is 254 (0x00FE β€” a fully IEEE-legal AID; 0xC0FE also works because net80211 only clears 0xC000).
  3. Victim completes association β†’ rt2860_newassoc() runs with wcid = (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-following txq[6], rt2860var.h:150). The Tx ring-0 descriptor base pointer is replaced by a pointer to the live ieee80211_node.
  4. 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) at txq[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.

VERDICT.md
↓ download raw

DF-3748 β€” VERDICT

status: not_reproduced (hardware absent on audit guest) β€” source-certain, every link proven from code

The bug, end to end

  1. Attacker controls ni_associd in STA mode. sys/netproto/802_11/wlan/ieee80211_sta.c:1688: ni->ni_associd = associd; where associd is the raw 16-bit AID field of the AP's (re)association response (ieee80211_sta.c:1627). There is no range validation anywhere (only status != 0 at :1620 and rate negotiation are checked). An AID of 254 (or any value ≑254 mod 256, and the 0xC000-bits are ignored by IEEE80211_AID(b) ((b) &~ 0xc000), ieee80211.h:1314) is fully IEEE-legal and is simply accepted.

  2. 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->wcid2ni is struct ieee80211_node *wcid2ni[RT2860_WCID_MAX] with RT2860_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)") and vap->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 through iv_max_aid β€” the guard does not apply.

  3. 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 and ni_associd set at :1688) net80211 calls ic->ic_newassoc(vap->iv_bss, ostate != IEEE80211_S_RUN) → rt2860_newassoc with isnew=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).

  4. 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] = ni overwrites txq[0].txd (kernel virtual address of ring-0's 64-entry struct rt2860_txd DMA block) with a pointer to the live BSS ieee80211_node. - AID ≑ 255 (mod 256) β†’ wcid2ni[255] = ni overwrites txq[0].paddr. - Every subsequent rt2860_tx()/rt2860_tx_raw() on AC-BE (qid 0) does txd = &ring->txd[ring->cur] (rt2860.c:1715) and writes sdp0 (kernel-physical TXWI addr), sdl0/sdl1 (lengths β€” attacker-traffic-sized), flags at node + cur*16, walking cur 0..63 (rt2860.c:1726,1756) β€” a β‰₯1 KB, 16-byte-stride semi-controlled write across the node (which embeds ni_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.

  5. Secondary sink of the same truncation (OOB read). rt2860_tx rt2860.c:1586-1587 txwi->wcid = IEEE80211_AID(ni->ni_associd); (uint8 truncation again) β†’ the MAC latches it into TX_STAT_FIFO β†’ rt2860_drain_stats_fifo rt2860.c:1136-1137 wcid = (stat >> 8) & 0xff; ni = sc->wcid2ni[wcid]; β€” the only guard is wcid == 0xff (rt2860.c:1140), 254 is not filtered β†’ wcid2ni[254] is read (== the already-corrupted txq[0].txd/node pointer β€” after (4) this returns the node and "works"; if the newassoc write somehow did not alias yet, it yields the txq[0].txd DMA pointer which is then dereferenced as a node: ni->ni_vap at :1157/:1160/:1162 β†’ ieee80211_ratectl_tx_complete (ieee80211_ratectl.h:99-103, indirect call vap->iv_rate->ir_tx_complete) β†’ wild pointer chase). Same family: rt2860_ampdu_rx_start/stop rt2860.c:919/:934 feed an unbounded wcid into RT2860_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_testable
baseline no→ patch + rebuild →patched clean

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

Confirmed kernel references

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.