# 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`

```c
/* 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`:

```c
// 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).
