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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0350 Β· 13 files
FileTypeDescriptionSize
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
README.md readme human-facing build/run/expected + file index
↓ download 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. -DFIXED toggles 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 #0 unpatched.
  • fix_build.log β€” single-fix kernel build (full).
  • fix_run.log β€” FIXED harness on #1 patched kernel.
  • env.txt β€” guest environment.
  • manifest.json β€” artifact catalog.
VERDICT.md verdict full narrative: line-by-line trace, mechanism, impact ceiling, fix, validation
↓ download raw

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:

  1. Cap the per-vap route table (closes claim #1). Add a uint32_t ms_rtcount to struct ieee80211_mesh_state, enforce IEEE80211_MESH_RT_MAX = 4096 in mesh_rt_add_locked() before the kmalloc (returning NULL, which every caller already handles as is_mesh_rtaddfailed++ + drop-frame), increment on insert, decrement in mesh_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.
  2. Clamp the lifetime (closes claim #2). In ieee80211_mesh_rt_update(), clamp the (unsigned-interpreted) new_lifetime to IEEE80211_MESH_RT_LIFETIME_MAX_MS = 60 000 msec (60 s β€” 12Γ— the 5 s default, still bounds the dwell). The cast to uint32_t is deliberate: the caller passes preq->preq_lifetime (uint32) into the int parameter, so 0xFFFFFFFF arrives as -1 and 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)

  1. Baseline (#0 unpatched, vm.sh reset with-src): harness shows 8000/8000 entries added, 0 rejected, rt_lifetime = 0xFFFFFFFF (49.7 d).
  2. Applied fix.diff to /usr/src β€” patch -p1 clean, all 7 hunks succeeded.
  3. Built single-fix kernel make -j6 nativekernel KERNCONF=X86_64_GENERIC β†’ rc=0 (full log fix_build.log).
  4. Installed /usr/obj/.../kernel.stripped β†’ /boot/kernel/kernel (bare name), sync; sync; sync before vm.sh down (without the explicit sync the loader fails to read the new file β€” "don't know how to load module 'kernel'" β€” because vm.sh down hard-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 (sha256 0bd7465b021bde63d5c5f2c1cb55d3b4a7adf937088750b834300729daebb147).
  5. Patched kernel healthy: boots clean (no panic in serial log/dmesg); net.wlan.hwmp.* sysctls intact (mesh init not broken); ssh + uptime normal.
  6. Post-fix harness (harness_fixed, compiled -DFIXED on the #1 patched kernel, embedding the verbatim cap+clamp logic from fix.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 in fix_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

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED: baseline 8000 routes no cap; patched 4096 cap + 60s lifetime.

BEFORE: 8000/0. AFTER: 4096/3904 rejected, 60s.
↓ fix.diffDragonFly 6.5-DEVELOPMENT #1: Thu Jul 16 09:56:39 UTC 2026

Confirmed kernel references

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.