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

Remote kernel panic: hwmp_send_action KASSERT/NULL-deref when a proactive/propagated/intermediate PREP is sent along a never-validated zero nexthop

Summary

A single crafted HWMP PREQ action frame (target=ff:ff:ff:ff:ff:ff with TO bit, origseq=0, metric=0) from an auto-peered mesh neighbor makes hwmp_recv_preq's proactive branch send a PREP to rtorig->rt_nexthop of an originator route that exists but was never marked VALID - a fresh M_ZERO route whose nexthop is 00:00:00:00:00:00. hwmp_send_action resolves find_txnode(00:00:00:00:00:00) to NULL and hits KASSERT(ni != NULL) - panic on INVARIANTS kernels; ic = ni->ni_ic NULL-derefs on production. Same sink reachable via PREP propagation and intermediate PREP reply. VERIFIED on stock INVARIANTS guest with the fwm harness (beacon+OPEN+CONFIRM dance then one injected PREQ): 'panic: null node', backtrace hwmp_send_action+0x4ee <- hwmp_recv_action_meshpath <- mesh_input <- ieee80211_input_mimo, guest wedged in ddb. Any default-config mesh vap (AP|FWD by default); adjacent-network attacker with auto-granted peering kills the victim kernel with one 65-byte frame, repeatable - remote DoS of every mesh node in range. Not a write primitive - DoS ceiling. Fix validated (NULL-check + ENOENT in hwmp_send_action, rebuilt kernel): same PoC twice -> no panic, PREQ still forwarded.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3327 Β· 14 files
FileTypeDescriptionSize
README.md β€” 3.1 KB ↓ raw
VERDICT.md β€” 3.3 KB ↓ raw
fwm.c β€” 26.9 KB view raw
Makefile β€” 150 B ↓ download
build.sh β€” 87 B view raw
run.sh β€” 111 B view raw
build.log β€” 5.6 KB view raw
run.log β€” 1.2 KB view raw
panic.txt β€” 762 B view raw
env.txt β€” 300 B view raw
fix.diff β€” 764 B view raw
fix_build.log β€” 364 B view raw
fix_run.log β€” 164 B view raw
verdict.json β€” 4.1 KB view raw

DF-3327 β€” Remote kernel panic in hwmp_send_action: PREP sent along a never-validated (zero) nexthop

File: sys/netproto/802_11/wlan/ieee80211_hwmp.c Severity: High (unauthenticated adjacent-network DoS, default-config mesh vap) Verdict: REPRODUCED β€” panic: null node on the stock INVARIANTS kernel; fix validated.

What

A single broadcast-target HWMP PREQ action frame makes hwmp_recv_preq() emit a proactive PREP addressed to the nexthop of an originator route that exists but was never marked VALID. Such a route is M_ZERO-allocated (ieee80211_mesh.c:203-215), so its nexthop is 00:00:00:00:00:00. hwmp_send_action() resolves that address with ieee80211_mesh_find_txnode(), which returns NULL for an unrouted address (ieee80211_mesh.c:1028-1037), then hits KASSERT(ni != NULL, ("null node")) (ieee80211_hwmp.c:636) β€” panic on INVARIANTS kernels; on production kernels the next line (ic = ni->ni_ic, ieee80211_hwmp.c:637) is a NULL deref β†’ fatal page fault. Same sink is reachable from PREP propagation (ieee80211_hwmp.c:1558) and the intermediate PREP reply (ieee80211_hwmp.c:1303).

Why the route stays INVALID: for a fresh originator route hr_seq == 0; a PREQ with origseq == 0 and metric not better than 0 satisfies neither update branch (ieee80211_hwmp.c:1090-1102) nor the discard branch (:1103-1106), so processing falls through with the route INVALID, yet the proactive branch (:1216-1255) still sends a PREP to rtorig->rt_nexthop == 00:00:00:00:00:00.

Reproduce

Requires the repo-root working dir and the QEMU guest (see dfbsd-qemu/).

sh findings/poc/DF-3327/build.sh     # compile extended fwm.ko in guest
sh findings/poc/DF-3327/run.sh       # boot mesh vap, peer, fire step 30

Expected (stock kernel): serial console shows

fwm: inject HWMP PREQ origseq=0 targ=bc TO (proactive PREP to zero nexthop)
panic: null node
hwmp_send_action() at hwmp_send_action+0x4ee
hwmp_recv_action_meshpath() at ...
Debugger("panic")

and the guest wedges in ddb (reset afterwards: dfbsd-qemu/vm.sh reset with-src).

Fix validation

fix.diff adds a NULL check in hwmp_send_action() (drop + ENOENT). Applied in-guest, make -j6 nativekernel KERNCONF=X86_64_GENERIC + make installkernel, reboot into kernel #1, same PoC (twice): no panic, guest stays up, PREQ still fully processed (forwarded broadcast PREQ visible in kern.fwm_txlog). See fix_run.log, fix_build.log.

Harness

fwm.c extends the DF-3286/3287 shared software-radio KLD. New step 30 builds the 65-byte action frame:

802.11 hdr | cat=13 (MESH) code=1 (HWMP) | PREQ IE (130) len=37:
flags=0 hop=0 ttl=32 id=0x41414141 orig=02:00:00:00:e0:01 (fresh)
origseq=0 lifetime=5000 metric=0 tcount=1
target: flags=0x01 (TO) addr=ff:ff:ff:ff:ff:ff seq=0

Peer establishment (step 4: beacon + OPEN + CONFIRM) is required because ieee80211_recv_action() gates CAT_MESH frames on an ESTABLISHED link (ieee80211_action.c:243-256); peering is auto-granted on default-config mesh vaps (ieee80211_mesh.c:683 β€” MESHFLAGS_AP|MESHFLAGS_FWD).

VERDICT.md
↓ download raw

DF-3327 VERDICT β€” REPRODUCED (remote kernel panic, single HWMP PREQ action frame)

Runs

run kernel input result
baseline stock X86_64_GENERIC #0 (INVARIANTS), clean with-src snapshot step 4 (peering dance) + one step-30 PREQ panic: null node, backtrace hwmp_send_action β†’ hwmp_recv_action_meshpath β†’ mesh_recv_mgmt β†’ mesh_input β†’ ieee80211_input_mimo, guest wedged in ddb (panic.txt, run.log/console)
fixed #1 with fix.diff (NULL check in hwmp_send_action) same, step 30 fired twice no panic, NO_PANIC_HERE / STILL_UP_AFTER_SECOND_PREQ printed, RC=0, guest up; PREQ still fully processed (forwarded broadcast PREQ in txlog, fix_run.log)

Why it reproduces (path:line)

  1. ieee80211_action.c:243-256 β€” CAT_MESH action frames dispatch once the attacker link is ESTABLISHED (auto-peered via the fwm dance; no user auth on default open mesh).
  2. ieee80211_hwmp.c:417-474 β€” the PREQ IE (len=37, tcount=1) parses cleanly; verify_mesh_preq_len passes (:328).
  3. ieee80211_hwmp.c:1055-1079 β€” rt_add(orig 02:00:00:00:e0:01) β†’ M_ZERO route (ieee80211_mesh.c:203-215): INVALID, nexthop 0, metric 0, hr_seq 0.
  4. ieee80211_hwmp.c:1089-1102 β€” origseq=0 is neither GT the stored 0 nor (equal-seq ∧) better-metric β‡’ no update, route stays INVALID. :1103-1106 β€” neither discard clause fires (no target route, originator route not VALID) β‡’ processing continues.
  5. :1216-1242 β€” target is broadcast + TO bit β‡’ proactive branch; (rt_flags & VALID) == 0 β‡’ "reply with PREP" β‡’ :1254 hwmp_send_prep(vap, rtorig->rt_nexthop /* 00:00:00:00:00:00 */, &prep).
  6. ieee80211_hwmp.c:627 β€” ieee80211_mesh_find_txnode(vap, 00:…:00) β†’ no route for the zero MAC β‡’ NULL (ieee80211_mesh.c:1030-1033).
  7. ieee80211_hwmp.c:636 β€” KASSERT(ni != NULL, ("null node")) β‡’ panic (stock INVARIANTS kernel). On production kernels :637 ic = ni->ni_ic is a NULL-page deref β‡’ fatal fault either way.

Same sink, two more remote routes: PREP propagation (ieee80211_hwmp.c:1521-1558, rtorig exists-but-invalid) and the intermediate PREP reply (:1271-1303).

Impact

Remote (adjacent, unauthenticated after auto-peering) kernel panic of any default-config mesh node, one frame per panic, repeatable at will. Not a memory-corruption primitive (NULL deref), so DoS is the ceiling.

Fix validation

fix.diff (NULL check + drop + ENOENT in hwmp_send_action) β€” applied in guest /usr/src, make -j6 nativekernel KERNCONF=X86_64_GENERIC (0 errors), make installkernel, reboot, same PoC twice: panic gone, guest healthy, PREQ still forwarded (behavior preserved). Baseline vs patched transcript in run.log / fix_run.log.

Notes

  • The panic occurred inside the sysctl kern.fwm_step=30 write (synchronous RX injection), matching the ieee80211_input_mimo frames in the backtrace.
  • Guest reset to clean with-src snapshot after both runs.

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

fixed
baseline reproduced→ patch + rebuild →patched clean

fix.diff applied to guest /usr/src, make -j6 nativekernel (clean), make installkernel, reboot into #1: the same step-30 PREQ fired twice causes no panic (RC=0, guest stays up) and normal PREQ processing/forwarding is preserved (txlog shows the forwarded broadcast PREQ).

findings/poc/DF-3327/fix_build.log, findings/poc/DF-3327/fix_run.log
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT #1: Thu Sep 10 22:05:55 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

Confirmed kernel references

Detail

Exploit chain

fwm software-radio injects 65-byte action frame after the beacon/OPEN/CONFIRM auto-peering dance -> ieee80211_input_mimo -> mesh_input -> hwmp_recv_action_meshpath -> verify+parse PREQ -> hwmp_recv_preq: rt_add(orig) M_ZERO route stays INVALID (origseq==stored 0, metric not better) -> proactive branch hwmp_send_prep(nexthop=00:00:00:00:00:00) -> hwmp_send_action: find_txnode returns NULL -> KASSERT 'null node' panic (guest wedged in ddb). One frame per panic, repeatable.

Evidence (decisive lines)

["panic.txt: 'panic: null node' + backtrace hwmp_send_action+0x4ee <- hwmp_recv_action_meshpath <- mesh_input <- ieee80211_input_mimo, preceded by 'fwm: inject HWMP PREQ origseq=0 targ=bc TO'", 'run.log: baseline session transcript (ssh hung at step-30 sysctl, RC=124)', 'fix_build.log: make -j6 nativekernel + installkernel of fix.diff (kernel #1)', 'fix_run.log: same PoC twice on fixed kernel - NO_PANIC_HERE / STILL_UP_AFTER_SECOND_PREQ, RC=0, guest up, forwarded PREQ still in txlog']

PoC changes

Extended the DF-3286 shared fwm.c harness with step 30 (PREQ builder), plus steps 31/32 for the sibling DF-3328 PoC and kern.fwm_ttl. The seed sketch in this finding did not exist; the frame layout was derived from ieee80211_mesh.h IE constants (PREQ=130, len=26+11, TCNT offset 27).

Verified recommended fix

NULL-check the find_txnode result in hwmp_send_action and drop the frame (return ENOENT); see fix.diff.

Verdict

A single broadcast-target HWMP PREQ action frame (origseq=0, metric=0, target=ff:ff:ff:ff:ff:ff with the TO bit) from an auto-peered mesh neighbor leaves the originator route INVALID with an all-zero nexthop, and hwmp_recv_preq's proactive branch (ieee80211_hwmp.c:1241-1254) still sends a PREP to that nexthop; hwmp_send_action resolves ieee80211_mesh_find_txnode(00:00:00:00:00:00) to NULL and hits KASSERT(ni != NULL) (:636) on INVARIANTS kernels (NULL-deref at :637 on production). Reproduced as 'panic: null node' with a hwmp_send_action backtrace on the stock INVARIANTS kernel; fix.diff (NULL check + drop) validated: no panic, guest healthy, PREQ still forwarded.