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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2307 Β· 9 files
FileTypeDescriptionSize
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

fixed
↓ fix.diffper-fix-DF-2307

Confirmed kernel references

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