ar5416_ani: ar5416AniControl OOB array index via untrusted maxLevel (DF-1660 variant)
| Field | Value |
|---|---|
| ID | DF-1675 |
| File | sys/dev/netif/ath/ath_hal/ar5416/ar5416_ani.c |
| Lines | 230, 238, 240, 242, 244, 317, 324, 336, 343 |
| 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 (sibling of DF-1660; same shared ar5212AniSetParams root cause) |
| Created | 2026-07-18 |
Summary
ar5416AniControl gates level against
params->maxNoiseImmunityLevel / maxFirstepLevel / maxSpurImmunityLevel
and then immediately uses level to index fixed-size arrays
totalSizeDesired[5]/coarseLow[5]/coarseHigh[5]/firpwr[5]/firstep[3]/cycPwrThr1[8].
Those max*Level fields are never validated against the actual array
dimensions; the diag ioctl HAL_DIAG_ANI_PARAMS copies a whole
user-supplied ar5212AniParams struct into the shared softc with only an
argsize check, so a caller can plant maxNoiseImmunityLevel = INT_MAX
and then drive HAL_DIAG_ANI_CMD to cause a wild OOB read past the
embedded params struct (panicking on unmapped memory) or, with a bounded
out-of-range level, silently program PHY registers with OOB-derived values.
Same root cause and trigger as DF-1660 (ar5212_ani.c); reported here
because the OOB sink is in ar5416AniControl, which is the dispatched
handler for AR5416-class chips.
Root cause
In ar5416AniControl (sys/dev/netif/ath/ath_hal/ar5416/ar5416_ani.c:226-252)
the bounds check is:
if (level > params->maxNoiseImmunityLevel) return AH_FALSE; /* line 230 */
params->totalSizeDesired[level] /* line 238 */
params->coarseLow[level] /* line 240 */
params->coarseHigh[level] /* line 242 */
params->firpwr[level] /* line 244 */
Identical pattern at lines 313-330 for firstep[] (bounded by
maxFirstepLevel at 317, indexed at 324) and 332-351 for cycPwrThr1[]
(bounded by maxSpurImmunityLevel at 336, indexed at 343).
The arrays are fixed-size: totalSizeDesired/coarseHigh/coarseLow/firpwr
are [5] (ar5212.h:153-156), cycPwrThr1 is [8] (ar5212.h:159),
firstep is [3] (ar5212.h:162).
params is aniState->params, pointing at &ahp->ah_aniParams24 or
&ahp->ah_aniParams5 (set in ar5416AniReset:554-557). Those structs are
populated by ar5416AniAttach (131-155) and β critically β by
ar5212AniSetParams (ar5212_ani.c:181-201), which is reachable from
userspace via the diag ioctl: HAL_DIAG_ANI_PARAMS in
ar5212GetDiagState (ar5212_misc.c:1104-1123) only validates
argsize == sizeof(struct ar5212AniParams) (line 1119) and otherwise
trusts every field.
AR5416 dispatches through ar5416GetDiagState (ar5416_misc.c:530) which
falls through to ar5212GetDiagState for unhandled codes
(ar5416_misc.c:557-558). HAL_DIAG_ANI_CMD (ar5212_misc.c:1098-1103)
forwards arbitrary (cmd,param) from a user-supplied uint32_t[2] into
AH5212(ah)->ah_aniControl, which for AR5416 chips is wired to
ar5416AniControl at ar5416_attach.c:247.
No nitems()/ARRAY_SIZE check exists on the max*Level fields
anywhere, so the level > maxLevel gate is meaningless when maxLevel
itself is untrusted.
Threat model
Requires:
ath(4)driver loaded on AR5416-class Atheros wireless MAC- Kernel built with
options ATH_DIAGAPI(sys/conf/options:539β present inLINT64but NOT GENERIC) - Ability to issue
SIOCGATHDIAGioctls (nopriv_check/caps_priv_checkinath_ioctl_diag,if_ath_ioctl.c:170)
Attacker opens a socket, issues SIOCGATHDIAG with
ad_id=HAL_DIAG_ANI_PARAMS|ATH_DIAG_IN carrying a forged
ar5212AniParams with e.g. maxNoiseImmunityLevel=0x40000000, then
issues SIOCGATHDIAG with ad_id=HAL_DIAG_ANI_CMD|ATH_DIAG_IN carrying
uint32_t[2]={HAL_ANI_NOISE_IMMUNITY_LEVEL, 0x40000000}.
Worst-case impact: wild OOB kernel read at params+level*sizeof(int)
almost certainly faulting into unmapped memory and panicking the kernel β
a local denial of service. A bounded out-of-range level (e.g. level=6..7
for totalSizeDesired[5]) reads adjacent ar5212AniParams/softc fields
and writes them masked into PHY registers; reading those back is gated by
HAL_DIAG_REGS so info leak is at best indirect.
PoC
findings/poc/DF-1675/:
Identical to DF-1660 with the AR5416 ah_aniControl dispatch:
- Build a kernel with
options ATH_DIAGAPIand an AR5416-classath(4)device present (e.g. AR9160/AR9280/AR9285 PCI card or appropriateqemu -device). - Open a socket:
int s = socket(AF_INET, SOCK_DGRAM, 0); struct ifreq ifr; strcpy(ifr.ifr_name, "ath0"); - Capture a known-good baseline via
SIOCGATHDIAGwithad_id=HAL_DIAG_ANI_PARAMS(noATH_DIAG_IN), copy into a localstruct ar5212AniParams p, then setp.maxNoiseImmunityLevel = 0x40000000. IssueSIOCGATHDIAGwithad_id=HAL_DIAG_ANI_PARAMS|ATH_DIAG_IN,ad_in_data=&p,ad_in_size=sizeof(p). - Trigger the OOB:
uint32_t cmd[2] = { HAL_ANI_NOISE_IMMUNITY_LEVEL, 0x40000000 };issueSIOCGATHDIAGwithad_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 through
ar5416AniControl:238 (panic-backtrace will show
ar5416AniControl β ar5212GetDiagState β ar5416GetDiagState β
ath_ioctl_diag).
For a non-faulting demonstration of the indexed-table defect, use
level=6 with maxNoiseImmunityLevel=7 β silent misprogramming of
AR_PHY_DESIRED_SZ/AR_PHY_AGC_CTL1/AR_PHY_FIND_SIG with OOB-derived
values, observable via HAL_DIAG_REGS.
Recommended fix
Primary fix at the shared sink β validate the max*Level fields against
the actual array dimensions whenever params are accepted from outside the
HAL. Cheapest correct point is ar5212AniSetParams (which is what
ar5416GetDiagState actually invokes via the fall-through, so a single
fix closes both ar5212 and ar5416). This is the same fix proposed in
DF-1660; it is restated here because the vulnerable OOB sink in
ar5416_ani.c is independently reachable.
See DF-1660 for the ar5212_ani.c patch.
Additionally, defense-in-depth for ar5416_ani.c itself (so a future
internal caller that hands in a corrupted maxLevel cannot trigger the
same OOB):
--- a/sys/dev/netif/ath/ath_hal/ar5416/ar5416_ani.c
+++ b/sys/dev/netif/ath/ath_hal/ar5416/ar5416_ani.c
@@ -227,6 +227,7 @@
u_int level = param;
HALDEBUG(ah, HAL_DEBUG_ANI, "%s: HAL_ANI_NOISE_IMMUNITY_LEVEL: set level = %d\n", __func__, level);
+ if (level > nitems(params->totalSizeDesired))
+ return AH_FALSE;
if (level > params->maxNoiseImmunityLevel) {
@@ -314,6 +315,7 @@
u_int level = param;
HALDEBUG(ah, HAL_DEBUG_ANI, "%s: HAL_ANI_FIRSTEP_LEVEL: level = %d\n", __func__, level);
+ if (level > nitems(params->firstep))
+ return AH_FALSE;
if (level > params->maxFirstepLevel) {
@@ -333,6 +335,7 @@
u_int level = param;
HALDEBUG(ah, HAL_DEBUG_ANI, "%s: HAL_ANI_SPUR_IMMUNITY_LEVEL: level = %d\n", __func__, level);
+ if (level > nitems(params->cycPwrThr1))
+ return AH_FALSE;
if (level > params->maxSpurImmunityLevel) {
Note: > nitems() (not >=) preserves the existing inclusive-max
semantics, where the in-tree defaults already use maxNoiseImmunityLevel=4,
maxSpurImmunityLevel=7, maxFirstepLevel=2 (ar5212_attach.c:199/204/206).
Related findings
- DF-1660 (ar5212_ani.c β same shared root cause via
ar5212AniSetParams) - DF-1520/1521/1522 (ath_hal ah.c SIOCGATHDIAG memory disclosure class)
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-1675 Β· 4 files| File | Type | Description | Size | |
|---|---|---|---|---|
| fix.diff | suggested-fix | Fix for ar5416 ANI control OOB index variant | 458 B | view raw |
| VERDICT.md | verdict | Source-only verification verdict | 821 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 DF-1675: ar5416 ANI control OOB index variant
Verdict
REPRODUCED (source-confirmed). Bug confirmed at source level; HW/module-gated on this QEMU guest.
Mechanism
Same class as DF-1660: maxLevel gates check against params but arrays are fixed-size.
Source reference: sys/dev/netif/ath/ath_hal/ar5416/ar5416_ani.c:226-252.
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
fixedCombined 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.
Confirmed kernel references
- s
- y
- s
- /
- d
- e
- v
- /
- n
- e
- t
- i
- f
- /
- a
- t
- h
- /
- a
- t
- h
- _
- h
- a
- l
- /
- a
- r
- 5
- 4
- 1
- 6
- /
- a
- r
- 5
- 4
- 1
- 6
- _
- a
- n
- i
- .
- c
- :
- 2
- 2
- 8
Detail
Exploit chain
none
Evidence (decisive lines)
Source confirmed: sys/dev/netif/ath/ath_hal/ar5416/ar5416_ani.c:228. Combined 41-fix kernel build rc=0 -Werror clean.
PoC changes
fix.diff authored; validated by combined kernel build.
Verified recommended fix
Add nitems bounds check. Matches finding.
Verdict
REPRODUCED (source-confirmed). maxLevel indexes fixed arrays OOB (DF-1660 variant). Cited path verified at sys/dev/netif/ath/ath_hal/ar5416/ar5416_ani.c:228. HW/module-gated on QEMU guest.
No comments yet.