UAF race in gpio_acpi_unmap_aei: deferred AEI event handler dereferences freed info struct
Summary
When GPIO AEI interrupt fires gpio_acpi_aei_handler queues gpio_acpi_handle_event(arg) via AcpiOsExecute(OSL_GPE_HANDLER) passing raw pointer into sc->infos array. During detach gpio_acpi_unmap_aei calls GPIO_TEARDOWN_INTR which stops NEW interrupts but does NOT drain already-queued AcpiOsExecute work items. Then memset-zeros info struct and kfree entire infos array. A queued work item running after kfree invokes gpio_acpi_handle_event on freed memory reading info->dev from freed/zeroed slab - UAF that in common case is NULL-deref kernel panic. Comment XXX Wait until ACPI Event handler has finished acknowledges gap. Requires root + GPIO interrupt in race window. [VERIFIED pass2: reproduced on planted-ACPI boot - real map_intr arming, real OSL GPE FIFO, deferred handle_event on freed info Fatal trap 12 fault 0x28 at device_get_parent of NULL; in-flight refcount drain fix-validated disassembly-verified]
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2307 Β· 9 files| File | Type | Description | Size | |
|---|---|---|---|---|
| dsdt_aei.dsl | β | 2.2 KB | β download | |
| gen_aei.py | β | 4.1 KB | view raw | |
| aei_race.c | β | 6.7 KB | view raw | |
| Makefile | β | 126 B | β download | |
| run.log | β | 2.4 KB | view raw | |
| panic.txt | β | 1.2 KB | view raw | |
| fix.diff | β | 2.1 KB | view raw | |
| verdict.json | β | 4.4 KB | view raw | |
| README.md | β | 2.2 KB | β raw |
DF-2307 β gpio_acpi_unmap_aei() UAF: deferred AEI event handler on freed info
What (sys/bus/gpio/gpio_acpi/gpio_acpi.c:441-458 vs :347/:310-338)
gpio_acpi_aei_handler queues gpio_acpi_handle_event(info) via AcpiOsExecute(OSL_GPE_HANDLER) (:347). unmap_aei's GPIO_TEARDOWN_INTR stops NEW dispatches but drains NOTHING (the "XXX Wait until ACPI Event handler has finished" comment at :452 admits it), then memsets the info and kfrees the whole infos array (:453-455). A queued-but-not-yet-run work item then reads info->dev at :317 on freed/zeroed memory.
Rig (stock kernel, planted ACPI tables)
DSDT with _SB.GPO0 (INT33FF UID 2, mem 0xD0000+0x8000, irq 5) + _AEI GpioInt pin 2 + _E02; RSDP->RSDT->FACP->DSDT chain planted at 0x40000000 (DF-5483/DF-5518 injection family) and the Cherry-View pin-2 CTL0/CTL1 image planted at runtime (boot-time planting cannot survive firmware β see VERDICT.md). aei_race.ko then runs the DETERMINISTIC choreography, all through production code paths:
detach pair -> plant CTL0(2)=0x40008200/CTL1(2)=2 -> re-attach (real gpio_cherryview_map_intr ACCEPTS: "pin=2 ... ctrl0=0x40008200"; intrmaps[4] armed with the REAL gpio_acpi_aei_handler and the REAL info pointer) -> blocker queued first on the real GPE FIFO (acpi_task) -> pin-2 IS bit raised + REAL ISR body (lockmgr sc->lk + sc->fns->intr) dispatches the REAL handler -> REAL AcpiOsExecute queues handle_event(info) behind the blocker -> device_detach(gpio_acpi0) runs unmap_aei (memset+kfree) -> blocker released.
Result (run.log + panic.txt)
Fatal trap 12: page fault ... fault virtual address = 0x28 (info->dev == NULL from the memset: acpi_get_handle( device_get_parent(info->dev)) at gpio_acpi.c:317)
Guest died. Requires the GPIO IRQ race on hardware (root or physical adjacency for the detach); the rig replaces only the physical line assert with an equivalent direct ISR invocation.
Fix
fix.diff adds an inflight refcount (inc before queueing, dec+wakeup in handle_event) and a bounded wait in unmap_aei that degrades to LEAKING the info instead of freeing memory a deferred handler still uses. Validated on the fix kernel (VERDICT.md): same choreography, no panic.
Fix verification
fixedConfirmed kernel references
- sys/bus/gpio/gpio_acpi/gpio_acpi.c:441
- sys/bus/gpio/gpio_acpi/gpio_acpi.c:450
- sys/bus/gpio/gpio_acpi/gpio_acpi.c:452
- sys/bus/gpio/gpio_acpi/gpio_acpi.c:453
- sys/bus/gpio/gpio_acpi/gpio_acpi.c:455
- sys/bus/gpio/gpio_acpi/gpio_acpi.c:310
- sys/bus/gpio/gpio_acpi/gpio_acpi.c:317
- sys/bus/gpio/gpio_acpi/gpio_acpi.c:347
- sys/dev/acpica/Osd/OsdSchedule.c:100
Detail
Exploit chain
planted DSDT _AEI GpioInt pin2 -> real map_intr/SETUP_INTR arming -> deferred AcpiOsExecute item queued -> unmap_aei frees infos array without draining -> deferred handle_event derefs freed info -> Fatal trap 12 (panic; memory-safety UAF read with garbage-pointer potential if the slab is reused)
Evidence (decisive lines)
run.log + panic.txt: full console choreography (intrmaps[4] armed, ISR dispatched, detach returned, blocker released, Fatal trap 12 fault 0x28) and the trap dump
PoC changes
Built the whole rig: dsdt_aei.dsl (+iasl fix: GpioInt needs the DebounceTimeout word), gen_aei.py chain builder (reuses the DF-5518 proven FADT/MADT clone), aei_race.c trigger (detach->plant->re-attach choreography because the register image cannot survive firmware to boot-time attach; blocker-on-GPE-queue interposition; real ISR body invocation). Register-image arithmetic fixed after the first attempt (GPIOEN|GPIOCFG=0x200 is 0x8200 not 0xA00).
Verified recommended fix
inflight refcount (inc before AcpiOsExecute, dec+wakeup in handle_event) + bounded wait in unmap_aei degrading to leaking the info (fix.diff).
Verdict
REPRODUCED deterministically on a stock-kernel planted-ACPI boot (RSDP->RSDT->FACP->DSDT chain at 0x40000000 per the DF-5518 injection family, DSDT carrying _SB.GPO0 + _AEI GpioInt pin 2 + _E02): the rig detaches the boot-attached gpio pair, plants the Cherry-View pin-2 CTL0/CTL1 words (boot-time planting cannot survive firmware), re-attaches through the REAL newbus path -- gpio_cherryview_map_intr accepts (pin=2 trigger=1 polarity=0 ctrl0=0x40008200) and intrmaps[4] is armed with the REAL gpio_acpi_aei_handler and the REAL info pointer -- then queues a blocker on the single-kthread OSL_GPE_HANDLER queue (OsdSchedule.c:100-141 FIFO), raises the pin-2 IS bit and runs the REAL ISR dispatch (lockmgr sc->lk + sc->fns->intr == gpio_cherryview_intr) so aei_handler AcpiOsExecute()s the REAL gpio_acpi_handle_event(info) behind the blocker, detaches gpio_acpi0 (real device_detach -> gpio_acpi_unmap_aei: TEARDOWN/FREE_INTR + memset + kfree with the item still queued; the in-source XXX at :452 admits no draining), and releases the blocker: Fatal trap 12, fault virtual address 0x28 = device_get_parent(NULL) from handle_event info->dev at gpio_acpi.c:317 on the freed/zeroed info. Two clean stock runs panicked identically. Guest dies. Root/physical-adjacency gated (kmod trigger replaces the physical GPIO line assert).
No comments yet.