gpio_intel teardown methods trust the cookie: stale/double GPIO_FREE_INTR reaches gpio_cherryview_unmap_intr with pin == -1, uint16_t-wrapped to 65535 β OOB kernel-KVA read + masked write ~4.5MB past the BAR mapping (deterministic KKASSERT panic on INVARIANTS kernels)
Summary
The only cookie validation is an INVARIANTS-only KKASSERT whose uint16_t parameter truncates map->pin before the range check; on production kernels nothing stands between a stale cookie (map->pin==-1 after first unmap) and unmap_intr, where the uint16_t conversion yields pin 65535 and PIN_CTLx(65535) addresses KVA+0x448800/0x448804 - 4.5MB past the 0x8000 BAR mapping (fixed-offset masked OOB write). Reproduced on the planted-ACPI rig: control leg passes, stale-cookie GPIO_FREE_INTR panics the stock INVARIANTS kernel at gpio_intel.c:256 (guest dead in DDB). Also closes the alloc_intr cookiep contract hole (backend success without registered map -> gpio_acpi.c:359 uninit stack cookie into setup_intr wild writes) and the u_int/int pin aliasing. Kernel-internal reachability only (no /dev/gpio; sole caller gpio_acpi) - Info per contract; real-guards fix validated (patched kernel logs 'stale cookie' and survives).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5541 Β· 15 files| File | Type | Description | Size | |
|---|---|---|---|---|
| gfree2.c | β | 5.9 KB | view raw | |
| Makefile.gfree2 | β | 89 B | β download | |
| build.sh | β | 102 B | view raw | |
| run.sh | β | 148 B | view raw | |
| build.log | β | 5.8 KB | view raw | |
| fix_build.log | β | 5.6 MB | β download | |
| run.log | β | 2.7 KB | view raw | |
| run.fixed.log | β | 1.1 KB | view raw | |
| panic.txt | β | 890 B | view raw | |
| console_full.txt | β | 2.7 KB | view raw | |
| env.txt | β | 182 B | view raw | |
| gpio_chain.bin | β | 4.5 KB | β download | |
| fix.diff | β | 1.6 KB | view raw | |
| README.md | β | 3.5 KB | β raw | |
| VERDICT.md | β | 6.3 KB | β raw |
DF-5541 β gpio_intel teardown methods trust the cookie: stale/double GPIO_FREE_INTR reaches the backend with pin == -1 β uint16_t wrap to 65535 β OOB KVA read+write ~4.5MB past the BAR mapping (KKASSERT panic on INVARIANTS kernels)
Target: sys/bus/gpio/gpio_intel/gpio_intel.c (GPIO method layer; backend:
sys/bus/gpio/gpio_intel/gpio_cherryview.c)
Class: missing input validation on a kernel-internal API β OOB MMIO
register access (memory corruption primitive) / KKASSERT panic
Severity: Info (defense-in-depth; trigger requires kernel-internal
cookie misuse β root KLD today, driver-consumer bug / DF-5518-class teardown
race in the field)
Bucket: memcorrupt (primitive) / base:hw (reachability)
Reproduce
cd findings/poc/DF-5541
sh run.sh # boots the DF-5518 planted-ACPI rig, builds gfree2.ko,
# kldloads it: CONTROL (documented alloc/teardown/free)
# passes, then the second GPIO_FREE_INTR on the stale
# cookie fires the defect
Expected on the stock guest kernel (INVARIANTS build):
gfree2[CONTROL]: documented order done; map5 now: pin=-1 intidx=-1 (freed cleanly) gfree2[DEFECT]: calling GPIO_FREE_INTR on the stale cookie again (map->pin == -1)... panic: assertion "gpio_intel_pin_exists(sc, map->pin)" failed in gpio_intel_free_intr at /usr/src/sys/bus/gpio/gpio_intel/gpio_intel.c:256
guest dead in DDB (panic.txt, run.log).
On a production (non-INVARIANTS) kernel the KKASSERT is compiled out
(sys/sys/systm.h:98/118) and the same call proceeds into
gpio_cherryview_unmap_intr (gpio_cherryview.c:380-414) whose
uint16_t pin = map->pin; maps -1 β 65535, so the register restore
reads PIN_CTL1(65535) = resource KVA + 0x448804 and conditionally writes
PIN_CTL0(65535)/PIN_CTL1(65535) = KVA + 0x448800/0x448804 β ~4.5MB past
the 0x8000-sized pmap_mapdev() mapping (harness prints the exact KVA and
offsets; see run.log / run.fixed.log header lines).
Fix validation
fix.diff replaces the KKASSERTs in gpio_intel_free_intr /
gpio_intel_setup_intr / gpio_intel_teardown_intr with real
map->pin < 0 || !pin_exists() guards, closes the *cookiep contract hole
in gpio_intel_alloc_intr (backend success without a registered map now
returns ENXIO instead of success-with-uninitialized-cookie), and rejects
pins > 0xFFFF in gpio_intel_alloc_io_pin (the pin_exists() parameter
truncates u_int β uint16_t).
Applied in the guest's /usr/src copy, kernel rebuilt
(fix_build.log, BUILD-RC=0), rebooted (#1: Wed Oct 7 07:32:27 UTC 2026),
same gfree2.ko re-loaded β no panic, guest alive:
gfree2[DEFECT]: calling GPIO_FREE_INTR on the stale cookie again (map->pin == -1)... gpio_intel0: gpio_intel_free_intr: stale cookie (pin -1 not mapped)
(run.fixed.log; the harness's unconditional "SURVIVED" print follows the
kernel guard line, which proves unmap_intr never ran.)
Files
gfree2.c,Makefile.gfree2β production-code harness (stock kernel)build.sh/run.shβ exact build & run commandsbuild.logβ kmod build output;fix_build.logβ patched-kernel buildrun.logβ decisive baseline console (boot + harness + panic)run.fixed.logβ patched-kernel run (guard fires, no panic)panic.txtβ panic signature + backtraceenv.txtβ guest unamegpio_chain.binβ planted ACPI chain (copied from DF-5518)fix.diffβ the verified fix (never applied to the audit tree'ssys/)
DF-5541 Verification Verdict
Target: sys/bus/gpio/gpio_intel/gpio_intel.c
Class: missing cookie/pin validation in the GPIO method teardown layer β
backend OOB register access (uint16_t pin conversion of -1 β 65535 β
PIN_CTLx(65535) β KVA base + 0x448800) β KKASSERT panic on INVARIANTS
kernels, silent OOB kernel-memory read + masked write on production kernels.
Severity: Info (defense-in-depth). Trigger requires kernel-internal misuse
of the cookie API (root KLD today; a gpio_acpi consumer bug or a
DF-5518-class detach/in-flight race in the field).
Verdict: REPRODUCED (panic on the stock guest kernel; production-kernel
OOB write traced line-by-line)
Root cause
gpio_intel_free_intr (gpio_intel.c:250-263) validates the caller's cookie
only with
KKASSERT(gpio_intel_pin_exists(sc, map->pin));
KKASSERT is INVARIANTS-only (sys/sys/systm.h:98 vs :118) β a no-op on
production kernels β and the gpio_intel_pin_exists parameter itself is
uint16_t, so the check cannot see width problems anyway. A stale cookie
(map->pin == -1 after a first unmap_intr, gpio_cherryview.c:393) therefore
falls through to sc->fns->unmap_intr(sc, map) β gpio_cherryview_unmap_intr
(gpio_cherryview.c:380-414):
uint16_t pin = map->pin; /* -1 -> 65535 */
...
reg = chvgpio_read(sc, PIN_CTL1(pin)); /* KVA + 0x448804 */
if ((reg & CHV_GPIO_CTL1_INTCFG_MASK) != intcfg) {
reg &= ~CHV_GPIO_CTL1_INTCFG_MASK; reg |= intcfg; /* intcfg = 0
chvgpio_write(sc, PIN_CTL1(pin), reg); * after 1st free */
}
reg = chvgpio_read(sc, PIN_CTL0(pin)); /* KVA + 0x448800 */
if ((reg & CHV_GPIO_CTL0_GPIOCFG_MASK) != gpiocfg) { ... chvgpio_write ... }
PIN_ADDRESS(65535) = 0x4400 + (65535/15)0x400 + (65535%15)8 = 0x448800 β
~4.5MB past the 0x8000 pmap_mapdev() mapping of the controller BAR.
bus_read_4/bus_write_4 add the offset to the resource KVA with no bounds
check: an out-of-bounds kernel virtual-memory read, and (whenever the read
value's bits 0..2 / 8..10 are nonzero β orig_intcfg/orig_gpiocfg are 0
after the first free) an out-of-bounds write that clears those bits at
KVA+0x448800 and KVA+0x448804. Same shape in gpio_intel_setup_intr
(gpio_intel.c:272) and gpio_intel_teardown_intr (gpio_intel.c:287).
Reachability (why Info, not higher)
The GPIO method layer has no userland entry (no /dev/gpio in DragonFly;
rg GPIO_FREE_INTR β only sys/bus/gpio/gpio_acpi/gpio_acpi.c:451). Today's
sole caller frees each AEI cookie exactly once. The realistic kernel-internal
mistakes that reach the defect:
- double
GPIO_FREE_INTR/ free-after-slot-reuse by a consumer, - teardown-order bugs of exactly the kind this family keeps finding (DF-5518 proved dispatches and detaches race; DF-5527 proved the documented teardown order itself leaves the line armed),
- the same family's
gpio_acpi_unmap_aeiloopingnum_aeientries whose cookies were handed out under thesc->lkthe racing detach path also touches.
Hence: root-privilege KLD on a running system (proven here), or buggy/malicious in-kernel consumers. No unprivileged path exists.
Harness (stock kernel, planted tables only β DF-5518 rig reused)
Fake INT33FF community _UID "2", _CRS Memory32Fixed 0xD0000+0x8000 +
IRQ 5 (gpio_chain.bin, loader blob at 0x40000000, kenv
hint.acpi.0.rsdp). gfree2.ko (production code only):
- plants pin 8's register image (CTL0 = 5<<28|GPIOEN, CTL1 = INTCFG 2) via
PHYS_TO_DMAP, - CONTROL:
GPIO_ALLOC_INTR(8)βGPIO_SETUP_INTRβGPIO_TEARDOWN_INTRβGPIO_FREE_INTRβ passes cleanly, - DEFECT:
GPIO_FREE_INTRagain on the same stale cookie.
Baseline result (stock kernel, INVARIANTS snapshot)
gfree2[DEFECT]: calling GPIO_FREE_INTR on the stale cookie again (map->pin == -1)...
panic: assertion "gpio_intel_pin_exists(sc, map->pin)" failed in gpio_intel_free_intr at /usr/src/sys/bus/gpio/gpio_intel/gpio_intel.c:256
gpio_intel_free_intr() at gpio_intel_free_intr+0x74
gfree2_modevent() at gfree2_modevent+0x2ae
linker_load_module() at linker_load_module+0x116
Debugger("panic")
Guest dead in DDB, ssh dead, vm.sh status β down (panic.txt, run.log).
The stock guest snapshot is an INVARIANTS build, so on THIS kernel the defect
is a deterministic panic (local DoS, root-triggered). On production kernels
the same path executes the OOB KVA read/write described above β traced
line-by-line here; the harness prints the exact OOB targets
(KVA=0xffffffff800d0000 size=0x8000, offsets 0x448800/0x448804) from the
live softc before firing.
Fix (fix.diff, applied in the guest's /usr/src copy for validation)
gpio_intel_free_intr/setup_intr/teardown_intr: replace the KKASSERT with a real guard (map->pin < 0 || !pin_exists()β log + return).gpio_intel_alloc_intr: if the backend reports success but registered nopin_intr_map, returnENXIOinstead of0with*cookiepuntouched (gpio_acpi.c:359 hands an uninitialized stack cookie toGPIO_SETUP_INTRin that case β wild kernel write atcookie+8/cookie+16).gpio_intel_alloc_io_pin: rejectpin > 0xFFFFbefore the truncatingpin_exists()check (interface isu_int, storageint, validationuint16_t).
Fix validation
- Baseline: stock kernel β panic (above).
- Patched: fix.diff applied,
make -j6 nativekernel KERNCONF=X86_64_GENERIC(fix_build.log,BUILD-RC=0), installed, rebooted (#1: Wed Oct 7 07:32:27 UTC 2026), identicalgfree2.kore-loaded:
gfree2[DEFECT]: calling GPIO_FREE_INTR on the stale cookie again (map->pin == -1)... gpio_intel0: gpio_intel_free_intr: stale cookie (pin -1 not mapped)
No panic, KLDLOAD-RC=0, guest alive, the rig's own thermal-zone GpioIo
polls keep working (run.fixed.log). The unconditional "SURVIVED" harness
print that follows is a harness artifact; the kernel guard line proves
unmap_intr never ran on the stale map.
Guest reset to the clean-source snapshot after the runs.
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 (fix_build.log BUILD-RC=0) and rebooted (#1 Wed Oct 7 07:32:27 2026); the identical gfree2.ko run that panicked the baseline kernel now logs 'gpio_intel0: gpio_intel_free_intr: stale cookie (pin -1 not mapped)', returns KLDLOAD-RC=0, leaves the guest alive and the rig's own GpioIo thermal-zone polls working (run.fixed.log). The previously-observed panic is GONE.
run.fixed.log; fix_build.log; VERDICT.md 'Fix validation' section
Confirmed kernel references
- sys/bus/gpio/gpio_intel/gpio_intel.c:250-263
- sys/bus/gpio/gpio_intel/gpio_intel.c:266-294
- sys/bus/gpio/gpio_intel/gpio_intel.c:226-239
- sys/bus/gpio/gpio_intel/gpio_intel.c:296-341
- sys/bus/gpio/gpio_intel/gpio_intel_var.h:9-17
- sys/bus/gpio/gpio_intel/gpio_intel_var.h:37-40
- sys/bus/gpio/gpio_intel/gpio_cherryview.c:380-414
- sys/bus/gpio/gpio_intel/gpio_cherryview.c:75-81
- sys/bus/gpio/gpio_acpi/gpio_acpi.c:355-387
- sys/sys/systm.h:98
- sys/sys/systm.h:118
Detail
Exploit chain
root KLD (or in-kernel consumer bug) double-frees a GPIO intr cookie -> gpio_intel_free_intr KKASSERT no-op on production kernels -> gpio_cherryview_unmap_intr with pin -1 -> uint16_t 65535 -> PIN_CTLx(65535) = KVA+0x448800/0x448804 -> OOB kernel-memory read + masked write at a fixed offset past the controller BAR mapping (not attacker-positioned/valued; no unprivileged path - GPIO methods have no userland entry). On INVARIANTS kernels: immediate panic (DoS).
Evidence (decisive lines)
panic.txt (panic at gpio_intel.c:256 + backtrace through gfree2_modevent/linker_load_module); run.log (decisive baseline console: CONTROL clean, DEFECT panic, harness prints mem_res KVA=0xffffffff800d0000 size=0x8000 and OOB offsets 0x448800/0x448804); run.fixed.log (patched kernel: 'gpio_intel0: gpio_intel_free_intr: stale cookie (pin -1 not mapped)', KLDLOAD-RC=0, guest alive); fix_build.log (BUILD-RC=0); VERDICT.md line-by-line trace of the production-kernel OOB path
PoC changes
Seed sketch rewritten as gfree2.c modeled on DF-5527's gpiofree.c: added
Verified recommended fix
Replace the KKASSERT cookie validation in gpio_intel_free_intr/setup_intr/teardown_intr with real guards (map->pin < 0 || !gpio_intel_pin_exists -> log + return); return ENXIO from gpio_intel_alloc_intr when the backend reports success without registering a map; reject pin > 0xFFFF in gpio_intel_alloc_io_pin (see fix.diff).
Verdict
gpio_intel_free_intr/teardown_intr/setup_intr validate the caller's cookie only with an INVARIANTS-only KKASSERT (a no-op on production kernels, and its uint16_t parameter cannot express the width anyway). A stale/double-freed cookie (map->pin == -1 after the first unmap) reaches gpio_cherryview_unmap_intr, whose 'uint16_t pin = map->pin' maps -1 to 65535, so the register restore reads PIN_CTL1(65535) = resource KVA + 0x448804 and conditionally writes PIN_CTL0/1(65535) = KVA + 0x448800/0x448804, ~4.5MB past the 0x8000 pmap_mapdev mapping of the BAR - an out-of-bounds kernel-virtual-memory read plus a masked write (clears bits 0..2 and 8..10 of the words there). On the guest's stock INVARIANTS snapshot the KKASSERT fires first: deterministic panic at gpio_intel.c:256, reproduced with the DF-5518 planted-ACPI rig + a production-code KLD harness (gfree2.ko: CONTROL documented-order free passes, DEFECT second free panics; guest dead in DDB). No userland entry to the GPIO method layer exists in DragonFly, so the trigger is kernel-internal cookie misuse (root KLD today; consumer teardown-order bugs / DF-5518-class detach races in the field) - hence Info severity for a memcorrupt-shaped primitive. fix.diff adds real guards (map->pin < 0 || !pin_exists -> log+return) in all three teardown methods, closes the alloc_intr *cookiep contract hole (backend success without a registered map now returns ENXIO instead of leaving gpio_acpi.c:359's uninitialized stack cookie to be written through), and rejects pins > 0xFFFF in alloc_io_pin; validated on a rebuilt kernel: the same harness run logs 'stale cookie (pin -1 not mapped)', no panic, guest alive, rig keeps working.
No comments yet.