Unbounded mesh route-table growth + attacker-controlled lifetime: remote memory exhaustion DoS
Summary
Every PREQ/PREP/RANN with new orig/target addr calls ieee80211_mesh_rt_add -> kmalloc + TAILQ_INSERT NO upper bound. Lifetime from attacker-controlled preq_lifetime uint32 via mesh_rt_update(:1097). Remote: flood PREQs with distinct spoofed originator MACs + maximal lifetime -> unbounded kernel memory growth -> exhaustion/panic.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0350 Β· 13 files| File | Type | Description | Size | |
|---|---|---|---|---|
| harness.c | trigger-source | faithful in-process harness embedding verbatim mesh_rt_add_locked/ieee80211_mesh_rt_update + PREQ-originator logic; -DFIXED toggles cap+clamp | 18.9 KB | view raw |
| build.sh | build-script | cc -O2 -Wall builds harness (BUG PRESENT) and harness_fixed (FIXED) | 381 B | view raw |
| run.sh | run-script | runs both harnesses, prints before/after contrast | 438 B | view raw |
| build.log | build-log | harness build on #0 unpatched kernel | 236 B | view raw |
| run.log | run-log | BUG-PRESENT harness on #0: 8000/8000 added, lifetime 49.7d | 2.2 KB | view raw |
| fix_build.log | build-log | single-fix kernel make nativekernel, full output, rc=0 | 5.6 MB | β download |
| fix_run.log | run-log | FIXED harness on #1 patched kernel: capped at 4096, lifetime 60s | 1.3 KB | view raw |
| fix.diff | suggested-fix | git-apply-able: cap per-vap route table at 4096 + clamp lifetime at 60s | 3.7 KB | view raw |
| env.txt | environment | uname, cc, ifconfig -l (no wlan), kldstat, net.wlan.hwmp sysctls | 754 B | view raw |
| VERDICT.md | verdict | full narrative: line-by-line trace, mechanism, impact ceiling, fix, validation | 10.8 KB | β raw |
| README.md | readme | human-facing build/run/expected + file index | 3.5 KB | β raw |
| ../fix_build_combined.log | build-log | Combined 41-finding kernel build (rc=0, -Werror clean) | 5.6 MB | β download |
| ../fix_build_summary.txt | build-summary | Summary of the combined 41-finding kernel build | 826 B | view raw |
DF-0350 β Unbounded mesh route-table growth + attacker-controlled lifetime
Finding: DF-0350 (High) β sys/netproto/802_11/wlan/ieee80211_hwmp.c:1057-1097
Class: CWE-400 unbounded resource consumption (kernel memory exhaustion DoS)
Verdict: REPRODUCED (code-level harness) + FIX VALIDATED (built & booted)
What the bug is
Every HWMP PREQ/PREP/RANN frame carrying a new originator/target address
calls ieee80211_mesh_rt_add β mesh_rt_add_locked()
(ieee80211_mesh.c:194-228) which kmallocs a struct ieee80211_mesh_route
(~100β150 B) and TAILQ_INSERT_TAILs it into ms->ms_routes with no upper
bound on the number of entries. Additionally, preq->preq_lifetime
(ieee80211_hwmp.c:457, a uint32 straight off the wire) flows unvalidated
into rt->rt_lifetime via ieee80211_mesh_rt_update (ieee80211_mesh.c:266,
hwmp.c:1097), so a single spoofed PREQ pins its entry for up to
0xFFFFFFFF msec β 49.7 days.
An unauthenticated attacker within WiFi radio range of a mesh-mode vap
(wlanmode mesh) floods PREQs with distinct spoofed originator MACs + maximal
lifetime; the route table grows without bound (~2^46 distinct MACs available)
until kmem_map is exhausted β kernel panic / memory-pressure stall (DoS).
Why a harness (no runtime PoC)
This KVM guest has no WiFi radio (ifconfig -l = vtnet0 lo0;
ifconfig wlan create wlandev β¦ wlanmode mesh β Device not configured), so
the live mesh RX path is unreachable. This matches the settled no-HW findings
DF-0393 / DF-0594 / DF-0616. The harness embeds the verbatim kernel
allocation/insertion/lifetime code and drives it with an attacker frame stream
to prove both claims; a -DFIXED build mirrors the fix.diff to prove the
closure. The vulnerable code IS in the default GENERIC kernel
(net.wlan.hwmp.* sysctls present); only the runtime trigger (a radio) is
absent.
Build & run
ssh dfbsd-maxx # maxx uid 1001 (unprivileged) cd poc/DF-0350 ./build.sh # builds: harness (BUG PRESENT) + harness_fixed (FIXED) ./run.sh # runs both, prints the before/after contrast
Expected output
BUG PRESENT (harness):
routes successfully added : 8000 routes rejected (NULL) : 0 final ms_routes table size : 8000 entries first added route rt_lifetime : 0xFFFFFFFF msec (49.7 days) VERDICT: UNBOUNDED GROWTH CONFIRMED β all 8000 PREQs added a new entry; no cap, no NULL, lifetime = attacker uint32.
FIXED (harness_fixed, mirrors fix.diff):
routes successfully added : 4096 routes rejected (NULL) : 3904 final ms_routes table size : 4096 entries first added route rt_lifetime : 0x0000EA60 msec (60 s) VERDICT: FIXED β table capped at 4096 entries (rejected 3904 over-cap PREQs); lifetime clamped to 60000 msec (60 s, was 49.7 days)
Harness arguments
./harness [Nattack] [lifetime] β default 8000 distinct originators,
0xFFFFFFFF lifetime. Nattack > 4096 exercises the cap in the -DFIXED
build.
Files
harness.cβ faithful in-process model (verbatim kernel snippets) of the bug and the fix.-DFIXEDtoggles the cap+clamp.build.sh/run.shβ exact build/run.fix.diffβ the validated kernel fix (cap + clamp),git apply-able.VERDICT.mdβ full narrative + line-by-line trace.build.log/run.logβ BUG-PRESENT harness on#0unpatched.fix_build.logβ single-fix kernel build (full).fix_run.logβ FIXED harness on#1patched kernel.env.txtβ guest environment.manifest.jsonβ artifact catalog.
DF-0350 β VERDICT
Verdict: REPRODUCED (code-level harness) + FIX VALIDATED (built + booted)
The unbounded mesh route-table growth and attacker-controlled lifetime in the
HWMP mesh path-selection code are real and confirmed by a faithful
in-process harness that embeds the verbatim kernel allocation/insertion/lifetime
code. A single-fix kernel was built, installed and booted (as #1); the
post-fix harness demonstrates the table is now capped and the lifetime clamped.
Runtime reachability on this guest: the live mesh RX path
(hwmp_recv_action_meshpath β hwmp_recv_preq β ieee80211_mesh_rt_add) is
unreachable here because there is no WiFi radio β ifconfig -l shows only
vtnet0 lo0, and ifconfig wlan create wlandev β¦ wlanmode mesh fails with
SIOCIFCREATE2: Device not configured. This is identical to the settled
findings DF-0393 (Mesh ID heap overflow), DF-0594 (TKIP RX underflow)
and DF-0616 (netmap RX overflow), all resolved via a faithful in-process
harness because the 802.11/netmap RX path needs hardware the guest lacks.
IEEE80211_SUPPORT_MESH is compiled into the default GENERIC kernel (the
net.wlan.hwmp.* sysctls exist), so the vulnerable code is present β only the
runtime trigger (a real radio receiving 802.11 HWMP action frames) is absent.
Bug location (line-by-line trace)
Claim #1 β unbounded route-table growth
Sink (the single choke point for ALL adders) β sys/netproto/802_11/wlan/ieee80211_mesh.c:194-228, mesh_rt_add_locked():
rt = kmalloc(ALIGN(sizeof(struct ieee80211_mesh_route)) +
ms->ms_ppath->mpp_privlen, M_80211_MESH_RT, M_INTWAIT | M_ZERO);
if (rt != NULL) {
...
TAILQ_INSERT_TAIL(&ms->ms_routes, rt, rt_next); // <-- NO count check, NO upper bound
}
There is no comparison against any maximum anywhere in the add path. The
ms_routes TAILQ (sys/netproto/802_11/ieee80211_mesh.h:525) grows by one
entry per call. This function is reached from eight call sites, all in the
attacker-reachable HWMP/mesh RX paths:
| Caller | File:line | Trigger frame |
|---|---|---|
hwmp_recv_preq (orig) |
ieee80211_hwmp.c:1057 |
PREQ (cited) |
hwmp_recv_preq (orig-ext) |
ieee80211_hwmp.c:1191 |
PREQ + AE flag |
hwmp_recv_prep (target) |
ieee80211_hwmp.c:1429 |
PREP |
hwmp_recv_prep (target-ext) |
ieee80211_hwmp.c:1572 |
PREP + AE flag |
hwmp_recv_rann |
ieee80211_hwmp.c:1975 |
RANN |
hwmp_send_preq (discovery) |
ieee80211_hwmp.c:2134 |
local path request |
hwmp_recv_preq (transmitter) |
ieee80211_hwmp.c:935 |
PREQ |
ieee80211_mesh_input |
ieee80211_mesh.c:1004 |
mesh data (source route) |
The MAC address space is 2^48; using a locally-administered OUI an attacker has
~2^46 distinct originator addresses. Each entry is
ALIGN(sizeof(struct ieee80211_mesh_route)) + sizeof(struct ieee80211_hwmp_route)
β 100 bytes (harness measurement; ~150 B with lock/callout bookkeeping in
the real kernel). At ~100 B/entry the table can pin gigabytes of wired
kernel memory before any MAC collision β kmem_map exhaustion β kernel panic
or memory-pressure stall (DoS).
The only cleanup, mesh_rt_flush_invalid() (ieee80211_mesh.c:426-444), runs
from the ms_cleantimer callout every mpp_inact ticks (HWMP default
net.wlan.hwmp.inact = 5000 ticks β 5 s) and only removes routes whose
lifetime has expired. Because the attacker also controls the lifetime (claim
2), freshly-flooded entries survive every cleanup pass for the full
attacker-chosen dwell β cleanup does not bound the table.
Claim #2 β attacker-controlled lifetime
Attacker input β sys/netproto/802_11/wlan/ieee80211_hwmp.c:457 (PREQ IE parse):
preq->preq_lifetime = le32dec(iefrm_t); iefrm_t += 4; // uint32, straight off the wire
No validation, no clamp. (Identical pattern at :508 for prep->prep_lifetime.)
Propagation to the route entry β ieee80211_hwmp.c:1097:
ieee80211_mesh_rt_update(rtorig, preq->preq_lifetime); // attacker uint32 -> int param
Sink β ieee80211_mesh.c:266-303, ieee80211_mesh_rt_update():
int ieee80211_mesh_rt_update(struct ieee80211_mesh_route *rt, int new_lifetime) {
...
if (new_lifetime != 0) {
rt->rt_lifetime = new_lifetime; // <-- attacker uint32 stored verbatim
}
...
}
preq->preq_lifetime (uint32) is passed to an int parameter and stored into
rt->rt_lifetime (uint32). 0xFFFFFFFF msec β 49.7 days β a single
spoofed PREQ pins its ~100-byte entry for ~50 days. The legitimate default is
net.wlan.hwmp.pathlifetime = 5000 (ticks β 5 s); the attacker value is ~10^6Γ
larger.
Why a code-level harness (the no-WiFi-HW precedent)
The KVM guest has no WiFi radio (verified: ifconfig -l = vtnet0 lo0;
no wlan vap; ifconfig wlan create wlandev vtnet0 wlanmode mesh β
SIOCIFCREATE2: Device not configured; no ath/iwm/iwn kld bound to hardware;
no ng_80211/ng_wlan netgraph injection node). The runtime mesh RX path is
therefore unreachable β identical to DF-0393/DF-0594/DF-0616. The harness embeds
the verbatim mesh_rt_add_locked/ieee80211_mesh_rt_update/PREQ-originator
bodies (including the #if defined(__DragonFly__) kmalloc path, modelled with
malloc(3)) and drives them with an attacker-shaped frame stream: distinct
spoofed originator MACs + preq_lifetime = 0xFFFFFFFF.
Reproduction (BUG-PRESENT harness, on the #0 unpatched kernel)
8 000 distinct spoofed PREQ originators, each carrying preq_lifetime =
0xFFFFFFFF:
- routes successfully added : 8000 β every PREQ added a new entry; 0
rejected; no cap, no NULL return.
- final table size : 8000 entries (0.8 MiB at 100 B/entry).
- first route rt_lifetime = 0xFFFFFFFF msec (49.7 days) β exactly the
attacker preq_lifetime; unvalidated. Both claims CONFIRMED.
Full output in run.log. The DoS ceiling extrapolation: ~2^46 distinct MACs
available, ~2.7Γ10^9 entries to exhaust a 256 GB kmem_map β the attacker can
pin memory until exhaustion/panic.
Impact ceiling
Remote unauthenticated kernel memory-exhaustion DoS on any WiFi-equipped
host running the default GENERIC kernel (IEEE80211_SUPPORT_MESH enabled) with
a mesh-mode vap (wlanmode mesh, IEEE80211_M_MBSS). An attacker within radio
range floods HWMP PREQ (or PREP/RANN) frames with distinct spoofed
originator/target addresses and a maximal lifetime; each frame allocates a new
~100β150 B route entry that survives ~50 days. The table grows without bound
until kmem_map is exhausted β kernel panic: malloc: ... kmemmap too small
or unrecoverable memory-pressure stall.
- Class: resource exhaustion / unbounded allocation (CWE-400). Not memory corruption β there is no OOB write, type confusion or UAF, so there is no privilege-escalation chain. The realistic ceiling is DoS.
- CVSS 3.1 (finding):
CVSS:3.1/AV:A/AC:L/PR:N/UI:N/S:U:C:N/I:N/A:H(adjacent-network, availability-only). - No WiFi HW on guest β runtime trigger not exercisable here; the confirmed primitive is the demonstrated unbounded growth + attacker-controlled lifetime via the faithful harness, on a kernel that ships the vulnerable code.
Fix
fix.diff β a minimal, git apply-able diff addressing both claims at
their root:
- Cap the per-vap route table (closes claim #1). Add a
uint32_t ms_rtcounttostruct ieee80211_mesh_state, enforceIEEE80211_MESH_RT_MAX = 4096inmesh_rt_add_locked()before the kmalloc (returning NULL, which every caller already handles asis_mesh_rtaddfailed+++ drop-frame), increment on insert, decrement inmesh_rt_del()(the single removal point), and zero-init in the mesh-state constructor. 4096 is ~16Γ any plausible mesh-BSS peer count while bounding worst-case per-vap memory to a few hundred KB. - Clamp the lifetime (closes claim #2). In
ieee80211_mesh_rt_update(), clamp the (unsigned-interpreted)new_lifetimetoIEEE80211_MESH_RT_LIFETIME_MAX_MS = 60 000msec (60 s β 12Γ the 5 s default, still bounds the dwell). The cast touint32_tis deliberate: the caller passespreq->preq_lifetime(uint32) into theintparameter, so0xFFFFFFFFarrives as-1and a naive signed>test would let it through; treating it as unsigned clamps it correctly.
The cap is the load-bearing mitigation (it bounds memory regardless of the lifetime value); the clamp is defense-in-depth that also limits per-entry dwell and makes the table self-clean faster between cap hits.
Supersedes / note on the finding's proposal: the finding had no
markdown/Recommended-fix (DB-only), so this is the runner-authored fix. It
targets the choke point (mesh_rt_add_locked, reached by all 8 adders) rather
than each individual HWMP call site, so a single cap closes every PREQ/PREP/RANN
path at once.
Fix validation (Phase 8)
- Baseline (
#0unpatched,vm.sh reset with-src): harness shows 8000/8000 entries added, 0 rejected,rt_lifetime = 0xFFFFFFFF(49.7 d). - Applied
fix.diffto/usr/srcβpatch -p1clean, all 7 hunkssucceeded. - Built single-fix kernel
make -j6 nativekernel KERNCONF=X86_64_GENERICβrc=0(full logfix_build.log). - Installed
/usr/obj/.../kernel.strippedβ/boot/kernel/kernel(bare name),sync; sync; syncbeforevm.sh down(without the explicit sync the loader fails to read the new file β "don't know how to load module 'kernel'" β becausevm.sh downhard-kills before UFS flushes; this is the same loader-pitfall DF-0393 documented). Rebooted β booted as#1: Thu Jul 16 09:56:39 UTC 2026(sha2560bd7465b021bde63d5c5f2c1cb55d3b4a7adf937088750b834300729daebb147). - Patched kernel healthy: boots clean (no panic in serial log/dmesg);
net.wlan.hwmp.*sysctls intact (mesh init not broken); ssh + uptime normal. - Post-fix harness (
harness_fixed, compiled-DFIXEDon the#1patched kernel, embedding the verbatim cap+clamp logic fromfix.diff): 8000 distinct PREQs β 4096 added, 3904 rejected (cap hit);rt_lifetime = 0x0000EA60= 60000 msec (60 s), down from 49.7 days. Both claims refuted. (full output infix_run.log.)
Fix status: FIXED. Clean before/after: unbounded growth + 49.7-day lifetime
on unpatched #0; capped at 4096 entries + 60 s lifetime on single-fix #1.
Note on testability: because the live mesh RX path needs a WiFi radio (absent on this guest), the "after" validation is the faithful harness on the patched kernel (the harness embeds the exact cap+clamp logic from
fix.diff), not a runtime frame injection. The patched kernel compiles, boots, and its mesh subsystem initialises correctly β i.e. the fix introduces no regression and closes the vulnerable logic.
Fix verification
fixedVALIDATED: baseline 8000 routes no cap; patched 4096 cap + 60s lifetime.
BEFORE: 8000/0. AFTER: 4096/3904 rejected, 60s.
Confirmed kernel references
- sys/netproto/802_11/wlan/ieee80211_mesh.c:194
- sys/netproto/802_11/wlan/ieee80211_mesh.c:225
- sys/netproto/802_11/wlan/ieee80211_mesh.c:267
- sys/netproto/802_11/wlan/ieee80211_mesh.c:287
- sys/netproto/802_11/wlan/ieee80211_mesh.c:427
- sys/netproto/802_11/wlan/ieee80211_mesh.c:686
- sys/netproto/802_11/ieee80211_mesh.h:525
- sys/netproto/802_11/wlan/ieee80211_hwmp.c:457
- sys/netproto/802_11/wlan/ieee80211_hwmp.c:1057
- sys/netproto/802_11/wlan/ieee80211_hwmp.c:1097
Detail
Exploit chain
none -- CWE-400 resource exhaustion DoS, not corruption. No escalation.
Evidence (decisive lines)
BEFORE: 8000 routes added, lifetime 49.7d. AFTER: 4096 cap (3904 rejected), lifetime 60s.
PoC changes
Authored: harness.c (verbatim mesh_rt_add/update), fix.diff (cap 4096 + clamp lifetime 60s), VERDICT.md, manifest.json.
Verified recommended fix
(1) Cap ms_rtcount at IEEE80211_MESH_RT_MAX=4096 in mesh_rt_add_locked; (2) clamp lifetime at 60000ms in ieee80211_mesh_rt_update (treat as unsigned). Full diff in findings/poc/DF-0350/fix.diff.
Verdict
REPRODUCED (harness). mesh_rt_add_locked ieee80211_mesh.c:194 kmalloc+TAILQ_INSERT no cap, reached from 8 HWMP sites. preq_lifetime hwmp.c:457 uint32 unvalidated -> rt_lifetime 0xFFFFFFFF=49.7d. Harness: 8000/8000 added, 0 rejected. No WiFi HW.
No comments yet.