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

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.

  • ar5416GpioCfgOutput then computes gpio_shift = gpio << 1 (ar5416_gpio.c:121) and reg &= ~(AR_GPIO_OE_OUT_DRV << gpio_shift) where AR_GPIO_OE_OUT_DRV == 0x3 (ar5416reg.h:570); for gpio >= 16, gpio_shift >= 32 so 0x3 << gpio_shift is UB (shift β‰₯ width).
  • ar5416GpioSet (ar5416_gpio.c:171,173) and ar5416GpioSetIntr (ar5416_gpio.c:218,223,229,234,239,247,250,257,262,268,273) use AR_GPIO_BIT(gpio) = 1 << gpio (ar5416_gpio.c:29) where 1 is signed int, so for gpio >= 31 this is signed overflow (UB) and for gpio >= 32 shift β‰₯ 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 a GENERIC-class kernel built without options AH_ASSERT.
  • Reachability: write an out-of-range value to the writable sysctl, e.g. sysctl hw.ath0.ledpin=100 or sysctl hw.ath0.ledpin=-1 (ath_sysctl_ledpin at if_ath_sysctl.c:210 stores any int into sc_ledpin (u_int, if_athvar.h:749) with no validation). Toggling softled/hardled invokes ath_led_config (if_ath_led.c:118) which calls ath_hal_gpioCfgOutput(ah, sc_ledpin, ...) and ath_hal_gpioset(ah, sc_ledpin, ...) (if_ath_led.c:128-130), feeding the bad gpio into 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.

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_ASSERT declaration.
  • sys/config/LINT64:1104 β€” only place AH_ASSERT is 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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2098 Β· 2 files
FileTypeDescriptionSize
fix.diff suggested-fix git-apply-able fix 1.5 KB view raw
VERDICT.md verdict source-trace confirmation 649 B ↓ raw
VERDICT.md verdict source-trace confirmation
↓ download 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

fixed
baseline reproduced→ patch + rebuild →patched clean

Added runtime gpio>=halNumGpioPins checks; batch build rc=0.

Added runtime gpio>=halNumGpioPins checks; batch build rc=0.
↓ fix.diffcombined 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).