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)
PoC verification
Evidence pack
findings/poc/DF-5070 Β· 17 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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βindatais only allocated whenad->ad_id & ATH_DIAG_IN;if_ath_spectral.c:233-236β the SET_PARAMS arm checks ONLYinsize < sizeof(HAL_SPECTRAL_PARAM)(ad_in_sizeis 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()readsss->ss_fft_period(ar5416_spectral.c:92) /ss_period(:97) andar5416GetSpectralParamswrites 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
- 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 samefirecall returnsrc=-1 errno=22 (EINVAL), dmesg showsdf5070f: SET_PARAMS rejected: indata == NULL (ATH_DIAG_IN not set), the guest stays up, and thecontrol/contrastarms are unchanged. - Compile validation (
fix_validation.log):fix.diffapplied to the guest's/usr/src/sys/dev/netif/ath/ath/if_ath_spectral.c;makein/usr/src/sys/dev/netif/ath/athproducedif_ath.ko(includesif_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
fixedBehavioral 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
Confirmed kernel references
- sys/dev/netif/ath/ath/if_ath_spectral.c:233
- sys/dev/netif/ath/ath/if_ath_spectral.c:237
- sys/dev/netif/ath/ath/if_ath_spectral.c:238
- sys/dev/netif/ath/ath/if_ath_spectral.c:197
- sys/dev/netif/ath/ath/if_ath_spectral.c:263
- sys/dev/netif/ath/ath/if_athioctl.h:183
- sys/dev/netif/ath/ath_hal/ar5416/ar5416_spectral.c:92
- sys/dev/netif/ath/ath_hal/ar5416/ar5416_spectral.c:143
- sys/dev/netif/ath/ath_hal/ar5416/ar5416_spectral.c:157
- sys/netproto/802_11/wlan/ieee80211_ioctl.c:3516
- sys/dev/netif/ath/ath/if_ath_ioctl.c:304
- sys/dev/netif/ath/ath_hal/ar9002/ar9280_attach.c:904
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).
No comments yet.