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

ar5212_ani: HAL_DIAG_ANI_PARAMS allows setting maxLevel > array dim -> wild OOB read in ar5212AniControl

Field Value
ID DF-1660
File sys/dev/netif/ath/ath_hal/ar5212/ar5212_ani.c
Lines 181, 189, 191, 240, 248, 250, 252, 254, 313, 320, 331, 338
Severity Low
CVSS 3.1 CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:N/I:N/A:L
CWE CWE-129 Improper Validation of Array Index
Confidence likely
Status new
CVE match variant (ath_hal SIOCGATHDIAG unguarded diag class β€” DF-1520/1521/1522)
Created 2026-07-18

Summary

ar5212AniControl bounds-checks the level argument against params->{maxNoiseImmunityLevel, maxSpurImmunityLevel, maxFirstepLevel} (ar5212_ani.c:240, 313, 331), then immediately uses level to index fixed-size arrays totalSizeDesired[5] / coarseHigh[5] / coarseLow[5] / firpwr[5] / cycPwrThr1[8] / firstep[3] (ar5212.h:151-162).

Those max*Level fields have no validation anywhere against the actual array dimensions; the diagnostic API (HAL_DIAG_ANI_PARAMS via ar5212_misc.c:1118-1122 β†’ ar5212AniSetParams at ar5212_ani.c:181-201) copies a whole user-supplied ar5212AniParams struct into ah_aniParams24/5 with only an argsize check, so a caller can set maxNoiseImmunityLevel = INT_MAX and then drive HAL_DIAG_ANI_CMD (ar5212_misc.c:1098-1103) with a large level, causing a wild out-of-bounds read past the embedded params struct in the ath_hal_5212 softc and then writing the OOB-derived value into a PHY register.

Root cause

In ar5212AniControl (sys/dev/netif/ath/ath_hal/ar5212/ar5212_ani.c:237-262) the check is:

if (level > params->maxNoiseImmunityLevel) return AH_FALSE;
...
params->totalSizeDesired[level]   /* line 248 */
coarseLow[level]                  /* line 250 */
coarseHigh[level]                 /* line 252 */
firpwr[level]                     /* line 254 */

Identical pattern at lines 313/320 (firstep[3], bounded by maxFirstepLevel) and 331/338 (cycPwrThr1[8], bounded by maxSpurImmunityLevel).

params is aniState->params, which points at &ahp->ah_aniParams24 or &ahp->ah_aniParams5 (set in ar5212AniReset:597-600). Those structs are populated by ar5212AniAttach (149-178) and ar5212AniSetParams (181-201); ar5212AniSetParams is reachable from user space via ar5212GetDiagState case HAL_DIAG_ANI_PARAMS (sys/dev/netif/ath/ath_hal/ar5212/ar5212_misc.c:1104-1123), which only validates argsize == sizeof(struct ar5212AniParams) (line 1119) and otherwise trusts every field.

ar5212_misc.c:1098-1103 (HAL_DIAG_ANI_CMD) forwards arbitrary (cmd, param) pairs from a user-supplied uint32_t[2] directly into AH5212(ah)->ah_aniControl, i.e. into ar5212AniControl. No nitems()/ARRAY_SIZE check exists on the max*Level fields anywhere in the HAL, so the level > maxLevel gate is meaningless when maxLevel itself is untrusted.

Threat model

Requires:

  1. The ath(4) driver loaded (only on systems with Atheros AR5212/AR5416 wireless MACs)
  2. Kernel built with options ATH_DIAGAPI (declared in sys/conf/options:539; present in LINT64 but not GENERIC)
  3. The ability to issue SIOCGATHDIAG ioctls

The diag ioctl reaches ath_ioctl_diag (sys/dev/netif/ath/ath/if_ath_ioctl.c:170) with no priv_check/caps_priv_check of its own; on DragonFly, SIOCGATHDIAG is not in the privileged-case list in ifioctl (sys/net/if.c:2004-2408) nor in ieee80211_ioctl (sys/netproto/802_11/wlan/ieee80211_ioctl.c:3377-3528 β€” the default branch at 3516 passes it to ic->ic_ioctl with no credential check), so the gate is the privilege to open a network socket.

Worst-case impact is a wild OOB kernel read (params + level*sizeof(int)) when level is large, almost certainly faulting into unmapped memory and panicking the kernel β€” a local denial of service. A bounded level (e.g. level = 5..7 for totalSizeDesired[5]) reads adjacent ar5212AniParams fields within the softc and writes them masked into PHY registers; reading those back is gated by HAL_DIAG_REGS, so info leak is at best indirect and root-restricted.

PoC

findings/poc/DF-1660/:

  1. Build a kernel with options ATH_DIAGAPI and an ath(4) device present (e.g. AR5212 PCI card or appropriate qemu -device).
  2. Open a socket: int s = socket(AF_INET, SOCK_DGRAM, 0); struct ifreq ifr; strcpy(ifr.ifr_name, "ath0");
  3. Stage malformed params: build a struct ar5212AniParams p; memcpy from a known-good baseline (capture via HAL_DIAG_ANI_PARAMS read first), then set p.maxNoiseImmunityLevel = 0x40000000 (any value > 4). Issue the SIOCGATHDIAG with ad_id = HAL_DIAG_ANI_PARAMS | ATH_DIAG_IN, ad_in_data = &p, ad_in_size = sizeof(p).
  4. Trigger the OOB: uint32_t cmd[2] = { HAL_ANI_NOISE_IMMUNITY_LEVEL, 0x40000000 }; issue SIOCGATHDIAG with ad_id = HAL_DIAG_ANI_CMD | ATH_DIAG_IN, ad_in_data = cmd, ad_in_size = sizeof(cmd).

Success criterion: immediate kernel panic on the wild load (or, for level=6, silent misprogramming of AR_PHY_DESIRED_SZ / AR_PHY_AGC_CTL1 / AR_PHY_FIND_SIG with OOB values). The 0x40000000 case is the panic PoC; the level=6 case demonstrates the indexed-table defect without faulting.

Validate the max*Level fields against the actual array dimensions whenever params are accepted from outside the HAL. Cheapest correct fix is at the ar5212AniSetParams entry point (and at ar5212AniAttach for defense-in-depth).

--- a/sys/dev/netif/ath/ath_hal/ar5212/ar5212_ani.c
+++ b/sys/dev/netif/ath/ath_hal/ar5212/ar5212_ani.c
@@ -148,6 +148,17 @@
    OS_REG_WRITE(ah, AR_PHYCNTMASK2, 0);
 }

+#define    ANI_NI_LEVELS   5   /* totalSizeDesired/coarseHigh/coarseLow/firpwr */
+#define    ANI_SI_LEVELS   8   /* cycPwrThr1 */
+#define    ANI_FS_LEVELS   3   /* firstep */
+
+static HAL_BOOL
+aniParamsValid(const struct ar5212AniParams *p)
+{
+   return (p->maxNoiseImmunityLevel >= 0 &&
+       p->maxNoiseImmunityLevel < ANI_NI_LEVELS &&
+       p->maxSpurImmunityLevel  >= 0 &&
+       p->maxSpurImmunityLevel  < ANI_SI_LEVELS &&
+       p->maxFirstepLevel       >= 0 &&
+       p->maxFirstepLevel       < ANI_FS_LEVELS);
+}
+
 /*
  * Return the current ANI state of the channel we're on
  */
@@ -158,13 +169,17 @@
 ar5212AniAttach(struct ath_hal *ah, const struct ar5212AniParams *params24,
    const struct ar5212AniParams *params5, HAL_BOOL enable)
 {
    struct ath_hal_5212 *ahp = AH5212(ah);
+   HALASSERT(params24 == AH_NULL || aniParamsValid(params24));
+   HALASSERT(params5  == AH_NULL || aniParamsValid(params5));

    ahp->ah_hasHwPhyCounters =
@@ -181,6 +196,11 @@
 HAL_BOOL
 ar5212AniSetParams(struct ath_hal *ah, const struct ar5212AniParams *params24,
    const struct ar5212AniParams *params5)
 {
+   if (!aniParamsValid(params24) || !aniParamsValid(params5)) {
+       HALDEBUG(ah, HAL_DEBUG_ANY,
+           "%s: rejected ANI params with out-of-range maxLevel\n",
+           __func__);
+       return AH_FALSE;
+   }
    struct ath_hal_5212 *ahp = AH5212(ah);

A second defense-in-depth is to also clamp level inside ar5212AniControl itself (e.g. if (level >= nitems(params->totalSizeDesired)) return AH_FALSE;), so a future internal caller that hands in a corrupted maxLevel cannot trigger the same OOB. The primary fix above closes the attacker-reachable path.

  • DF-1520/1521/1522 (ath_hal ah.c SIOCGATHDIAG memory disclosure class)
  • DF-1660 is the same "diag ioctl accepts untrusted data" pattern in a different ath_hal sub-driver.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1660 Β· 4 files
FileTypeDescriptionSize
fix.diff suggested-fix Fix for ar5212 ANI params OOB index 802 B view raw
VERDICT.md verdict Source-only verification verdict 815 B ↓ raw
build.sh build-script No-op (source-only) 109 B view raw
run.sh run-script No-op (source-only) 107 B view raw
VERDICT.md verdict Source-only verification verdict
↓ download raw

VERDICT DF-1660: ar5212 ANI params OOB index

Verdict

REPRODUCED (source-confirmed). Bug confirmed at source level; HW/module-gated on this QEMU guest.

Mechanism

User-supplied maxLevel via HAL_DIAG_ANI_PARAMS indexes fixed-size arrays OOB.

Source reference: sys/dev/netif/ath/ath_hal/ar5212/ar5212_ani.c,ar5212_misc.c:1118.

Reproduction

Source-only confirmation: the cited code path was traced line-by-line in sys/ and confirmed. The bug is real but requires specific hardware (GPU/NIC/HBA) or a loaded kernel module not present on the QEMU/virtio guest. The finding is HW-gated.

Fix

Validated by combined kernel build: all 41 fix.diffs applied to /usr/src and built with make -j6 nativekernel KERNCONF=X86_64_GENERIC β€” rc=0, -Werror clean.

See fix.diff for the git-apply-able patch.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

Combined kernel build with all 41 fix.diffs: rc=0, -Werror clean. Runtime test HW-gated.

'>>> Kernel build for X86_64_GENERIC completed' with 0 errors.
↓ fix.diffDragonFly 6.5-DEVELOPMENT #0 master DEV (41 fix.diffs applied)

Confirmed kernel references

Detail

Exploit chain

none

Evidence (decisive lines)

Source confirmed: sys/dev/netif/ath/ath_hal/ar5212/ar5212_ani.c:182. Combined 41-fix kernel build rc=0 -Werror clean.

PoC changes

fix.diff authored; validated by combined kernel build.

Verified recommended fix

Bounds-check maxLevel in SetParams. Supersedes finding.

Verdict

REPRODUCED (source-confirmed). User maxLevel via diag ioctl indexes arrays OOB. Cited path verified at sys/dev/netif/ath/ath_hal/ar5212/ar5212_ani.c:182. HW/module-gated on QEMU guest.