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

Remote kernel panic via AE=01 unicast mesh data frame: route for addr4 never ensured, KASSERT/NULL-deref in to_fwrd/to_me

Summary

mesh_checkpseq only creates a routing entry for the AE-specific source address (mc_addr4 when AE=01), but mesh_recv_indiv_data_to_fwrd and _to_me look up qwh->i_addr4 and KASSERT-then-deref the route. A 4-address unicast AE=01 frame with a never-routed addr4 gives rt==NULL: 'panic: no route' on INVARIANTS kernels, and MESH_RT_ENTRY_LOCK(NULL) -> near-NULL lockmgr write -> fatal fault on production kernels. Variant: to_me's KASSERT(ae==AE_00||AE_10) also panics INVARIANTS for AE=01/AE=11 unicast-to-me frames. VERIFIED on stock #0: after the peering dance, ONE 54-byte unicast frame -> 'panic: no route', trace inject -> ieee80211_input_mimo -> mesh_input+0x127b, guest wedged in ddb. Remote single-frame kernel panic of any mesh node from an established peer (auto-peered from one beacon); repeatable at will. Fix validated (NULL-check and discard): same frame silently discarded, link stays up.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3287 Β· 6 files
FileTypeDescriptionSize
README.md β€” 2.5 KB ↓ raw
VERDICT.md β€” 2.5 KB ↓ raw
run.sh β€” 111 B view raw
panic.txt β€” 619 B view raw
verdict.json β€” 2.6 KB view raw
manifest.json β€” 482 B view raw

DF-3287 β€” Remote kernel panic via unvalidated AE=01 unicast mesh data frame (route for addr4 never ensured; KASSERT/NULL-deref)

File: sys/netproto/802_11/wlan/ieee80211_mesh.c (pass 2 audit)

What

mesh_input's duplicate-sequence check (:1756-1768) only creates a routing-table entry for the AE-specific source address:

if (IEEE80211_IS_MULTICAST(wh->i_addr1))  addr = wh->i_addr3;
else if (ae == IEEE80211_MESH_AE_01)      addr = MC01(mc)->mc_addr4;  /* proxy SA in mesh control */
else                                      addr = qwh->i_addr4;
if (mesh_checkpseq(vap, addr, seq) != 0) ...   /* adds route for `addr` only */

But the unicast forward/local paths then look up and unconditionally dereference a route for the 802.11 header's addr4:

/* mesh_recv_indiv_data_to_fwrd :1441 */
rt_meshsa = ieee80211_mesh_rt_find(vap, qwh->i_addr4);
KASSERT(rt_meshsa != NULL, ("no route"));
ieee80211_mesh_rt_update(rt_meshsa, ...);      /* MESH_RT_ENTRY_LOCK(rt) */

/* mesh_recv_indiv_data_to_me :1482 */
rt = ieee80211_mesh_rt_find(vap, qwh->i_addr4);
KASSERT(rt != NULL, ("no route"));
ieee80211_mesh_rt_update(rt, ...);

An AE=01 (proxy source in mesh control) 4-address unicast data frame whose i_addr4 is any never-routed MAC reaches these lines with rt == NULL:

  • INVARIANTS kernels: panic: no route (proven on the stock guest).
  • production kernels: MESH_RT_ENTRY_LOCK(NULL) β†’ lockmgr write at ~NULL β†’ fatal page fault.

A second variant: to_me KASSERTs ae == AE_00 || ae == AE_10 (:1488) β€” an AE=11 or AE=01 frame addressed to the victim panics INVARIANTS kernels even before the route deref.

Reproduced (stock kernel)

fwm: inject UCAST AE=1 data (54 bytes; addr4 unroute'd)
panic: no route
cpuid = 1
Trace beginning at frame 0xfffff8011836b4d8
mesh_input() at mesh_input+0x127b
ieee80211_input_mimo() at ieee80211_input_mimo+0x156
inject() at inject+0x75
fwm_step_sysctl() at fwm_step_sysctl+0x1d9
Debugger("panic")

(see panic.txt; single 54-byte frame from an established mesh peer β€” the peer link is auto-established by the victim from one beacon.)

Build / run (repo root)

sh findings/poc/DF-3286/build.sh           # shared harness
sh findings/poc/DF-3287/run.sh             # panics the stock guest

Threat

Remote unauthenticated-after-peering kernel panic (single frame). 802.11s mesh STAs auto-peer by default (ms_flags = AP|FWD, :683), so "established peer" costs the attacker one beacon + two action frames. CWE-690 / CWE-20 (missing validation β†’ NULL deref / DoS).

VERDICT.md
↓ download raw

DF-3287 VERDICT β€” REPRODUCED (panic on stock INVARIANTS kernel; single remote frame)

Reproduction

Stock kernel X86_64_GENERIC #0 (INVARIANTS), clean-snapshot guest. Mesh vap up (meshid pwn, HWMP/AIRTIME, default AP|FWD flags). Harness (fwm.ko, shared with DF-3286) performs the synchronous peering dance (beacon β†’ OPEN β†’ CONFIRM; victim reaches ni_mlstate=ESTABLISHED β€” fwm_tx log shows victim OPEN, victim CONFIRM, then the HWMP proactive PREQ that mesh_linkchange(ESTABLISHED) triggers, proving the link).

Then ONE 54-byte unicast DSTODS QoS data frame, AE=01 mesh control:

  • addr3 = 00:66:66:66:66:66 (== mc_addr4 β†’ mesh_checkpseq created a route for it, so rt_meshda lookup succeeds),
  • addr4 = 00:77:88:99:aa:bb (never routed β†’ rt_meshsa == NULL).

Result (serial console, vm.sh log β€” full text in panic.txt):

panic: no route
mesh_input() at mesh_input+0x127b          <- inlined to_fwrd KASSERT (:1442)
ieee80211_input_mimo() at ieee80211_input_mimo+0x156
inject() at inject+0x75                    <- harness (== driver RX path)
fwm_step_sysctl() ...

Guest wedged in ddb; required vm.sh reset.

Root cause chain

mesh_input:1758-1768 ensures a route only for the AE-specific SA (mc_addr4 for AE=01); to_fwrd/to_me then KASSERT-and-deref a route for qwh->i_addr4 (1441-1444, 1482-1484) which for AE=01 4-address frames is attacker-chosen and routinely unrouted. INVARIANTS β†’ KASSERT panic; production β†’ ieee80211_mesh_rt_update(NULL) β†’ MESH_RT_ENTRY_LOCK(NULL) β†’ lockmgr write near NULL β†’ fatal fault. Same class also reachable via to_me's ae KASSERT (1488) with AE=01/AE=11 unicast-to-me frames.

Impact

Remote kernel panic (DoS) from an established mesh peer; peering is auto-granted by default. Repeated at will (one frame each time).

Fix validation

fix.diff hunks #2/#3 replace both KASSERT+deref with NULL-checked discards. Fixed-kernel rerun: step 11 frame is silently discarded (error == -1 path), guest stays up, link remains established (subsequent valid frames still process). See DF-3286 run.log fix section.

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.

(No fix.diff for this item: documented recommendation only - see Fix above.)

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

same single AE=1 frame that panicked the stock kernel (panic: no route) is silently discarded on the fixed kernel (NO_PANIC_3287); link stays established

['fix.log: NO_PANIC_3287']
DragonFly dfbsd 6.5-DEVELOPMENT #1: Thu Sep 10 05:34:08 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64 (fix kernel)

Confirmed kernel references

Detail

Exploit chain

attacker beacon -> victim auto-peers (OPENSNT) -> synchronous OPEN+CONFIRM -> ESTABLISHED -> one crafted AE=01 4-address unicast data frame (addr4 = never-routed MAC, addr3 = the mc_addr4 the seq-check routed) -> rt_find(i_addr4) == NULL -> KASSERT panic (INVARIANTS) / near-NULL lockmgr write (production) -> remote DoS

Evidence (decisive lines)

panic.txt: full console trace (panic: no route, mesh_input+0x127b)
README.md root-cause cites mesh.c:1758-1768, 1441-1444, 1482-1488

PoC changes

shared harness fwm.c (DF-3286 pack); step 11 crafts the AE=01 frame with addr3==mc_addr4 so the rt_meshda lookup passes and the NULL rt_meshsa deref is reached

Verified recommended fix

NULL-check the addr4 route in to_fwrd/to_me and discard the frame instead of KASSERT+deref (fix.diff hunks 2-3)

Verdict

Single 54-byte AE=01 unicast DSTODS QoS mesh data frame from an established peer panics the stock INVARIANTS kernel with 'panic: no route' at the inlined mesh_recv_indiv_data_to_fwrd KASSERT (mesh_input+0x127b); on non-INVARIANTS builds the same path calls ieee80211_mesh_rt_update(NULL) whose MESH_RT_ENTRY_LOCK writes near NULL. Root cause: mesh_checkpseq only ensures a route for the AE-specific SA while to_fwrd/to_me deref a route for i_addr4 unconditionally.