# DF-5024 Verification Verdict

**File:** sys/dev/netif/ath/ath_dfs/null/dfs_null.c:253,275-281
**Severity:** Medium (unprivileged kernel-heap disclosure; hardware/config-gated entry)
**Method:** sink-level reproduction on the stock guest kernel (DF-4986/DF-5012
house precedent) + line-level source trace + behavioral fix A/B + real-module
compile validation of the fix.

## Reproduced: YES (leak, sink-level; entry path hardware-gated)

### The defect (source trace)

`ath_ioctl_phyerr()` (dfs_null.c:220-288):

1. `dfs_null.c:245-258` — when the user sets `ATH_DIAG_DYN` (0x8000) in
   `ad_id`, `outdata = kmalloc(outsize, M_TEMP, M_INTWAIT)` is allocated with
   the attacker-chosen `ad_out_size` and **never initialized**.
2. `dfs_null.c:259-277` — the command dispatch. Two error paths leave
   `outdata` untouched:
   * `default:` for any id ∉ {2,3} (`DFS_RADARDETECTS`=6 from
     if_athioctl.h:404, or any garbage id) → `error = EINVAL` (:275-276);
   * `DFS_SET_THRESH` with `ad_in_size < sizeof(HAL_PHYERR_PARAM)` (60) →
     `error = EINVAL` (:261-264) — this path also works with
     `ATH_DIAG_IN|ATH_DIAG_DYN` and a 0/short input buffer.
3. `dfs_null.c:278-281` — **unconditionally**: clamp `ad_out_size`, then
   `if (outdata && copyout(outdata, ad->ad_out_data, ad->ad_out_size))` —
   the full uninitialized M_TEMP block is published to userspace even though
   the command was rejected (EINVAL is then returned; the data is already
   out).

Contrast with the sane sibling `ath_ioctl_diag()` (if_ath_ioctl.c:214-222):
there the copyout happens only when `ath_hal_getdiagstate()` accepts the id —
unknown ids never copy out. The null-DFS copy dropped that guard.

Reachability: `SIOCGATHPHYERR` → `ath_ioctl()` (if_ath_ioctl.c:301-302,
`#ifdef ATH_DIAGAPI`) — no privilege check anywhere on the path
(`SIOCZATHSTATS` two cases above shows what a privileged variant looks like:
`caps_priv_check_self`, :288). Entry needs an `ath` interface (hardware) and
a kernel built with `option ATH_DIAGAPI`. On such a system **any local user**
holds this leak.

### Runtime proof (stock guest kernel, uname in env.txt)

`run.log` / `leak_sample.txt` (sink = verbatim ioctl body):

* ioctl returns EINVAL while 2048 bytes of recycled kernel M_TEMP land in
  the user buffer: 255/256 slots carry the sysctl-sprayed marker
  `DF5024!!`; the leading qword (`ff..ff`) is malloc-chunk metadata —
  kernel provenance proven, not zeros/stale user data.
* Both error paths leak (unknown id AND short SET input).
* Stable across 3 runs.

### Fix validation

* **Behavioral A/B** (`run_fixed_ab.log`): with the fix's `error == 0` gate
  transplanted into the sink, the same ioctls return EINVAL and the buffer
  stays all-zero (`marker8=0/256`, `nonzero=0`) — the disclosure is gone.
* **Compile validation**: `fix.diff` applied to the guest's `/usr/src` copy;
  `sys/dev/netif/ath/ath_dfs/null` module rebuilt clean with `-Werror`
  (`ath_dfs.ko`). Sources restored afterward.
* Also batch-compiled clean together with the three ips fixes (ips.ko,
  `-Werror`).

## Fix

`fix.diff` — gate the clamp+copyout on `error == 0` (mirrors
ath_ioctl_diag). Independent `git apply --check` clean; no interaction with
the DF-2051/DF-2052 fixes (different lines).

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