# DF-5070 Verification Verdict

**Severity:** Medium · **Impact class:** panic (unprivileged local DoS)
**Method:** sink-level reproduction on the stock guest kernel
(DF-4986/DF-5012/DF-5024 house precedent) + fix compile-validation
against the guest's /usr/src.

## Verdict: REPRODUCED (panic), fix VALIDATED

### The bug (source chain, all lines verified)

`ath_ioctl_spectral()` validates `ad_in_size` for
`SPECTRAL_CONTROL_SET_PARAMS` but never checks that the `ATH_DIAG_IN` flag
was set or that `indata != NULL`:

- `if_ath_spectral.c:184` — `void *indata = NULL;`
- `if_ath_spectral.c:197` — `indata` is only allocated when
  `ad->ad_id & ATH_DIAG_IN`;
- `if_ath_spectral.c:233-236` — the SET_PARAMS arm checks ONLY
  `insize < sizeof(HAL_SPECTRAL_PARAM)` (`ad_in_size` is a fully
  user-controlled struct field, independent of the flag);
- `if_ath_spectral.c:237` — `pe = (HAL_SPECTRAL_PARAM *) indata;` → NULL;
- `if_ath_spectral.c:238` — `ath_hal_spectral_configure(sc->sc_ah, pe)`
  → `ar5416ConfigureSpectralScan()` reads `ss->ss_fft_period`
  (`ar5416_spectral.c:92`) / `ss_period` (`:97`) and
  `ar5416GetSpectralParams` writes through the same pointer
  (`:143` → `:157-167`).

The asymmetry that proves oversight: the SAME switch statement's
`SPECTRAL_CONTROL_ENABLE_AT_RESET` arm explicitly checks
`indata == NULL` and rejects with EINVAL (`if_ath_spectral.c:263-267`).
The dfs_null twin of this bug (`DFS_SET_THRESH`) was already filed as
DF-2052; pass 1 missed the spectral instance.

Reachability (per DF-2081, established): `SIOCGATHSPECTRAL` reaches
`ath_ioctl_spectral` via `ieee80211_ioctl`'s default arm
(`sys/netproto/802_11/wlan/ieee80211_ioctl.c:3516-3523` →
`if_ath_ioctl.c:304-305`) with **no privilege check**, for any user who
can open the wlan interface's control socket, on cards with
`halSpectralScanSupport = AH_TRUE` (AR9280 `ar9280_attach.c:904`, AR9285
`:558`, AR9287 `:457`).

### Reproduction (run.log, panic.txt)

The `fire` call (`ad_id = SPECTRAL_CONTROL_SET_PARAMS` (7) with NO
`ATH_DIAG_IN`, `ad_in_size = 64 ≥ sizeof(HAL_SPECTRAL_PARAM) = 36`)
produced:

```
Fatal user address access from kernel mode from df5070_trigger
Fatal trap 12: page fault while in kernel mode
fault virtual address = 0x2      (NULL + offsetof(HAL_SPECTRAL_PARAM, ss_period))
fault code = supervisor read data, page not present
Stopped at df5070_ioctl+0x110: movzwl 0x2(%r14),%ecx
```

Controls in the same run:
- `control` (same ioctl WITH `ATH_DIAG_IN` + 64-byte buffer):
  `rc=0`, dmesg `hal_configure(ah, ss=0xfffff8008976ba40) ...` — the
  identical code path is safe when the flag is present;
- `contrast` (`ENABLE_AT_RESET`, `ad_in_size=4`, no flag): `errno=22`
  — its explicit `indata == NULL` check defends;
- `user-side sizeof(HAL_SPECTRAL_PARAM)=36` printed by the trigger,
  matching the kernel-side `sizeof` print at module load.

### Exploitability ceiling

This is a NULL-deref READ at a fixed small offset (page 0 is not mappable
on this platform), i.e. a reliable unprivileged **panic / local DoS**, not
a corruption primitive — no escalation chain is available. (The known
GET_PARAMS heap overflow DF-2079 remains the corruption-class issue in
this function; it is a different arm and was NOT re-exercised here.)

### Fix validation

1. Behavioral A/B at sink level (`run_fixed_ab.log`): with the single
   added check (`if (indata == NULL) { error = EINVAL; break; }` in the
   SET_PARAMS arm only), the same `fire` call returns
   `rc=-1 errno=22 (EINVAL)`, dmesg shows
   `df5070f: SET_PARAMS rejected: indata == NULL (ATH_DIAG_IN not set)`,
   the guest stays up, and the `control`/`contrast` arms are unchanged.
2. Compile validation (`fix_validation.log`): `fix.diff` applied to the
   guest's `/usr/src/sys/dev/netif/ath/ath/if_ath_spectral.c`;
   `make` in `/usr/src/sys/dev/netif/ath/ath` produced `if_ath.ko`
   (includes `if_ath_spectral.o`) with `-Werror`, 0 warnings. Sources
   restored pristine afterwards (grep markers 0).

### Hardware gate

The real entry (a spectral-capable Atheros NIC) is absent from the QEMU
guest, so the panic was demonstrated on the verbatim control-flow replica
per house precedent; every line of the real path was traced and cited
above. On real AR9280/85/87 hardware the same ioctl from an unprivileged
user reaches the identical dereference (DF-2081's missing-priv premise).

Guest returned to a clean state (fresh `vm.sh reset with-src`, sources
pristine, modules unloaded): `guest_dirty = 0`.

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