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

gpio_acpi detach does not quiesce in-flight GPIO address-space handler dispatches: use-after-free of softc/provider state (OpRegion half of the missing-drain defect; complements DF-1094/DF-2307)

Summary

Detach removes the address-space handler and returns while a dispatch can still run: ACPICA executes non-default handlers with the interpreter lock released and AcpiRemoveAddressSpaceHandler neither waits nor takes a dispatch reference, so the teardown (softc+device_t freed, parent frees MMIO/IRQ, lockuninit) races the running handler into freed state. Full planted-ACPI rig: fake INT33FF controller with a RAM-window register image brings the STOCK drivers up on QEMU; a 59-pin GpioIo field hammered ~19k dispatches/s by TZ0._TMP; holding the provider lock then detaching mid-dispatch -> Fatal trap 12 at gpio_cherryview_read_pin+0x4c (guest dead); detaching-flag + busy-drain fix rebuilt into the guest kernel eliminated the panic on an identically-armed run. Root+concurrency gated, complements the AEI/AcpiOsExecute half (DF-1094/2307).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5518 Β· 26 files
FileTypeDescriptionSize
README.md β€” 3.4 KB ↓ raw
VERDICT.md β€” 6.9 KB ↓ raw
env.txt β€” 1.1 KB view raw
dsdt_gpio.dsl β€” 4.9 KB ↓ download
gen_gpio.py β€” 4.3 KB view raw
gpoke.c β€” 3.1 KB view raw
gpo_detach.c β€” 3.6 KB view raw
gpo_race2.c β€” 3.3 KB view raw
gpo_race3.c β€” 3.0 KB view raw
diag.c β€” 1.9 KB view raw
Makefile.gpoke β€” 77 B ↓ download
Makefile.gpo_detach β€” 87 B ↓ download
build.sh β€” 160 B view raw
run.sh β€” 228 B view raw
race.sh β€” 1.1 KB view raw
build.log β€” 12.0 KB view raw
run.control.log β€” 1.3 KB view raw
run.race.1.log β€” 2.9 KB view raw
run.race.2.log β€” 2.7 KB view raw
run.race2.1.log β€” 2.6 KB view raw
run.race3.stock.log β€” 13.1 KB view raw
run.fixed.log β€” 13.0 KB view raw
panic.txt β€” 36.8 KB view raw
fix.diff β€” 2.2 KB view raw
fix_build.log β€” 705 B view raw
verdict.json β€” 6.9 KB view raw

DF-5518 β€” gpio_acpi: driver detach does not quiesce in-flight GPIO

address-space handler dispatches (UAF of freed softc/provider state)

What this pack contains

A full production-code harness that brings the stock DragonFly gpio_intel + gpio_acpi drivers up on the QEMU guest using only planted ACPI tables (the proven madt-batch/DF-5483 injection vector: kenv hint.acpi.0.rsdp=0x40000000 + QEMU -device loader blob), a fake Cherry-View "register" image in the option-ROM window at 0xD0000, and AML (TZ0._TMP) that continuously evaluates a 59-pin GeneralPurposeIo field so gpio_acpi_space_handler is dispatched thousands of times per second β€” plus race triggers that detach the live device pair mid-dispatch.

planted DSDT \_SB.GPO0 (INT33FF, _UID "2", _CRS: mem 0xD0000+0x8000, irq 5)
  -> gpio_intel0 + gpio_acpi0 attach (stock drivers, unmodified kernel)
  -> OpRegion(GOPR, GeneralPurposeIo) + Connection(GpioIo, 59 pins) + FLD0,59
  -> TZ0._TMP reads FLD0 once, then (once the register image is planted by
     gpoke.ko and the magic pattern matches) hammers @ITER@ more reads
  -> every read: AcpiExReadGpio -> AcpiEvAddressSpaceDispatch
     (interpreter EXITED, evregion.c:392-401) -> gpio_acpi_space_handler
     -> gpioio_alloc_pins -> 59x GPIO_ALLOC_IO_PIN/READ/RELEASE on
        gpio_intel0 (provider lock + MMIO)

Triggers

  • gpo_detach.ko β€” lottery race: plain device_detach+delete+poison-spray while the hammer runs (ΞΌs window; 8 attempts, no natural hit β€” the in-flight handler always completed before the frees became visible).
  • gpo_race2.ko β€” deterministic: hold the provider sc->lk, let the next dispatch block mid-handler, then detach (frees softc/device_t/resources), then release -> handler resumes on freed state -> Fatal trap 12, gpio_cherryview_read_pin+0x4c (panic.txt / run.race2.1.log).
  • gpo_race3.ko β€” fix-validation variant: detach on its own thread, delayed lock release (500 ms). On the stock kernel the outcome depends on heap-reuse state (observed silent once); on the fixed kernel no schedule can fault because detach drains in-flight dispatches.

Build

sh build.sh          # iasl on guest, chain assembly on host, kmods on guest
# race2/race3 are built the same way (see VERDICT.md exact commands)

Run

# control (harness verification, no race):
sh run.sh control
#   expected: gpio_intel0/gpio_acpi0 attach, gpoke plants the pattern,
#   `sysctl -n hw.acpi.thermal.tz0.temperature` -> 34.5C (= 3000 + FLD0),
#   /dev/mem dump at 0xD6000: DBG0=0x55555555 (exact planted 59-bit
#   pattern through the whole stack), DBG1 = climbing hammer counter.

# deterministic race (panic):
#   boot rig; kldload gpoke.ko; sleep 25; kldload gpo_race2.ko
#   expected: "detach returned 0; freeing lock" followed by
#   Fatal trap 12, Stopped at gpio_cherryview_read_pin+0x4c  -> db>

# fix validation:
#   vm.sh reset with-src; apply fix.diff in guest /usr/src;
#   make -j6 nativekernel KERNCONF=X86_64_GENERIC; installkernel; reboot;
#   re-run race3 -> detach returns only after the blocked handler
#   completed (drain), no trap, guest stays up.

Success criterion

Stock kernel: kernel panic (fatal trap) in the in-flight GPIO space handler after detach freed its state. Fixed kernel: same trigger, no panic; detach blocked until the handler drained and then returned cleanly.

VERDICT.md
↓ download raw

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.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

fix.diff applied to the guest's /usr/src copy, kernel rebuilt (X86_64_GENERIC, RC=0) and installed. Baseline stock kernel + live hammer (9543 dispatches/s at trigger) + blocked in-flight handler -> Fatal trap 12 in gpio_cherryview_read_pin (panic.txt). Patched kernel, identical rig and trigger (gpo_race3.ko, fired while armed): no trap, guest stays up, detach completes only after the drain (completion marker 0x600d0000 written after device_detach returns 0). The previously-observed panic is gone.

fix.diff; fix_build.log (kernel install RC=0); run.fixed.log (no Fatal trap, MARK=0x600d0000); panic.txt (baseline)
↓ fix.diffper-fix-DF-5518

Confirmed kernel references

Detail

Exploit chain

planted RSDP/DSDT chain (kenv hint.acpi.0.rsdp=0x40000000 + QEMU loader blob, stock kernel) -> _SB.GPO0 (INT33FF, _UID 2, _CRS mem 0xD0000 irq 5) -> gpio_intel0+gpio_acpi0 attach -> OpRegion(GOPR, GeneralPurposeIo) + Connection(GpioIo, 59 pins) + FLD0,59 -> TZ0._TMP hammers field reads once gpoke.ko plants the register image (magic-gated; DBG0=0x55555555 proves exact pin values flow through the whole stack) -> gpo_race2.ko holds gpio_intel0 sc->lk so the next dispatch blocks inside gpioio_alloc_pins -> device_detach frees HandlerObj/softcs/device_t/resources -> LK_RELEASE resumes the blocked handler -> page fault in gpio_cherryview_read_pin on freed provider state -> DDB, guest dead.

Evidence (decisive lines)

panic.txt / run.race2.1.log: 'gpo_race2: detach returned 0; freeing lock' followed by 'Fatal trap 12 ... fault virtual address = 0x48 ... Stopped at gpio_cherryview_read_pin+0x4c: cmpq $0,0x48(%rcx)' -> db>, vm.sh status down. run.fixed.log + fix.diff: same rig on the fixed kernel with the hammer live (PRE-RACE delta=9543 dispatches/s, ARMED) -> no trap, guest up, detach-completion marker 0x600d0000 at 0xD6008 after the drain. VERDICT.md has the full narrative incl. the harness bring-up (why the register window must live at 0xD0000: ram0 reserves all SMAP RAM into nexus mem_rman and acpi_sysres_alloc never populates acpi_rman_mem).

PoC changes

Built the whole firmware-ABI harness from scratch (no prior gpio-family rig existed: DF-1094 was source-confirmed only): iasl DSDT with GPO0/TZ0/_REG, RSDP-chain generator adapted from DF-5483, gpoke.ko register-image planter via PHYS_TO_DMAP, three race-trigger kmods (lottery/deterministic/fix-validation), AML debug window (DBG0/1/2) for dispatch-level observability. Key empirical discoveries: register window must be at 0xD0000 (not RAM: ram0/nexus reservation, nexus.c:663-680; not via PNP0C02: acpi_sysres_alloc early-returns), IRQ 5 not 10 (SCI), GPIOCFG=0 passes both RX and TX checks, and the hammer must be raced in its ~13s first _TMP window (afterwards the acpi_tz thread silences errors for hours).

Verified recommended fix

Quiesce GPIO address-space dispatches in gpio_acpi_detach: refuse new dispatches via a detaching flag, drain in-flight ones (busy count + wakeup) BEFORE removing the address-space handler and before any teardown (fix.diff).

Verdict

gpio_acpi_detach (gpio_acpi.c:486-497) performs no quiesce of in-flight gpio_acpi_space_handler dispatches: ACPICA runs non-default address-space handlers with the AML interpreter exited (evregion.c:392-401) and AcpiRemoveAddressSpaceHandler (evxfregn.c:290-418) unlinks the region, runs _REG and frees the HandlerObj (incl. its ContextMutex) without waiting for running handlers; device_detach then frees both softcs and the device_t objects (subr_bus.c:2150 -> :1943-1947) and the parent gpio_intel_detach releases the MMIO/IRQ resources and lockuninits the provider lock. A dispatched handler therefore keeps executing against freed state. Reproduced on the stock QEMU guest with a full planted-ACPI-table harness that brings the unmodified gpio_intel/gpio_acpi drivers up on a fake INT33FF controller (register image in the 0xD0000 option-ROM window) and drives ~19k GeneralPurposeIo field dispatches/s from TZ0._TMP. Deterministic trigger (gpo_race2.ko): hold the provider sc->lk so the next dispatch blocks mid-handler, detach (everything freed), release -> Fatal trap 12, Stopped at gpio_cherryview_read_pin+0x4c: cmpq $0,0x48(%rcx) (panic.txt). Lottery variant with a freely running hammer (8 attempts) did not hit naturally: the natural window is interpreter-handoff-bound and freed-but-unreused slab memory keeps working, the classic first-touch UAF lottery. Root-triggered (devctl-style detach / kldunload), race-dependent -> Low, same trigger profile as the known AEI-half findings DF-1094/DF-2307; this is the complementary OpRegion-handler half of the same missing-drain defect.