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)
PoC verification
Evidence pack
findings/poc/DF-5518 Β· 26 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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: plaindevice_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 providersc->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.
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
INT33FFdevice\_SB.GPO0(_UID "2"-> chv_n_ranges, 59 pins),_CRSMemory32Fixed at the option-ROM window 0xD0000 (not in the loader SMAP, soram0never 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 atbus_alloc_resource_anybecause DF'sacpi_sysres_allocbails out early (nexus has no GET_RESOURCE_LIST) soacpi_rman_memis never populated, and the nexus fallback loses to ram0's reservation β see findings/poc/DF-5518/diag traces in git history. TZ0._TMPreads the 59-bitFLD0(Connection(GpioIo) over all 59 valid pins, IoRestrictionNone); oncegpoke.koplants 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 fullgpio_acpi_space_handlerdispatch (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
_REGmethod withSleep(200)on disconnect widens the detach-side window the way real firmware_REGmethods (which do real work) do:AcpiEvDetachRegionreleases the namespace mutex aroundAcpiEvExecuteRegMethod(evregion.c:555-581), so GPIO field accesses keep dispatching throughout (measured: +5623 hammer iterations duringdevice_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): holdgpio_intel0's provider lock from the trigger kmod so the next dispatch blocks mid-handler (insidegpio_intel_alloc_io_pin'slockmgr), rundevice_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.koon 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
fixedfix.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)
Confirmed kernel references
- sys/bus/gpio/gpio_acpi/gpio_acpi.c:486
- sys/bus/gpio/gpio_acpi/gpio_acpi.c:494
- sys/bus/gpio/gpio_acpi/gpio_acpi.c:217
- sys/contrib/dev/acpica/source/components/events/evregion.c:392
- sys/contrib/dev/acpica/source/components/events/evregion.c:449
- sys/contrib/dev/acpica/source/components/events/evregion.c:555
- sys/contrib/dev/acpica/source/components/events/evxfregn.c:290
- sys/kern/subr_bus.c:2150
- sys/kern/subr_bus.c:1943
- sys/bus/gpio/gpio_intel/gpio_intel.c:170
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.
No comments yet.