# DF-2201 — ar5212 GPIO index unvalidated (HALASSERT compiled out)

## Verdict
**REPRODUCED (source-only confirmation).** Bug confirmed by source tracing of
`sys/dev/netif/ath/ath_hal/ar5212/ar5212_gpio.c`. No runtime PoC attempted
because this code path is HW-gated (no Atheros NIC on the audit guest, so
no live `struct ath_hal` exists to exercise the ops vectors).

## Mechanism
`ar5212GpioCfgOutput` (line 41), `ar5212GpioCfgInput` (line 59),
`ar5212GpioSet` (line 76) validate `gpio` only via
`HALASSERT(gpio < AR_NUM_GPIO)` where `AR_NUM_GPIO == 6` (line 32).

`HALASSERT` is defined in `ah_internal.h:665-676`:
```c
#ifdef AH_ASSERT
#define HALASSERT(_x) do { if (!(_x)) ath_hal_assert_failed(...); } while (0)
#else
#define HALASSERT(_x)
#endif
```

`AH_ASSERT` is **only** enabled in `sys/config/LINT64:1105` (the LINT
config), NOT in `sys/config/X86_64_GENERIC` (the default kernel — its ath
lines are `device ath`, `device ath_hal`, `options AH_SUPPORT_AR5416`, no
`AH_ASSERT`). In any production kernel build, `HALASSERT(_x)` expands to
nothing, so the three CfgOutput/CfgInput/Set calls accept any `uint32_t gpio`
without bounds checking.

The fifth entry point, `ar5212GpioSetIntr` (line 104), is **worse**: it has
no HALASSERT at all, just the source comment `/* XXX bounds check gpio */`
at line 109. An out-of-range `gpio` flows straight into:
- `AR_GPIOCR_CR_A(gpio)` = `3 << (2*gpio)`  (ar5212reg.h:755) → UB shift for gpio≥16, OOB MMIO field for 6≤gpio<16
- `AR_GPIOCR_CR_N(gpio)` = `0 << (2*gpio)`  (ar5212reg.h:752)
- `AR_GPIOCR_INT(gpio)`  = `gpio << 12`     (ar5212reg.h:757)
- `(1 << gpio)`           at GpioSet (line 79-80) → UB shift for gpio≥32

Only `ar5212GpioGet` (line 92) has a real runtime check
(`if (gpio < AR_NUM_GPIO)`).

## Threat model / reachability
- Unlike its ar5312/ar5315 siblings, `ar5212_gpio.c` is **always compiled**
  (no `#ifdef` gate around the body, and it IS in the ath_hal Makefile
  SRCS at line 37). So this file is linked into the running `ath_hal.ko`
  on every standard DragonFly kernel that loads the module.
- The vulnerable entry points are exported through `struct ath_hal` ops
  vectors (`ah.h:1477-1483`).
- Callers in `sys/dev/netif/ath/ath/if_ath_led.c:128,142,145` pass
  `sc->sc_ledpin`, `sc->sc_led_pwr_pin`, `sc->sc_led_net_pin` — these come
  from EEPROM. `ar5212_misc.c:157,167` pass `select` (also driver-internal).
- A malicious or corrupted EEPROM could feed an out-of-range `gpio`,
  producing UB shifts and OOB MMIO field selection — local DoS / device
  misbehavior. No path to direct userspace control of `gpio` was found;
  this is a hardening gap with realistic-but-bounded impact.
- **Severity Low** is appropriate per the rubric (HW-gated caller surface,
  requires specific NIC + malicious EEPROM, no demonstrated escalation).

## Why no runtime PoC
- The audit guest has **no Atheros NIC** (`pciconf -l` shows no ath device),
  so no `ath` interface is ever attached and no `struct ath_hal` ops vector
  is ever wired up to these functions at runtime.
- Source-only confirmation is the strongest evidence available without
  bespoke HW; matches the established pattern of the sibling DF-2098.

## Fix
For each `HAL_BOOL` function, add a real runtime guard immediately after the
existing HALASSERT: `if (gpio >= AR_NUM_GPIO) return AH_FALSE;`. For the
`void` function `ar5212GpioSetIntr`, replace the `/* XXX bounds check gpio */`
comment with `if (gpio >= AR_NUM_GPIO) return;`. This is exactly the pattern
applied to the sibling ar5416 fix in DF-2098 and keeps the API contract
(callers already check the return value of `HAL_BOOL` ops).

See `fix.diff` (8 hunks, all `git apply`-able; applied cleanly with
`patch -p1 --forward`, every hunk "succeeded").

## Batch-build status (Phase 8)
- All three sibling fixes (DF-2199 / DF-2200 / DF-2201) applied together to
  `/usr/src` on the `with-src` snapshot (#0 unpatched baseline,
  6.5-DEVELOPMENT, INVARIANTS ON).
- `make -j6 nativekernel KERNCONF=X86_64_GENERIC` → **rc=0, 0 errors, 0
  warnings**.
- `ar5212_gpio.c` IS in the ath_hal Makefile SRCS (line 37), so the patched
  file was actually rebuilt into `ath_hal.ko` with `-Werror`:
  ```
  --- ar5212_gpio.o ---
  cc -c -O2 -pipe -Wall ... -Werror .../ar5212_gpio.c
  ```
  → linked into `ath_hal.ko` clean. This is the strongest validation
  available short of running the patched kernel on real Atheros HW.
- (ar5312/ar5315 SoC siblings separately compiled with their defines;
  see those findings.)

## Kernel references (confirmed during verification)
- `sys/dev/netif/ath/ath_hal/ar5212/ar5212_gpio.c:32` — `AR_NUM_GPIO 6`
- `sys/dev/netif/ath/ath_hal/ar5212/ar5212_gpio.c:41` — HALASSERT only (CfgOutput)
- `sys/dev/netif/ath/ath_hal/ar5212/ar5212_gpio.c:59` — HALASSERT only (CfgInput)
- `sys/dev/netif/ath/ath_hal/ar5212/ar5212_gpio.c:76` — HALASSERT only (Set)
- `sys/dev/netif/ath/ath_hal/ar5212/ar5212_gpio.c:92` — real runtime check (Get)
- `sys/dev/netif/ath/ath_hal/ar5212/ar5212_gpio.c:109` — `/* XXX bounds check gpio */` (SetIntr, none at all)
- `sys/dev/netif/ath/ath_hal/ah_internal.h:665-676` — HALASSERT compiles to nothing unless AH_ASSERT
- `sys/config/LINT64:1105` — only place AH_ASSERT is enabled
- `sys/dev/netif/ath/ath_hal/ar5212/ar5212reg.h:752,755,757` — shift macros
- `sys/dev/netif/ath/ath_hal/Makefile:37` — ar5212_gpio.c is in SRCS (always compiled)
