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

cac_timeout assigns dfs->newchan (may be NULL) to iv_des_chan β€” downstream hostap/adhoc/mesh dereference NULL via IEEE80211_IS_CHAN_RADAR

Summary

dfs.c:154: vap->iv_des_chan = dfs->newchan with author comment "may be NULL thats ok" β€” INCORRECT. dfs->newchan comes from ieee80211_dfs_pickchannel :432-447 which returns NULL when all channels radar-marked. IEEE80211_CHAN_ANYC is (struct ieee80211_channel*)0xffff _ieee80211.h:157-158 NOT NULL. Downstream hostap.c:217-218 tests iv_des_chan != IEEE80211_CHAN_ANYC then dereferences IEEE80211_IS_CHAN_RADAR(iv_des_chan) which expands to iv_des_chan->ic_state at byte offset 0x0A of struct ieee80211_channel. NULL != 0xffff = true so guard passes NULL->ic_state reads VA 0x0A page fault. Same pattern in adhoc.c:170-171 mesh.c:753-754 regdomain.c:425-427 scan_sta.c:418-419. Trigger: hostap vap on DFS 5GHz channel during CAC with radar on bsschan and all alternative channels also radar-marked. Radar event from driver PHY detector or root sysctl net.wlan.X.radar=1. Impact: hard kernel panic NULL-deref DoS. Fix: assign IEEE80211_CHAN_ANYC when dfs->newchan is NULL.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0713 Β· 12 files
FileTypeDescriptionSize
trigger.sh trigger-source reachability-check + trigger script (shell) 3.3 KB view raw
build.sh build-script no-op build (shell-only PoC) 328 B view raw
run.sh run-script copies trigger to guest, runs as maxx 501 B view raw
fix.diff suggested-fix NULL->IEEE80211_CHAN_ANYC conversion at ieee80211_dfs.c:154 732 B view raw
VERDICT.md verdict full code-trace analysis: bug real, unreachable on guest 7.3 KB ↓ raw
README.md readme summary, build/run, preconditions 1.6 KB ↓ raw
run.log run-log baseline (#0 kernel) trigger run β€” unreachable 838 B view raw
fix_run.log run-log patched (#1 kernel) trigger run β€” unreachable 838 B view raw
fix_build.log build-log full kernel build output with fix applied (NK_DONE rc=0) 5.6 MB ↓ download
env.txt environment uname, cc version, pci devices, wlan sysctls 530 B view 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 summary, build/run, preconditions
↓ download raw

DF-0713 β€” cac_timeout NULL-deref

Summary

ieee80211_dfs.c:154 assigns dfs->newchan (which may be NULL, from ieee80211_dfs_pickchannel returning NULL when all channels are radar-marked) to vap->iv_des_chan without converting NULL to the IEEE80211_CHAN_ANYC (0xffff) sentinel. Downstream hostap/adhoc/mesh/ scan_sta/regdomain code guards with iv_des_chan != IEEE80211_CHAN_ANYC (not != NULL), so NULL passes the guard and IEEE80211_IS_CHAN_RADAR(NULL) dereferences NULL->ic_state (offset 0x0A) β†’ kernel panic.

Build

./build.sh    # no compilation (shell-only PoC)

Run

./run.sh      # copies trigger.sh to guest, runs as maxx

Expected (on a system WITH DFS-capable wireless hardware)

Kernel panic β€” NULL pointer dereference in IEEE80211_IS_CHAN_RADAR / iv_des_chan->ic_freq during the SCAN state transition after cac_timeout sets iv_des_chan = NULL.

Expected (on this guest β€” no wireless hardware)

The trigger script reports "cannot trigger on this guest (no wireless hardware)" because no wlan vap can be created without a parent radio device. The bug is confirmed real by source-level trace (see VERDICT.md).

Preconditions

  • A DFS-capable 802.11 wireless NIC (e.g. Atheros ath(4) in 5GHz DFS channels)
  • A hostap/adhoc/mesh vap created on a DFS channel with CAC in progress
  • Radar detected on the bss channel while CAC is pending
  • All alternative channels also radar-marked (so pickchannel returns NULL)
  • The net.wlan.N.radar sysctl (root-writable) can simulate the radar event

Fix

See fix.diff β€” convert NULL to IEEE80211_CHAN_ANYC at the assignment site.

VERDICT.md verdict full code-trace analysis: bug real, unreachable on guest
↓ download raw

DF-0713 β€” cac_timeout NULL-deref (ieee80211_dfs.c)

Verdict

INCONCLUSIVE β€” bug confirmed REAL by code-level trace, but NOT REACHABLE on this guest (no DFS-capable 802.11 wireless hardware). The vulnerable code is compiled into the running kernel (device wlan in X86_64_GENERIC, ieee80211_dfs.c sysctls net.wlan.cac_timeout / net.wlan.nol_timeout are live), but the trigger path requires a DFS-capable wireless NIC to create a wlan vap and enter CAC state β€” which the QEMU guest lacks.

The bug (confirmed by source trace)

File: sys/netproto/802_11/wlan/ieee80211_dfs.c:154

/* NB: dfs->newchan may be NULL, that's ok */     // line 153 β€” INCORRECT comment
vap->iv_des_chan = dfs->newchan;                    // line 154 β€” assigns NULL

The author comment "may be NULL, that's ok" is wrong. dfs->newchan comes from ieee80211_dfs_pickchannel(ic) (ieee80211_dfs.c:366), which returns NULL when all candidate channels are radar-marked (ieee80211_dfs.c:447). The sentinel for "no channel" is not NULL β€” it is IEEE80211_CHAN_ANYC, defined as ((struct ieee80211_channel *) 0xffff) at sys/netproto/802_11/_ieee80211.h:157-158.

Every downstream consumer of iv_des_chan guards with != IEEE80211_CHAN_ANYC, not != NULL. Since NULL != 0xffff is true, the guard passes, and the subsequent IEEE80211_IS_CHAN_RADAR(iv_des_chan) macro (_ieee80211.h:345-346) dereferences iv_des_chan->ic_state β€” which is at byte offset 0x0A of struct ieee80211_channel (see _ieee80211.h:140-152: ic_flags u32 @0, ic_freq u16 @4, ic_ieee u8 @6, ic_maxregpower i8 @7, ic_maxpower i8 @8, ic_minpower i8 @9, ic_state u8 @10). Reading VA 0x000000000000000A (in the unmapped NULL page) β†’ page fault β†’ kernel panic.

iv_des_chan is initialized to IEEE80211_CHAN_ANYC at vap creation (ieee80211.c:650); ieee80211_dfs.c:154 is the only site that breaks this invariant by assigning a raw (possibly-NULL) channel pointer.

Trigger chain (requires DFS-capable 802.11 radio)

  1. Create a hostap/adhoc/mesh vap on a DFS 5GHz channel (requires wlandev parent).
  2. CAC starts β†’ ieee80211_dfs_cac_start arms cac_timer (ieee80211_dfs.c:191).
  3. Radar detected on ic_bsschan while CAC pending: - ieee80211_dfs_notify_radar(ic, chan) (ieee80211_dfs.c:300) - chan == ic->ic_bsschan (:358) β†’ dfs->newchan = ieee80211_dfs_pickchannel(ic) (:366) - If all channels radar-marked β†’ pickchannel returns NULL (:447) - callout_pending(&dfs->cac_timer) is true (:372) β†’ schedules cac_timeout immediately (:374-376)
  4. cac_timeout fires (:128): - vap->iv_state == IEEE80211_S_CAC β†’ passes guard (:137) - IEEE80211_IS_CHAN_RADAR(ic->ic_curchan) β†’ true (:144) - vap->iv_des_chan = dfs->newchan (= NULL) (:154) ← THE BUG - ieee80211_new_state(vap, IEEE80211_S_SCAN, 0) (:156)
  5. State transition to IEEE80211_S_SCAN triggers the NULL deref in one of: - ieee80211_hostap.c:217-218: iv_des_chan != ANYC && !IEEE80211_IS_CHAN_RADAR(iv_des_chan) β†’ NULL->ic_state - ieee80211_adhoc.c:170-171: same pattern - ieee80211_mesh.c:753-754: same pattern - ieee80211_scan_sta.c:418-419: iv_des_chan != ANYC && c->ic_freq != iv_des_chan->ic_freq β†’ NULL->ic_freq - ieee80211_regdomain.c:425-427: iv_des_chan != ANYC β†’ iv_des_chan->ic_freq

Impact

Kernel panic / DoS (NULL pointer dereference). No memory corruption, no escalation path β€” the dereference reads from the unmapped NULL page (VA 0x0A), causing an immediate fault. A NULL deref cannot yield uid=0.

Privilege boundary: reaching the trigger requires the ability to create a wlan vap on a DFS channel and inject a radar event. On a real system with a DFS-capable NIC, the net.wlan.N.radar sysctl (ieee80211_dragonfly.c:477-480) is writable by root and calls ieee80211_dfs_notify_radar directly β€” so root (or a process with net.wlan.N.radar write access) can trigger it. An unprivileged user cannot create vaps or write the radar sysctl. The finding's CVSS PR:H (privileged) is appropriate.

Reachability on this guest

Check Result
device wlan in X86_64_GENERIC βœ… (sys/config/X86_64_GENERIC:258)
ieee80211_dfs.c compiled in βœ… (sysctls net.wlan.cac_timeout=60, net.wlan.nol_timeout=1800 are live)
ath / ath_dfs in kernel βœ… (kldstat -v shows ath_dfs, wlan, etc.)
Wireless PCI device present ❌ (pciconf -l shows only virtio + hostbridge + ISA + ATA + VGA)
wlan vap creatable ❌ (ifconfig wlan0 create wlandev β†’ "must specify a parent device")
net.wlan.N.radar sysctl ❌ (only created when a DFS-capable vap exists, ieee80211_dragonfly.c:477)

Conclusion: The vulnerable code is live in the kernel but the runtime path (ieee80211_dfs_cac_start β†’ cac_timeout β†’ iv_des_chan=NULL β†’ SCAN deref) requires a DFS-capable 802.11 radio device. The QEMU guest has no wireless hardware, so the bug cannot be triggered. This is a latent bug on this guest β€” real on any DragonFlyBSD system with a DFS-capable wireless NIC (e.g. Atheros ath(4) in 5GHz).

Escalation

None. NULL pointer dereference β†’ kernel panic (DoS). No write primitive, no corruption. The dereference reads from the unmapped NULL page; it cannot be shaped into an arbitrary write or privilege escalation.

PoC changes

Authored trigger.sh from scratch (no prior PoC existed in the folder β€” the finding was filed DB-only with no evidence pack). The script: 1. Checks for wlan interfaces 2. Attempts to create a wlan vap (fails β€” no parent device) 3. Checks for net.wlan.N.radar sysctl (absent β€” no DFS vap) 4. Reports unreachable + points to this VERDICT for the code trace

On a real DFS system, the trigger would be: create hostap vap on DFS 5GHz channel β†’ start CAC β†’ sysctl net.wlan.N.radar=1 on every channel (mark all radar) β†’ wait for cac_timeout β†’ panic.

Fix

fix.diff β€” one-line change at ieee80211_dfs.c:154:

// Before (buggy):
vap->iv_des_chan = dfs->newchan;

// After (fixed):
vap->iv_des_chan = (dfs->newchan != NULL) ?
    dfs->newchan : IEEE80211_CHAN_ANYC;

This preserves the invariant that iv_des_chan is either a valid channel pointer or the IEEE80211_CHAN_ANYC (0xffff) sentinel, which all downstream guards check for. When pickchannel returns NULL (no free channels), the vap transitions to SCAN state with iv_des_chan == IEEE80211_CHAN_ANYC, and the downstream guards correctly skip the "already have a channel" fast-path and initiate a normal scan.

Fix validation

  • git apply --check: rc=0 (diff applies cleanly)
  • Kernel build: make -j6 nativekernel KERNCONF=X86_64_GENERIC β†’ NK_DONE rc=0 (full build log in fix_build.log, 35454 lines)
  • Patched kernel boots: kern.version β†’ 6.5-DEVELOPMENT #1: Mon Jul 13 11:41:58 UTC 2026
  • Runtime before/after: not_testable β€” the bug cannot be triggered on this guest (no wireless hardware), so no runtime before/after comparison is possible. The fix is validated at the code level: it converts the NULL that pickchannel may return into the IEEE80211_CHAN_ANYC sentinel that all downstream guards check, closing the NULL-deref path. See fix_run.log (patched kernel, same "unreachable" result as run.log baseline).

Fix verification

not_testable
baseline no→ patch + rebuild →patched clean

not_testable: no WiFi HW -> trigger path unreachable. fix.diff applies+compiles+boots; source trace confirms fix closes NULL-deref path.

Both #0 and #1: trigger -> 'no wireless hardware' EXIT=2. Build: NK_DONE rc=0.
↓ fix.diffDragonFly 6.5-DEVELOPMENT #1: Mon Jul 13 11:41:58 UTC 2026

Confirmed kernel references

Detail

Exploit chain

none. NULL deref -- reads from unmapped NULL page, immediate panic. No write primitive.

Evidence (decisive lines)

trigger.sh: no wlan interfaces, no parent device, no net.wlan.N.radar sysctl. Source trace confirms NULL assignment at :154 and all 5 downstream guards use != IEEE80211_CHAN_ANYC (not != NULL).

PoC changes

Authored trigger.sh, build.sh, run.sh, VERDICT.md, README.md, fix.diff (ternary: newchan!=NULL ? newchan : IEEE80211_CHAN_ANYC), manifest.json from scratch.

Verified recommended fix

At ieee80211_dfs.c:154, change to vap->iv_des_chan = (dfs->newchan != NULL) ? dfs->newchan : IEEE80211_CHAN_ANYC;. Matches finding proposal. Full git-apply-able diff in findings/poc/DF-0713/fix.diff.

Verdict

INCONCLUSIVE -- bug confirmed REAL by code-level trace but NOT REACHABLE on this guest (no WiFi hardware). ieee80211_dfs.c:154 assigns dfs->newchan (may be NULL from pickchannel:447 when all channels radar-marked) to vap->iv_des_chan without converting NULL to IEEE80211_CHAN_ANYC sentinel. All downstream guards check != IEEE80211_CHAN_ANYC (NOT != NULL), so NULL passes guard and IEEE80211_IS_CHAN_RADAR(NULL) dereferences NULL at offset 0x0A -> panic. Code IS compiled into kernel (device wlan in GENERIC) but trigger needs DFS-capable WiFi radio.