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

SPECTRAL_CONTROL_SET_PARAMS dereferences indata without checking the ATH_DIAG_IN flag β€” unprivileged NULL-pointer-deref panic (DFS_SET_THRESH twin of DF-2052, missed in pass 1)

Summary

ad_in_size is validated but is independent of the ATH_DIAG_IN flag that gates the indata allocation - no-flag + size>=36 leaves indata NULL into the HAL's first field read/write. The sibling ENABLE_AT_RESET arm in the same switch checks indata==NULL (the oversight asymmetry). Unpriv reachable per DF-2081 on AR9280/9285/9287. Sink-level KLD: Fatal trap 12 at fault va 0x2 reproduced; fixed variant EINVAL; if_ath.ko rebuilt -Werror clean. Mirror-the-sibling-guard fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5070 Β· 17 files
FileTypeDescriptionSize
df5070.c sink-module verbatim ath_ioctl_spectral control-flow replica (baseline + #ifdef fix) 8.4 KB view raw
df5070_fixed.c fixed-variant #define DF5070_FIXED wrapper (bsd.kmod.mk drops ad-hoc CFLAGS) 243 B view raw
df5070_trigger.c trigger control/contrast/fire ioctl driver 2.9 KB view raw
Makefile build 52 B ↓ download
Makefile.fixed build 59 B ↓ download
build.sh build-script 107 B view raw
build.log build-log final successful build (-Werror clean) 374 B view raw
run.sh run-script baseline: control + contrast + fire (panic) 148 B view raw
run.log run-log baseline run output (session dies at panic) 626 B view raw
panic.txt panic Fatal trap 12, va 0x2, df5070_ioctl+0x110 from serial console 999 B view raw
run_fixed_ab.sh run-script fixed-variant A/B (after reset) 866 B view raw
run_fixed_ab.log run-log fire -> EINVAL, no panic, guest alive 1.2 KB view raw
fix.diff suggested-fix indata==NULL check in SET_PARAMS (compile-validated) 559 B view raw
fix_validation.log fix-build-log patched if_ath.ko -Werror clean; sources restored 3.5 KB view raw
env.txt environment 336 B view raw
VERDICT.md verdict 4.7 KB ↓ raw
verdict.json verdict-machine 5.7 KB view raw
VERDICT.md verdict
↓ download raw

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.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

Behavioral A/B at sink level (run_fixed_ab.log): baseline control flow panics (Fatal trap 12, va 0x2); fixed variant (identical module + the single indata==NULL check in SET_PARAMS) returns EINVAL for the same call with the guest alive and the control/contrast arms unchanged. fix.diff also applied to the guest's /usr/src and compile-validated: if_ath.ko built -Werror clean including the patched if_ath_spectral.o (fix_validation.log; sources restored pristine). The real ath entry is HW-gated on this guest; the failure mode and its removal are properties of the driver-side control flow.

run_fixed_ab.log ('fire: ... errno=22' + 'df5070f: SET_PARAMS rejected: indata == NULL' + uptime); fix_validation.log (patched if_ath.ko -Werror clean); fix.diff
↓ fix.diffper-fix-DF-5070

Confirmed kernel references

Detail

Evidence (decisive lines)

panic.txt: 'Fatal trap 12: page fault while in kernel mode / fault virtual address = 0x2 / Stopped at df5070_ioctl+0x110: movzwl 0x2(%r14),%ecx' (preceded by the control run's 'hal_configure(ah, ss=0xfffff8008976ba40): reading ss_fft_period=65535' and the contrast arm's 'ENABLE_AT_RESET: indata=NULL'); run.log: control rc=0, contrast errno=22, fire -> ssh session killed by the panic; run_fixed_ab.log: 'fire: ... ad_in_size=64 -> ioctl rc=-1 errno=22' + dmesg 'df5070f: SET_PARAMS rejected: indata == NULL (ATH_DIAG_IN not set)' + uptime alive; fix_validation.log: if_ath.ko built -Werror clean from patched /usr/src/sys/dev/netif/ath/ath

PoC changes

authored fresh from source analysis (no seed); sink KLD per DF-4986/DF-5024 precedent because the guest has no spectral-capable Atheros NIC; fixed variant built as a #define wrapper (df5070_fixed.c) after discovering DFly's bsd.kmod.mk silently drops ad-hoc CFLAGS (first fixed .ko was byte-identical to baseline)

Verified recommended fix

if_ath_spectral.c SPECTRAL_CONTROL_SET_PARAMS: reject when indata == NULL (ATH_DIAG_IN not set), mirroring the ENABLE_AT_RESET check at :263-267 (fix.diff, compile-validated).

Verdict

Sink-level reproduction on the stock guest kernel (DF-4986/DF-5012/DF-5024 house precedent): a KLD carrying the verbatim ath_ioctl_spectral() control flow (copyin-flag logic + SET_PARAMS/ENABLE_AT_RESET arms, if_ath_spectral.c:184/197/233-238/255-273) proves that SPECTRAL_CONTROL_SET_PARAMS with ad_in_size >= sizeof(HAL_SPECTRAL_PARAM)=36 but WITHOUT the ATH_DIAG_IN flag passes the :233 size check with indata still NULL, and the HAL call (replicated as ar5416ConfigureSpectralScan's first field accesses, ar5416_spectral.c:92/:97 read + :143->:157-167 write) faults: Fatal trap 12, supervisor read, fault va 0x2 (NULL + offsetof(ss_period)), stopped at df5070_ioctl+0x110. Controls in the same run: the same ioctl WITH the flag succeeds (rc=0, hal_configure touched the params safely), and the sibling ENABLE_AT_RESET arm with no flag is REJECTED EINVAL by its explicit indata==NULL check (if_ath_spectral.c:263-267) -- the asymmetry that proves the missing check is an oversight (dfs_null twin already filed as DF-2052). Unprivileged reachability rides DF-2081 (SIOCGATHSPECTRAL dispatched with no priv check via ieee80211_ioctl.c:3516-3523 -> if_ath_ioctl.c:304-305; halSpectralScanSupport on AR9280/85/87). The real entry is HW-gated on this guest (no Atheros NIC); the control flow, sizes and dereference are source-exact. Exploit ceiling: fixed-offset NULL READ on an unmappable page -- reliable unprivileged panic/DoS, no corruption primitive (the corruption-class issue in this function is the known DF-2079, a different arm). Fix A/B: with the single indata==NULL check added to SET_PARAMS, the same call returns EINVAL and the guest stays up; fix.diff also compile-validated against the guest's /usr/src (if_ath.ko with patched if_ath_spectral.o, -Werror clean; sources restored).