# DF-5518 Verification Verdict

**Target:** `sys/bus/gpio/gpio_acpi/gpio_acpi.c`
**Class:** use-after-free / missing quiesce at driver detach (OpRegion
address-space-handler half; complementary to the AEI/AcpiOsExecute half
already filed as DF-1094/DF-2307)
**Severity:** Low (root-triggered detach + concurrency; panic/UAF)
**Verification:** reproduced on the QEMU guest with the stock kernel and
planted ACPI tables (production-code harness, no kernel modifications).

## Verdict: REPRODUCED (panic, deterministic trigger)

`gpio_acpi_detach` (gpio_acpi.c:486-497) calls
`AcpiRemoveAddressSpaceHandler` (gpio_acpi.c:494) and returns; the parent
(`gpio_intel_detach`, gpio_intel.c:170-198) then tears down and
`device_detach` itself frees both softcs and the device_t objects
(subr_bus.c:2150 `device_set_driver(dev, NULL)` -> :1943-1947
`kfree(dev->softc)`, and `kobj_delete` frees the device). None of this
waits for a `gpio_acpi_space_handler` dispatch that is already running:
ACPICA executes non-default address-space handlers with the AML
interpreter (namespace mutex) *exited* (evregion.c:392-401), and
`AcpiRemoveAddressSpaceHandler` (evxfregn.c:290-418) / `AcpiEvDetachRegion`
(evregion.c:491-660) unlink the region, run `_REG`, and
`AcpiUtRemoveReference(HandlerObj)` — freeing the handler object and its
`ContextMutex` — without any per-dispatch accounting or drain. A handler
that was dispatched before the region died therefore keeps executing after
everything it touches (`data` = `&sc->space_handler_data`, `dev` =
`sc->parent`, the provider softc/lock/MMIO resource) has been freed.

## Harness (all stock kernel, planted tables only)

* Fake `INT33FF` device `\_SB.GPO0` (`_UID "2"` -> chv_n_ranges, 59 pins),
  `_CRS` Memory32Fixed at the option-ROM window 0xD0000 (not in the loader
  SMAP, so `ram0` never reserves it into nexus mem_rman — nexus.c:663-680 —
  and the VM allocator never reuses it; the kernel maps it at KERNBASE).
  The first attempts used a 0x7D000000 RAM window and a PNP0C02
  sysresource; both failed at `bus_alloc_resource_any` because DF's
  `acpi_sysres_alloc` bails out early (nexus has no GET_RESOURCE_LIST) so
  `acpi_rman_mem` is never populated, and the nexus fallback loses to
  ram0's reservation — see findings/poc/DF-5518/diag traces in git history.
* `TZ0._TMP` reads the 59-bit `FLD0` (Connection(GpioIo) over all 59 valid
  pins, IoRestrictionNone); once `gpoke.ko` plants the register image
  (CTL0 = RXSTATE on even pins, GPIOCFG=0, CTL1=0) the read returns the
  magic 0x5556AA955555555 and _TMP hammers 200000 more reads, each a full
  `gpio_acpi_space_handler` dispatch (alloc+read+release of 59 pins).
  Observables: `sysctl -n hw.acpi.thermal.tz0.temperature` = 3000+FLD0;
  AML debug window at 0xD6000: DBG0 = raw FLD0 (read 0x55555555 low dword,
  i.e. the exact planted pattern), DBG1 = hammer counter (~11k
  dispatches/s), DBG2 = _REG connect/disconnect markers.
* A `_REG` method with `Sleep(200)` on disconnect widens the detach-side
  window the way real firmware `_REG` methods (which do real work) do:
  `AcpiEvDetachRegion` releases the namespace mutex around
  `AcpiEvExecuteRegMethod` (evregion.c:555-581), so GPIO field accesses
  keep dispatching throughout (measured: +5623 hammer iterations *during*
  `device_detach`).

## Race outcomes

* Lottery variant (`gpo_detach.ko`: detach + delete + poison-spray of the
  freed zones while the hammer runs freely): 8 attempts, no natural crash.
  The in-flight handler always completed before the frees became visible
  to it (interpreter handoffs serialize the last dispatch against the
  removal's critical section; and freed-but-unreused memory still looks
  intact on DF's slab allocator).
* Deterministic variant (`gpo_race2.ko`): hold `gpio_intel0`'s provider
  lock from the trigger kmod so the *next* dispatch blocks mid-handler
  (inside `gpio_intel_alloc_io_pin`'s `lockmgr`), run `device_detach`
  (completes: `_REG`, region unlink, HandlerObj+ContextMutex freed, IRQ
  torn down, mem_res released, `lockuninit`, softcs+device_t freed), then
  release the lock:

```
gpo_race2: holding gpio_intel0 sc->lk; next GPIO dispatch will block mid-handler
gpo_race2: detaching with in-flight handler...
gpio_acpi0: detached
gpio_intel0: detached
gpo_race2: detach returned 0; freeing lock

Fatal trap 12: page fault while in kernel mode
cpuid = 5; lapic id = 5
fault virtual address = 0x48
fault code = supervisor read data, page not present
instruction pointer = 0x8:0xffffffff80a7dfdc
...
Stopped at gpio_cherryview_read_pin+0x4c: cmpq $0,0x48(%rcx)
```

  The blocked space handler resumed after the frees and faulted dereferenc-
  ing freed provider state (`gpio_cherryview_read_pin` ->
  `bus_space_read_4` via the freed resource). Guest sits in DDB; ssh dead;
  `vm.sh status` -> down. Captured in panic.txt / run.race2.1.log.

* `gpo_race3.ko` on the stock kernel (detach on its own thread, lock
  released 500 ms later) observed *silent* once: with a reuse gap before
  the handler resumes, freed-but-recycled memory may happen to remain
  readable — the classic first-touch UAF lottery. The defect is identical;
  only the heap state differs.

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

`struct acpi_gpio_handler_data` gains `busy`/`detaching`; the space
handler refuses dispatch once detaching and accounts in-flight calls
(decrement+wakeup at the single exit); `gpio_acpi_detach` sets
`detaching`, removes the address-space handler (stopping new dispatches)
and drains `busy` before any teardown. With the fix, the same
`gpo_race3` trigger cannot fault: the detach thread sleeps in the drain
until the blocked handler completes, and only then returns.

## Fix validation

* Baseline: stock kernel (DragonFly 6.5-DEVELOPMENT #0 Thu Jul 2 06:02:54),
  race2 trigger -> Fatal trap 12, Stopped at gpio_cherryview_read_pin+0x4c,
  guest dead in DDB (panic.txt / run.race2.1.log).
* Patched: fix.diff applied to the guest's /usr/src copy of
  sys/bus/gpio/gpio_acpi/gpio_acpi.c, `make -j6 nativekernel
  KERNCONF=X86_64_GENERIC && make installkernel` -> RC=0 (fix_build.log;
  boot "DragonFly 6.5-DEVELOPMENT #1 Tue Oct 6 19:53:21"). Rig rebuilt on
  the new kernel, hammer armed (PRE-RACE delta = 9543 dispatches/s, i.e.
  the handler that gpo_race3 blocks was provably mid-dispatch), race3
  fired: **no trap, guest stays up, detach-completion marker 0x600d0000
  at 0xD6008 written only after device_detach returned 0** -- the drain
  held the detach until the blocked handler completed. Two additional
  patched-kernel runs with quieter hammers also completed cleanly.
  run.fixed.log is the decisive patched run.

## 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.
