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

DWDS discovery frames staged on ic_stageq are leaked forever on age-out: ieee80211_node_timeout discards the mbuf list returned by ieee80211_ageq_age (remote kernel memory-exhaustion DoS)

Summary

ieee80211_dwds_discover stages every authorized station's 4-addr frame on ic->ic_stageq with a finite TTL. ieee80211_ageq_age() REMOVES expired frames and returns them as a list 'for the caller to reclaim', but ieee80211_node_timeout calls it as a bare statement - the returned list is dropped on the floor. Whenever discovery events are not serviced (no hostapd DWDS plumbing, daemon restart/race - wds_flush is the only consumer), every staged frame leaks mbuf + ~2KB cluster after its TTL. The aq_maxlen bound limits only backlog, not cumulative leak: an associated station (association is the only gate) sending a sustained 4-addr stream leaks ~rate x 2KB/s until kernel OOM. FreeBSD HEAD has the identical bare call - inherited defect. DF-0618 covered the KASSERT overflow in the same age expression; the leak at expiry was missed. Hardware-gated on the audit guest (no radio; evidence in pack) - path:line chain documented. Fix: consume the returned list with ieee80211_ageq_mfree.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3429 Β· 8 files
FileTypeDescriptionSize
README.md β€” 2.0 KB ↓ raw
VERDICT.md β€” 2.8 KB ↓ raw
trigger_stageq_leak.c β€” 2.4 KB view raw
run.sh β€” 139 B view raw
wlan_gate.txt β€” 137 B view raw
fix.diff β€” 1.0 KB view raw
verdict.json β€” 3.1 KB view raw
manifest.json β€” 939 B view raw

DF-3429 β€” DWDS discovery staged frames leak on age-out (remote DoS)

Build / Run

  • No userland build: kernel-side defect.
  • ./run.sh β€” documents the hardware gate on the audit guest (no radio attaches; ifconfig -C shows no usable wlandev), so the path cannot be exercised in QEMU. All analysis is line-verified against both this tree and FreeBSD HEAD (identical defect inherited).

Mechanism (path:line)

  1. Attacker (authorized station of an AP vap) sends 4-address data frames.
  2. hostap_input β†’ dir == DSTODS && ni->ni_wdsvap == NULL + authorized β†’ ieee80211_dwds_discover(ni, m) (ieee80211_hostap.c:654-667).
  3. ieee80211_dwds_discover (ieee80211_wds.c:321-338) tags the mbuf with the MAC hash and appends it to ic->ic_stageq with a finite TTL.
  4. If a WDS vap is plumbed, wds_flush (ieee80211_wds.c:102-122) pulls the frames back out β€” no leak.
  5. If NOT serviced, ieee80211_node_timeout fires every IEEE80211_INACT_WAIT and calls ieee80211_ageq_age(&ic->ic_stageq, IEEE80211_INACT_WAIT) (ieee80211_node.c:2255) β€” return value (list of expired frames) ignored. ieee80211_ageq_age (ieee80211_ageq.c:162-185) removes the expired frames and returns them for the caller to free. They are dropped on the floor: permanent leak of mbuf + 2KB cluster per frame.

Impact

Remote, unauthenticated-after-association kernel memory exhaustion: sustained 4-addr frames at rate R β‰ˆ R Γ— 2KB/s leaked forever. Queue bound (aq_maxlen = ic_max_keyix * 8) only bounds the backlog, not the leak. Prerequisite: AP-mode vap with DWDS-capable setup and discovery events not consumed (daemon absent/restarting) β€” a common field state.

Upstream

FreeBSD HEAD sys/net80211/ieee80211_node.c:2568 has the identical bare ieee80211_ageq_age(&ic->ic_stageq, IEEE80211_INACT_WAIT); β€” inherited defect; upstream disclosure recommended together with the DFly fix.

Verdict

inconclusive β€” mechanics certain from source; runtime verification hardware-gated (QEMU guest has no 802.11 radio; wlan_gate.txt).

VERDICT.md
↓ download raw

DF-3429 VERDICT β€” stageq expired-frame leak (DWDS discovery)

status: inconclusive | reproduced: 0 | impact: none demonstrated (hw-gated) | finding severity: Medium (remote memory-exhaustion DoS)

Reproduced?

No β€” and cannot be, on this guest: exercising the path requires an 802.11 radio (driver β†’ ieee80211com β†’ AP vap β†’ associated station sending 4-addr frames). The QEMU guest has none: kldload if_iwm/if_run attach no device, pciconf -l lists zero wireless controllers, ifconfig -C shows the wlan cloner but no wlandev exists (wlan_gate.txt). The mechanics are nevertheless certain from source, with an upstream cross-check:

  • ieee80211_ageq_age() (sys/netproto/802_11/wlan/ieee80211_ageq.c:162-185) removes expired frames and returns them as an m_nextpkt list ("The list of removed frames is returned to the caller" β€” its own comment, ageq.c:157-159).
  • ieee80211_node_timeout() (ieee80211_node.c:2255) calls it as a bare statement β€” the returned list is unreachable β†’ leaked (mbuf + cluster per frame; staged DWDS frames are raw RX frames, not M_ENCAP, so no node ref rides along β€” pure mbuf/cluster leak).
  • Producer: ieee80211_dwds_discover (ieee80211_wds.c:333-337) appends every authorized station's 4-addr frame to ic->ic_stageq with a finite TTL ((ni_intval * ic_lintval << 2) / 1024 s β€” the DF-0618 expression; normal values β‰ˆ tens of seconds).
  • Gate: ieee80211_hostap.c:654-667 β€” requires ieee80211_node_is_authorized(ni), i.e. attacker must complete association (+802.1X if enabled). Open/PSK networks: trivial.
  • FreeBSD HEAD comparison: identical bare call at sys/net80211/ieee80211_node.c:2568 (fetched 2026-09-12) β€” inherited.

Why "inconclusive" and not "not_reproduced"

The classification difference matters: nothing about the code failed to behave β€” the environment cannot host the behavior at all. If a USB radio were passed through, the trigger is: AP vap up, associate, send 4-addr frames for ~TTL+20s with no WDS vap created, observe netstat -m/mbstat cluster count monotonically increase with no mbuf returned (or vmstat -m M_MBUFCL growth), repeatable across runs.

Fix

Consume the returned list in ieee80211_node_timeout: struct mbuf *m; m = ieee80211_ageq_age(...); if (m != NULL) ieee80211_ageq_mfree(m); (ieee80211_ageq_mfree applies M_ENCAP node-ref rules β€” ageq.c:87-98). See fix.diff (against node.c). Fix-status not_testable (hw-gated).

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 authored against ieee80211_node.c (consume the returned list); cannot be validated on the QEMU guest - no 802.11 hardware, so neither baseline nor patched behavior is exercisable.

fix.diff; wlan_gate.txt
↓ fix.diffper-fix-DF-3429

Confirmed kernel references

Detail

Exploit chain

associate to AP vap (open/PSK) -> send 4-addr data frames (no WDS vap plumbed) -> frames stage on ic_stageq with ~40s TTL -> ageq_age expires them -> list leaked -> repeat at rate R leaks ~R*2KB/s until OOM/panic.

Evidence (decisive lines)

VERDICT.md (path:line chain + upstream comparison); trigger_stageq_leak.c (annotated); wlan_gate.txt (hardware gate on guest); fix.diff

PoC changes

No seed; authored annotated trigger. Guest attempt limited to proving the hardware gate (no radio => no wlan vaps).

Verified recommended fix

Capture and free the list returned by ieee80211_ageq_age() in ieee80211_node_timeout (ieee80211_ageq_mfree); see fix.diff.

Verdict

DWDS discovery frames staged on ic->ic_stageq by ieee80211_dwds_discover (ieee80211_wds.c:333-337, gated only by station authorization at ieee80211_hostap.c:654-667) are removed-and-returned by ieee80211_ageq_age() when their TTL expires, but ieee80211_node_timeout() calls it as a bare statement (ieee80211_node.c:2255) and drops the returned mbuf list on the floor: every unserviced discovery frame permanently leaks its mbuf + 2KB cluster. Sustained 4-addr traffic from an associated station = sustained kernel heap exhaustion (remote DoS). Runtime verification is hardware-gated: the QEMU audit guest has no 802.11 radio (kldload if_iwm/if_run attach nothing, zero wireless PCI devices - wlan_gate.txt), so no ieee80211com exists to exercise the path; mechanics are certain from source and the identical bare call exists in FreeBSD HEAD (ieee80211_node.c:2568), i.e. the defect is inherited from upstream.