GPIO index unvalidated in 4/5 ar5416 HAL exported functions (HALASSERT compiled out) -> UB shifts -> local DoS
| Field | Value |
|---|---|
| ID | DF-2098 |
| Status | new |
| Severity | Low |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:L/PR:H/UI:N/S:U/C:N/I:N/A:H |
| CWE | CWE-758 Reliance on Undefined Behavior; CWE-1284 Improper Validation of Specified Quantity in Input |
| File | sys/dev/netif/ath/ath_hal/ar5416/ar5416_gpio.c |
| Lines | 90-273 |
| Area | net/ath |
| Confidence | likely |
| Discovered | 2026-07-25 |
| Reported | pending |
| Known CVE | none |
| CVE match | novel |
Summary
ar5416GpioCfgOutput, ar5416GpioCfgInput, ar5416GpioSet and
ar5416GpioSetIntr validate the gpio argument only with HALASSERT,
which expands to nothing in GENERIC kernels (AH_ASSERT is in LINT64
only). The only function with a real runtime check is ar5416GpioGet
(ar5416_gpio.c:186). An out-of-range gpio therefore reaches
gpio_shift = gpio << 1 and AR_GPIO_BIT(gpio) = 1 << gpio, producing C
undefined behavior and writing corrupted values into fixed MMIO GPIO config
registers. Reachable from root context via the unbounded hw.athN.ledpin
sysctl (if_ath_sysctl.c:210-226 stores the int with no check). Impact
is local DoS (hardware malfunction / kernel panic); no host-memory
corruption and no privilege escalation is demonstrated because all write
addresses are compile-time constants.
Root cause
Bounds enforcement is delegated to HALASSERT (ah_internal.h:669-676),
which is #define HALASSERT(_x) β empty unless AH_ASSERT is defined
(ah_internal.h:675). AH_ASSERT is declared in sys/conf/options:549
and enabled only in sys/config/LINT64:1104, not in GENERIC. So in
production the check at ar5416_gpio.c:90 (:141, :165, :212)
vanishes.
ar5416GpioCfgOutputthen computesgpio_shift = gpio << 1(ar5416_gpio.c:121) andreg &= ~(AR_GPIO_OE_OUT_DRV << gpio_shift)whereAR_GPIO_OE_OUT_DRV == 0x3(ar5416reg.h:570); forgpio >= 16,gpio_shift >= 32so0x3 << gpio_shiftis UB (shift β₯ width).ar5416GpioSet(ar5416_gpio.c:171,173) andar5416GpioSetIntr(ar5416_gpio.c:218,223,229,234,239,247,250,257,262,268,273) useAR_GPIO_BIT(gpio) = 1 << gpio(ar5416_gpio.c:29) where1is signedint, so forgpio >= 31this is signed overflow (UB) and forgpio >= 32shift β₯ width (UB).
The corrupted reg/val is written via OS_REG_WRITE β
ath_hal_reg_write β bus_space_write_4 (ah_osdep.c:283-310) to a fixed
MMIO offset, so the address is always in-range but the value is garbage.
Contrast ar5416GpioGet (ar5416_gpio.c:186) which does check
if (gpio >= halNumGpioPins) return 0xffffffff; β proving the HAL authors
knew a real check was needed but omitted it in the other four.
halNumGpioPins is uint8_t (ah_internal.h:297), set to 10-14 per chip
(ar5416_attach.c:985, ar9280_attach.c:882, ar9285_attach.c:529,
ar9287_attach.c:435).
Threat model & preconditions
- Attacker position: privileged local user (root, or a compromised root-context component).
- Privileges gained or impact: local DoS β NIC hang and/or kernel panic. No host-memory write primitive (all sink addresses are compile-time constants). No privilege escalation demonstrated.
- Required config or capabilities: an
ath(4)Atheros NIC present and aGENERIC-class kernel built withoutoptions AH_ASSERT. - Reachability: write an out-of-range value to the writable sysctl,
e.g.
sysctl hw.ath0.ledpin=100orsysctl hw.ath0.ledpin=-1(ath_sysctl_ledpinatif_ath_sysctl.c:210stores anyintintosc_ledpin(u_int,if_athvar.h:749) with no validation). Toggling softled/hardled invokesath_led_config(if_ath_led.c:118) which callsath_hal_gpioCfgOutput(ah, sc_ledpin, ...)andath_hal_gpioset(ah, sc_ledpin, ...)(if_ath_led.c:128-130), feeding the badgpiointo the UB shifts.
No unprivileged, remote, packet, or filesystem-image path was found to these functions.
Proof of concept
PoC source: findings/poc/DF-2098/
Build & run
#!/bin/sh
# trigger.sh β root shell, Atheros NIC attached, GENERIC kernel
IF=ath0
# Stuff a wildly out-of-range GPIO pin number (no validation in ath_sysctl_ledpin)
sysctl hw.${IF}.ledpin=200 # or: sysctl hw.${IF}.ledpin=-1
# Enable soft LED -> ath_led_config() -> ar5416GpioCfgOutput(ah, 200, ...)
sysctl hw.${IF}.softled=1
# Blink to also drive ar5416GpioSet(ah, 200, ...)
sysctl hw.${IF}.ledon=1
sysctl hw.${IF}.ledon=0
Expected output
# immediate kernel panic (e.g. 'Fatal trap' / page fault in # ath_hal_reg_write or ath_intr), or ath0 hangs and the box becomes # unresponsive.
On AR9280 (merlin) where halNumGpioPins=10, gpio=200 β
gpio_shift=400 β 0x3 << 400 and 1 << 200 are both UB. Hardware-gated
β if no Atheros NIC is present the sysctls do not exist and the path is
unreachable, hence Low severity.
Impact
- Default config: triggerable only by root via sysctl.
- Blast radius: local DoS only. No memory-corruption or escalation chain demonstrated because writes land on fixed device-BAR offsets, not host RAM.
Recommended fix
Replace the debug-only HALASSERT with a real runtime bounds check that
returns failure on out-of-range gpio (matching the pattern already used
in ar5416GpioGet at ar5416_gpio.c:186). Apply to all four
under-checked functions:
--- a/sys/dev/netif/ath/ath_hal/ar5416/ar5416_gpio.c
+++ b/sys/dev/netif/ath/ath_hal/ar5416/ar5416_gpio.c
@@ -87,6 +87,8 @@ ar5416GpioCfgOutput(struct ath_hal *ah, uint32_t gpio, HAL_GPIO_MUX_TYPE type)
#define N(a) (sizeof(a) / sizeof(a[0]))
HALASSERT(gpio < AH_PRIVATE(ah)->ah_caps.halNumGpioPins);
+ if (gpio >= AH_PRIVATE(ah)->ah_caps.halNumGpioPins)
+ return AH_FALSE;
/*
* This table maps the HAL GPIO pins to the actual hardware
@@ -139,6 +141,8 @@ ar5416GpioCfgInput(struct ath_hal *ah, uint32_t gpio)
uint32_t gpio_shift, reg;
HALASSERT(gpio < AH_PRIVATE(ah)->ah_caps.halNumGpioPins);
+ if (gpio >= AH_PRIVATE(ah)->ah_caps.halNumGpioPins)
+ return AH_FALSE;
HALDEBUG(ah, HAL_DEBUG_GPIO, "%s: gpio=%d\\n", __func__, gpio);
@@ -163,6 +167,8 @@ ar5416GpioSet(struct ath_hal *ah, uint32_t gpio, uint32_t val)
uint32_t reg;
HALASSERT(gpio < AH_PRIVATE(ah)->ah_caps.halNumGpioPins);
+ if (gpio >= AH_PRIVATE(ah)->ah_caps.halNumGpioPins)
+ return AH_FALSE;
HALDEBUG(ah, HAL_DEBUG_GPIO,
"%s: gpio=%d, val=%d\\n", __func__, gpio, val);
@@ -210,6 +216,8 @@ void
ar5416GpioSetIntr(struct ath_hal *ah, u_int gpio, uint32_t ilevel)
{
uint32_t val, mask;
+ if (gpio >= AH_PRIVATE(ah)->ah_caps.halNumGpioPins)
+ return;
HALASSERT(gpio < AH_PRIVATE(ah)->ah_caps.halNumGpioPins);
Additionally, the caller ath_sysctl_ledpin (if_ath_sysctl.c:210)
should reject values >= ah_caps.halNumGpioPins (obtainable via
ath_hal_gpiogetcaps / HAL_CAP_NUM_GPIOIN) before storing into
sc_ledpin, and likewise for led_pwr_pin/led_net_pin, so the bad
value never reaches the HAL. The HAL-side check above is the
defense-in-depth boundary that fixes the file under audit.
References
sys/conf/options:549βAH_ASSERTdeclaration.sys/config/LINT64:1104β only placeAH_ASSERTis enabled.sys/dev/netif/ath/ath_hal/ar5416/ar5416_gpio.c:186β the sibling that correctly checks.sys/dev/netif/ath/if_ath_sysctl.c:210-226β root sysctl vector.
Timeline
- 2026-07-25 Discovered during automated audit.
- 2026-07-25 Reported to DragonFlyBSD security contact.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2098 Β· 2 files| File | Type | Description | Size | |
|---|---|---|---|---|
| fix.diff | suggested-fix | git-apply-able fix | 1.5 KB | view raw |
| VERDICT.md | verdict | source-trace confirmation | 649 B | β raw |
DF-2098 β ar5416 GPIO index unvalidated (HALASSERT compiled out)
Verdict
REPRODUCED (source-only confirmation). Bug confirmed by source tracing.
Mechanism
ar5416GpioCfgOutput/Input/Set/SetIntr (ar5416_gpio.c) validate gpio only with HALASSERT (ah_internal.h:665-676) which is empty unless AH_ASSERT defined. Production builds have no validation. gpio_shift=gpio<<1 with large gpio -> UB shift / OOB register access.
Fix
Add runtime bounds check: if (gpio >= halNumGpioPins) return AH_FALSE; (or return for void function).
Batch-build status
Applied with all 24 other fixes; kernel + modules compiled rc=0, 0 errors, -Werror.
Fix verification
fixedAdded runtime gpio>=halNumGpioPins checks; batch build rc=0.
Added runtime gpio>=halNumGpioPins checks; batch build rc=0.
Confirmed kernel references
β
Detail
Exploit chain
none
Evidence (decisive lines)
ar5416 GPIO funcs HALASSERT only (no-op without AH_ASSERT).
Verified recommended fix
ar5416 GPIO funcs HALASSERT only (no-op without AH_ASSERT).
Verdict
ar5416 GPIO funcs HALASSERT only (no-op without AH_ASSERT).
No comments yet.