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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5541 Β· 15 files
FileTypeDescriptionSize
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 commands
  • build.log β€” kmod build output; fix_build.log β€” patched-kernel build
  • run.log β€” decisive baseline console (boot + harness + panic)
  • run.fixed.log β€” patched-kernel run (guard fires, no panic)
  • panic.txt β€” panic signature + backtrace
  • env.txt β€” guest uname
  • gpio_chain.bin β€” planted ACPI chain (copied from DF-5518)
  • fix.diff β€” the verified fix (never applied to the audit tree's sys/)
VERDICT.md
↓ download raw

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_aei looping num_aei entries whose cookies were handed out under the sc->lk the 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):

  1. plants pin 8's register image (CTL0 = 5<<28|GPIOEN, CTL1 = INTCFG 2) via PHYS_TO_DMAP,
  2. CONTROL: GPIO_ALLOC_INTR(8) β†’ GPIO_SETUP_INTR β†’ GPIO_TEARDOWN_INTR β†’ GPIO_FREE_INTR β€” passes cleanly,
  3. DEFECT: GPIO_FREE_INTR again 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 no pin_intr_map, return ENXIO instead of 0 with *cookiep untouched (gpio_acpi.c:359 hands an uninitialized stack cookie to GPIO_SETUP_INTR in that case β†’ wild kernel write at cookie+8/cookie+16).
  • gpio_intel_alloc_io_pin: reject pin > 0xFFFF before the truncating pin_exists() check (interface is u_int, storage int, validation uint16_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), identical gfree2.ko re-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

fixed
baseline reproduced→ patch + rebuild →patched clean

fix.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
↓ fix.diffper-fix-DF-5541

Confirmed kernel references

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 include (PHYS_TO_DMAP needs pmap defs), plant pin-8 register image (CTL0=5<<28|GPIOEN, CTL1=INTCFG 2) so GPIO_ALLOC_INTR passes map_intr's sanity block, CONTROL leg (documented SETUP->TEARDOWN->FREE) before the DEFECT leg (second FREE on the stale cookie), softc/rman introspection printing the exact KVA and OOB offsets. Rig reused verbatim from DF-5518 (gpio_chain.bin, kenv hint.acpi.0.rsdp, QEMU loader blob at 0x40000000).

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.