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

ar5416AniControl NULL-pointer dereference when driven before the first channel reset (ah_curani == NULL): kernel panic via HAL_DIAG_ANI_CMD

Summary

Only PRESENT/MODE guard against a missing current channel; every level/threshold command dereferences params (NULL until the first AniReset) or writes through aniState. Attach enables all commands (ah_ani_function=0xffffffff) and the softc is zeroed until first tune, while the diag interface forwards user (cmd,param) verbatim. Unpriv local panic on ATH_DIAGAPI kernels (which lack the SIOCGATHDIAG priv check per DF-2059/4320); root-only on stock. Harness-verified decision flow with the exact NULL load replicated. aniState-NULL guard fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4526 Β· 9 files
FileTypeDescriptionSize
ani_null_deref.c β€” 6.0 KB view raw
build.sh β€” 102 B view raw
run.sh β€” 151 B view raw
run.log β€” 1.3 KB view raw
fix.diff β€” 880 B view raw
README.md β€” 3.0 KB ↓ raw
VERDICT.md β€” 3.3 KB ↓ raw
verdict.json β€” 3.1 KB view raw
env.txt β€” 1.7 KB view raw

PoC DF-4526 β€” ar5416AniControl NULL-pointer deref before first channel set

What this is

ar5416AniControl() handles being called with no current channel ("This function may be called before there's a current channel (eg to disable ANI.)", ar5416_ani.c:180-185) only for HAL_ANI_PRESENT and HAL_ANI_MODE. For every other command it dereferences params/aniState:

struct ar5212AniState *aniState = ahp->ah_curani;   /* :177 */
const struct ar5212AniParams *params = AH_NULL;      /* :178 */
if (aniState != AH_NULL)
        params = aniState->params;                   /* :184-185 */
...
if (((1 << cmd) & AH5416(ah)->ah_ani_function) == 0) /* :217 gate */
        return AH_FALSE;
switch (cmd) {
case HAL_ANI_NOISE_IMMUNITY_LEVEL:
        u_int level = param;
        if (level > params->maxNoiseImmunityLevel) { /* :230 NULL READ */
...
case HAL_ANI_OFDM_WEAK_SIGNAL_DETECTION:
        ...
        aniState->ofdmWeakSigDetectOff = !on;        /* :296 NULL WRITE */

Attach-time state makes both reachable through the diagnostic api:

  • the ar5416 softc is allocated zeroed (ar5416_attach.c:312 "NB: memory is returned zero'd") β†’ ah_curani == NULL until the first ar5416AniReset() (only there is ah_curani published, ar5416_ani.c:561);
  • ar5416Attach() enables every ANI command: AH5416(ah)->ah_ani_function = 0xffffffff (ar5416_attach.c:244) β†’ the function gate at :217 passes;
  • HAL_DIAG_ANI_CMD forwards user (cmd, param) verbatim to ah_aniControl() (ar5212_misc.c:1098-1103, reached from ar5416 via ar5416GetDiagState β†’ ar5212GetDiagState, ar5416_misc.c:557), so ioctl(SIOCGATHDIAG, {HAL_DIAG_ANI_CMD, {1, 0}}) on an ath(4) device that has not yet tuned a channel executes the NULL deref β†’ kernel page fault β†’ panic. HAL_ANI_OFDM_WEAK_SIGNAL_DETECTION gives a NULL write at offset 3 instead.

Reachability gates: requires a kernel built with options ATH_DIAGAPI (not in stock X86_64_GENERIC β€” LINT64 only), ath(4) hardware present, and the SIOCGATHDIAG ioctl β€” which has no privilege check on DragonFly (if_ath_ioctl.c:299-300; contrast SIOCZATHSTATS at :288; already filed as DF-2059/DF-4320), making the panic reachable by any local user on such kernels.

Why a userspace harness (hardware gate)

The audit QEMU guest has no Atheros radio and QEMU cannot emulate one, so the in-kernel path cannot be executed here (same gate as DF-1676/ DF-2511 evidence packs). ani_null_deref.c is a compile-faithful replica of the dispatch flow with the attach-time state; --deref actually performs the NULL load and dies with SIGSEGV, demonstrating the fault is on the cited instruction.

Build / run (guest or any POSIX host)

cc -O2 -Wall -o ani_null_deref ani_null_deref.c
./ani_null_deref           # flow trace, flags the deref   -> BUGGY
./ani_null_deref --deref   # performs the NULL read        -> SIGSEGV
./ani_null_deref --fixed   # simulates the guard           -> PATCHED

Fix

fix.diff β€” NULL guard after the PRESENT/MODE handling, before the function gate.

VERDICT.md
↓ download raw

VERDICT β€” DF-4526

Status: not_reproduced on the guest (hardware-gated: no ath(4) radio exists in QEMU). Impact if the gate is met: kernel NULL-deref panic (local DoS). The defect is code-certain; the userspace harness proves the faulting instruction executes under attach-time state.

The bug

ar5416AniControl() (sys/dev/netif/ath/ath_hal/ar5416/ar5416_ani.c:172) guards only HAL_ANI_PRESENT (:190) and HAL_ANI_MODE (:193) against a missing current channel. All level/threshold commands dereference params (NULL whenever ah_curani == NULL, :178-185) or aniState itself:

  • :230 params->maxNoiseImmunityLevel (HAL_ANI_NOISE_IMMUNITY_LEVEL)
  • :317 params->maxFirstepLevel (HAL_ANI_FIRSTEP_LEVEL)
  • :336 params->maxSpurImmunityLevel (HAL_ANI_SPUR_IMMUNITY_LEVEL)
  • :296 aniState->ofdmWeakSigDetectOff = !on (NULL WRITE)
  • :310 aniState->cckWeakSigThreshold = high (NULL WRITE)

Why it is reachable

  1. ar5416 softc is zeroed at allocation (ar5416_attach.c:312) β†’ ah_curani == NULL after attach; the pointer is only published in ar5416AniReset() (ar5416_ani.c:561), which runs on the first channel set.
  2. ar5416Attach() sets ah_ani_function = 0xffffffff (ar5416_attach.c:244) β†’ the command gate at ar5416_ani.c:217 passes for every command.
  3. HAL_DIAG_ANI_CMD (ar5212_misc.c:1098-1103) passes user (cmd, param) straight to AH5212(ah)->ah_aniControl(); ar5416 routes its diag interface through the same handler (ar5416_misc.c:530,557). The SIOCGATHDIAG entry has no privilege check (if_ath_ioctl.c:299-300 β€” DF-2059/DF-4320), so on options ATH_DIAGAPI kernels any local user with a socket can trigger the panic between driver attach and first channel tune (e.g. immediately after boot, before ifconfig sets a channel).

Result in-kernel: read of *(int *)0x4-ish or write to *(uint8_t *)3 β†’ page fault β†’ Fatal trap 12 panic. Panic-only; no corruption primitive (fixed low address, fixed size).

Verification performed

  • Full code-path trace (cited lines) from ioctl entry to faulting instruction β€” no intervening guard exists.
  • ani_null_deref harness (this pack): replicates the dispatch with ah_curani=NULL, ah_ani_function=0xffffffff; --deref mode executes the NULL load and receives SIGSEGV on exactly the replicated instruction; --fixed shows the proposed guard returns AH_FALSE. Ran on the audit guest (BUILD_OK; outputs in run.log).
  • Hardware gate: pciconf -l on the audit guest shows no Atheros PCI device; QEMU 7.2 has no ath HAL emulation; the ath driver cannot attach β†’ in-kernel execution impossible in this environment.

Fix

One-line guard (fix.diff):

+   if (aniState == AH_NULL) {
+       HALDEBUG(ah, HAL_DEBUG_ANI, "%s: no current ani state\n", __func__);
+       return AH_FALSE;
+   }

placed after the HAL_ANI_PRESENT/HAL_ANI_MODE handling, before the function gate. (Upstream ar5212_ani.c has the same shape and would take the same guard.)

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

not_testable
baseline no→ patch + rebuild →patched clean

Hardware gate (no Atheros NIC emulatable in QEMU): in-kernel reproduction impossible in this environment; harness demonstrates the faulting instruction executes under attach-time state and that the one-line guard in fix.diff diverts the flow to AH_FALSE.

run.log (--fixed mode), fix.diff
↓ fix.diffper-fix-DF-4526

Confirmed kernel references

Detail

Evidence (decisive lines)

ani_null_deref.c, run.log (BUILD_OK + BUGGY/BUGGY-SIGSEGV/PATCHED outputs), VERDICT.md (path:line trace incl. attach-time state), fix.diff

PoC changes

No seed PoC. Wrote the decision-flow harness replicating the dispatch with attach-time state (zeroed softc + ah_ani_function=0xffffffff).

Verified recommended fix

Return AH_FALSE when aniState==AH_NULL after the PRESENT/MODE handling in ar5416AniControl().

Verdict

Code-certain NULL-pointer dereference in ar5416AniControl(), hardware-gated on this guest (no ath(4) radio; QEMU cannot emulate the Atheros HAL). ar5416AniControl() guards only HAL_ANI_PRESENT/HAL_ANI_MODE against a missing current channel; every level/threshold command dereferences params (NULL whenever ah_curani==NULL, ar5416_ani.c:178-185) at :230/:317/:336 or writes through aniState at :296/:310. The ar5416 softc is allocated zeroed (ar5416_ani.c 'ah_curani' is only published in ar5416AniReset, :561) and ar5416Attach() enables all commands (ah_ani_function=0xffffffff, ar5416_attach.c:244), so the function gate at :217 passes; HAL_DIAG_ANI_CMD (ar5212_misc.c:1098-1103, reachable from ar5416 via ar5416_misc.c:557) forwards user (cmd,param) verbatim -> immediate page fault -> kernel panic. On kernels built with options ATH_DIAGAPI (not stock GENERIC) the SIOCGATHDIAG entry has no privilege check (if_ath_ioctl.c:299-300; DF-2059/DF-4320), making it a local unprivileged panic there. Verified by full path trace + a compile-faithful userspace harness whose --deref mode executes the NULL load and takes SIGSEGV exactly on the replicated instruction (see run.log).