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

Raw-xmit path lacks TXWI pool-empty guard β†’ NULL-pointer write panic under Tx saturation

Summary

rt2860_tx and rt2860_tx_raw take SLIST_FIRST(&sc->data_pool) with only a comment asserting non-emptiness; the sole guard is rt2860_start's loop. The pool holds 128 entries while ring admission allows ~63 frames on each of 6 rings (~378), and raw/management frames (probe responses via ieee80211_raw_output -> rt2860_raw_xmit) bypass the guard: at exhaustion data==NULL -> txwi = data->txwi NULL write -> panic. DoS of any busy ral(4) interface (hostap probe-request bursts from any client + downlink congestion or power-save peer keep completions lagging); deterministic Fatal trap 12, no corruption beyond the NULL write.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3750 Β· 6 files
FileTypeDescriptionSize
README.md β€” 2.4 KB ↓ raw
VERDICT.md β€” 2.3 KB ↓ raw
fix.diff β€” 655 B view raw
verdict.json β€” 2.9 KB view raw
env.txt β€” 275 B view raw
manifest.json β€” 996 B view raw

DF-3750 β€” rt2860 raw-xmit path: TXWI pool exhaustion β†’ NULL deref panic

Severity / class

Medium (DoS; remotely assisted). CWE-476 / CWE-667 (unchecked invariant "the data pool contains at least one element").

Root cause

rt2860_tx (rt2860.c:1536) and rt2860_tx_raw (rt2860.c:1824) take data = SLIST_FIRST(&sc->data_pool); with only a comment asserting the pool is non-empty. The only empty-guard in the driver is the dequeue loop in rt2860_start (rt2860.c:2058, while (!SLIST_EMPTY(&sc->data_pool) && …)). rt2860_raw_xmit (rt2860.c:1788/1794) calls straight into rt2860_tx/rt2860_tx_raw with no such guard (net80211 ieee80211_raw_output doesn't check either).

Why the invariant does not hold

  • Pool size RT2860_TX_POOL_COUNT = RT2860_TX_RING_COUNT * 2 = 128 (rt2860var.h:21-23). One pool entry is consumed per queued frame (rt2860.c:1744/2002) and returned only by TX-complete processing (rt2860.c:1191) or ring reset.
  • Per-ring admission is ring->queued + ntxds >= RT2860_TX_RING_COUNT (rt2860.c:1671, 1704) β†’ up to 63 single-segment frames per ring, across 6 rings (4 EDCA + HCCA + mgmt; rt2860.c:291) β†’ up to ~378 frames in flight vs 128 pool entries. qfullmsk only marks full rings (rt2860.c:1758-1759, checked at 2058) β€” it does not protect the pool across rings.
  • Raw (management) frames land on mgtqid or an EDCA ring via qid = params->ibp_pri & 3 (rt2860.c:1840) β€” a probe response / auth frame transmitted while ~128 data frames are still unacknowledged in the rings takes the pool below empty β†’ SLIST_FIRST returns NULL β†’ txwi = data->txwi (rt2860.c:1582/1848) β†’ write to NULL+offset β†’ kernel panic.

Trigger recipe (hardware required)

Hostap ral(4) AP under downlink saturation (e.g. iperf to a station, filling β‰₯2 AC rings with ~63 frames each while TX completions lag), combined with probe-request bursts from any 802.11 client (probe responses go through ieee80211_send_mgmt β†’ ic_raw_xmit = rt2860_raw_xmit). Local variant: any local user pumping frames out a busy STA/hostap interface while a scan (probe requests) is in flight.

Expected observation on hardware: Fatal trap 12: page fault during read/write at 0x… in rt2860_tx/rt2860_tx_raw shortly after the interface goes quiet-but-full.

Status on this audit's guest

No Ralink NIC (env.txt) β€” not reproducible here; counting argument above is exact.

VERDICT.md
↓ download raw

DF-3750 β€” VERDICT

status: not_reproduced (hardware absent) β€” arithmetic of the invariant breach is exact; consequence is an unambiguous NULL write

  • Guard exists only in one of three call paths: rt2860_start:2058 checks !SLIST_EMPTY(&sc->data_pool); rt2860_raw_xmit (rt2860.c:1788 β†’ rt2860_tx at :1536, rt2860_tx_raw at :1824) has no check, and net80211's ieee80211_raw_output (wlan/ieee80211_output.c:635) just calls ic->ic_raw_xmit.
  • Capacity math (rt2860var.h:21-23): pool 128; admission per ring allows ~63 frames; 6 rings; pool entries are per-frame while queued counts TXDs (1-2 per frame) β€” in-flight frames can exceed 128 while every per-ring admission test passes.
  • Failure mode: data = NULL β†’ txwi = data->txwi (offset 0x0 of struct rt2860_tx_data) β†’ txwi->flags = 0 (rt2860.c:1583/1849) writes to 0x8-ish address β†’ unrecoverable panic. (No KASSERT guards this; note the same file shows the author knew the pattern β€” rt2860_tx_raw validates ridx at :1834 while rt2860_tx doesn't.)
  • Reachability: needs (a) β‰₯128 frames in flight, i.e. downlink congestion / power-save peer / TX stall before completions, plus (b) a management frame transmitted via the raw path at that moment β€” on a hostap interface, attacker probe requests continuously generate (b) for free; (a) is producible by ordinary traffic bursts (a station in 802.11 power-save also delays completions).
  • Severity Medium: deterministic panic, but needs a load coincidence; no memory corruption beyond the NULL write (first dereference faults immediately).
  • Not reproduced: no Ralink NIC on the audit guest (env.txt).

Fix (fix.diff): guard rt2860_raw_xmit exactly like rt2860_start β€” drop the frame with ENOBUFS when the pool is empty. (Guarding inside rt2860_tx/rt2860_tx_raw instead would be equivalent; the raw_xmit site keeps the invariant with the existing m_freem/ENETDOWN error shape used at rt2860.c:1778-1782.)

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

Minimal guard in the unguarded call path; compile-consistent; runtime validation needs hardware.

fix.diff (git apply --check passes against pristine sys/)
↓ fix.diffper-fix-DF-3750

Confirmed kernel references

Detail

Exploit chain

saturate >=2 Tx rings with unacknowledged frames (iperf / PS peer) -> pool (128) empties while rings still admit -> attacker's probe request triggers ieee80211_send_mgmt -> ieee80211_raw_output -> rt2860_raw_xmit -> rt2860_tx with empty pool -> NULL->txwi deref -> Fatal trap 12 panic (DoS).

Evidence (decisive lines)

README.md (exact capacity math + recipe), VERDICT.md, fix.diff (raw_xmit guard, git apply --check passes)

PoC changes

No PoC source: needs the NIC + radio load. Recipe documented.

Verified recommended fix

Guard rt2860_raw_xmit with SLIST_EMPTY(&sc->data_pool) (drop frame, ENOBUFS), matching rt2860_start:2058.

Verdict

rt2860_tx (rt2860.c:1536) and rt2860_tx_raw (rt2860.c:1824) take SLIST_FIRST(&sc->data_pool) with only a comment claiming the pool is non-empty; the sole empty-guard is rt2860_start's dequeue loop (rt2860.c:2058). The raw-xmit path (rt2860_raw_xmit rt2860.c:1788/1794, driven by net80211 management frames incl. hostap probe responses) bypasses it. Pool capacity is 128 (rt2860var.h:23) while ring admission allows ~63 single-segment frames on each of 6 rings (~378 > 128) with qfullmsk tracking only per-ring fullness -- under downlink congestion or a power-save peer, a management frame at the wrong moment makes SLIST_FIRST return NULL and txwi = data->txwi (rt2860.c:1582/1848) NULL-write-panics the kernel. Deterministic panic (DoS), remotely assistable (probe requests keep the raw path busy), no corruption beyond the NULL write. Not reproduced: guest has no Ralink NIC (env.txt); the capacity arithmetic is exact.