# DF-5541 Verification Verdict

**Target:** `sys/bus/gpio/gpio_intel/gpio_intel.c`
**Class:** missing cookie/pin validation in the GPIO method teardown layer →
backend OOB register access (`uint16_t` pin conversion of `-1` → 65535 →
`PIN_CTLx(65535)` ≈ KVA base + 0x448800) — KKASSERT panic on INVARIANTS
kernels, silent OOB kernel-memory read + masked write on production kernels.
**Severity:** Info (defense-in-depth). Trigger requires kernel-internal misuse
of the cookie API (root KLD today; a gpio_acpi consumer bug or a
DF-5518-class detach/in-flight race in the field).

## Verdict: REPRODUCED (panic on the stock guest kernel; production-kernel
## OOB write traced line-by-line)

### Root cause

`gpio_intel_free_intr` (gpio_intel.c:250-263) validates the caller's cookie
only with

```c
KKASSERT(gpio_intel_pin_exists(sc, map->pin));
```

`KKASSERT` is INVARIANTS-only (sys/sys/systm.h:98 vs :118) — a no-op on
production kernels — and the `gpio_intel_pin_exists` parameter itself is
`uint16_t`, so the check cannot see width problems anyway. A stale cookie
(`map->pin == -1` after a first `unmap_intr`, gpio_cherryview.c:393) therefore
falls through to `sc->fns->unmap_intr(sc, map)` → `gpio_cherryview_unmap_intr`
(gpio_cherryview.c:380-414):

```c
uint16_t pin = map->pin;          /* -1 -> 65535 */
...
reg = chvgpio_read(sc, PIN_CTL1(pin));    /* KVA + 0x448804 */
if ((reg & CHV_GPIO_CTL1_INTCFG_MASK) != intcfg) {
        reg &= ~CHV_GPIO_CTL1_INTCFG_MASK; reg |= intcfg;   /* intcfg = 0
        chvgpio_write(sc, PIN_CTL1(pin), reg);               * after 1st free */
}
reg = chvgpio_read(sc, PIN_CTL0(pin));    /* KVA + 0x448800 */
if ((reg & CHV_GPIO_CTL0_GPIOCFG_MASK) != gpiocfg) { ... chvgpio_write ... }
```

`PIN_ADDRESS(65535)` = 0x4400 + (65535/15)*0x400 + (65535%15)*8 = 0x448800 —
**~4.5MB past the 0x8000 `pmap_mapdev()` mapping** of the controller BAR.
`bus_read_4`/`bus_write_4` add the offset to the resource KVA with no bounds
check: an out-of-bounds kernel virtual-memory read, and (whenever the read
value's bits 0..2 / 8..10 are nonzero — `orig_intcfg`/`orig_gpiocfg` are 0
after the first free) an out-of-bounds write that clears those bits at
KVA+0x448800 and KVA+0x448804. Same shape in `gpio_intel_setup_intr`
(gpio_intel.c:272) and `gpio_intel_teardown_intr` (gpio_intel.c:287).

### Reachability (why Info, not higher)

The GPIO method layer has no userland entry (no `/dev/gpio` in DragonFly;
`rg GPIO_FREE_INTR` → only `sys/bus/gpio/gpio_acpi/gpio_acpi.c:451`). Today's
sole caller frees each AEI cookie exactly once. The realistic kernel-internal
mistakes that reach the defect:

* double `GPIO_FREE_INTR` / free-after-slot-reuse by a consumer,
* teardown-order bugs of exactly the kind this family keeps finding
  (DF-5518 proved dispatches and detaches race; DF-5527 proved the documented
  teardown order itself leaves the line armed),
* the same family's `gpio_acpi_unmap_aei` looping `num_aei` entries whose
  cookies were handed out under the `sc->lk` the racing detach path also
  touches.

Hence: root-privilege KLD on a running system (proven here), or buggy/malicious
in-kernel consumers. No unprivileged path exists.

### Harness (stock kernel, planted tables only — DF-5518 rig reused)

Fake `INT33FF` community `_UID "2"`, `_CRS` Memory32Fixed 0xD0000+0x8000 +
IRQ 5 (`gpio_chain.bin`, loader blob at 0x40000000, kenv
`hint.acpi.0.rsdp`). `gfree2.ko` (production code only):

1. plants pin 8's register image (CTL0 = 5<<28|GPIOEN, CTL1 = INTCFG 2) via
   `PHYS_TO_DMAP`,
2. **CONTROL**: `GPIO_ALLOC_INTR(8)` → `GPIO_SETUP_INTR` →
   `GPIO_TEARDOWN_INTR` → `GPIO_FREE_INTR` — passes cleanly,
3. **DEFECT**: `GPIO_FREE_INTR` again on the same stale cookie.

### Baseline result (stock kernel, INVARIANTS snapshot)

```
gfree2[DEFECT]: calling GPIO_FREE_INTR on the stale cookie again (map->pin == -1)...
panic: assertion "gpio_intel_pin_exists(sc, map->pin)" failed in gpio_intel_free_intr at /usr/src/sys/bus/gpio/gpio_intel/gpio_intel.c:256
gpio_intel_free_intr() at gpio_intel_free_intr+0x74
gfree2_modevent() at gfree2_modevent+0x2ae
linker_load_module() at linker_load_module+0x116
Debugger("panic")
```

Guest dead in DDB, ssh dead, `vm.sh status` → down (`panic.txt`, `run.log`).
The stock guest snapshot is an INVARIANTS build, so on THIS kernel the defect
is a deterministic panic (local DoS, root-triggered). On production kernels
the same path executes the OOB KVA read/write described above — traced
line-by-line here; the harness prints the exact OOB targets
(`KVA=0xffffffff800d0000 size=0x8000`, offsets `0x448800/0x448804`) from the
live softc before firing.

### Fix (fix.diff, applied in the guest's /usr/src copy for validation)

* `gpio_intel_free_intr` / `setup_intr` / `teardown_intr`: replace the
  KKASSERT with a real guard (`map->pin < 0 || !pin_exists()` → log + return).
* `gpio_intel_alloc_intr`: if the backend reports success but registered no
  `pin_intr_map`, return `ENXIO` instead of `0` with `*cookiep` untouched
  (gpio_acpi.c:359 hands an uninitialized stack cookie to `GPIO_SETUP_INTR`
  in that case → wild kernel write at `cookie+8`/`cookie+16`).
* `gpio_intel_alloc_io_pin`: reject `pin > 0xFFFF` before the truncating
  `pin_exists()` check (interface is `u_int`, storage `int`, validation
  `uint16_t`).

### Fix validation

* Baseline: stock kernel → panic (above).
* Patched: fix.diff applied, `make -j6 nativekernel KERNCONF=X86_64_GENERIC`
  (`fix_build.log`, `BUILD-RC=0`), installed, rebooted
  (`#1: Wed Oct  7 07:32:27 UTC 2026`), identical `gfree2.ko` re-loaded:

```
gfree2[DEFECT]: calling GPIO_FREE_INTR on the stale cookie again (map->pin == -1)...
gpio_intel0: gpio_intel_free_intr: stale cookie (pin -1 not mapped)
```

No panic, `KLDLOAD-RC=0`, guest alive, the rig's own thermal-zone GpioIo
polls keep working (`run.fixed.log`). The unconditional "SURVIVED" harness
print that follows is a harness artifact; the kernel guard line proves
`unmap_intr` never ran on the stale map.

Guest reset to the clean-source snapshot after the runs.

## Fix status (batch validation)
Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest `make -j6 nativekernel KERNCONF=X86_64_GENERIC` completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.
