gpio_cherryview_unmap_intr never masks the IRQ line: GPIO_FREE_INTR without a preceding GPIO_TEARDOWN_INTR leaves an armed orphaned interrupt (latent irq-storm / hard-lockup DoS)
Summary
unmap_intr destroys the mapping (pin=-1/intidx=-1/is_level=0) but never clears the community MASK register - the only MASK-clearing site is disable_intr, reachable solely via TEARDOWN: FREE without teardown leaves the line armed with no mapping, and a level-asserted pad takes the edge-clear W1C path on a source that re-asserts immediately -> endless IRQ storm/hard lockup. Sole in-tree free path always teardowns first (latent API-contract hazard; attacker-planted-ACPI reachable per DF-5518's model). A/B-proven on the planted-ACPI rig with the STOCK kernel (MASK=0x20 while intrmaps[5].pin=-1; /dev/mem cross-check) and the mask+IS-clear fix on a rebuilt kernel (MASK=0, armed=0). Register map otherwise verified against Linux pinctrl-cherryview byte-for-byte.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5527 Β· 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| gpiofree.c | β | 5.7 KB | view raw | |
| Makefile.gpiofree | β | 93 B | β download | |
| build.sh | β | 66 B | view raw | |
| run.sh | β | 90 B | view raw | |
| run.log | β | 8.2 KB | view raw | |
| run.fixed.log | β | 1.4 KB | view raw | |
| fix_build.log | β | 5.7 MB | β download | |
| fix.diff | β | 1.1 KB | view raw | |
| verdict.json | β | 4.2 KB | view raw | |
| VERDICT.md | β | 4.6 KB | β raw | |
| README.md | β | 2.6 KB | β raw | |
| env.txt | β | 123 B | view raw |
DF-5527 β gpio_cherryview: unmap_intr never masks the IRQ line; free-without-teardown leaves an armed orphaned interrupt
What this pack contains
A production-code harness that runs on the DF-5518 planted-ACPI rig
(stock DragonFly kernel + stock gpio_intel/gpio_acpi drivers brought
up by planted ACPI tables, fake Cherry-View register image at
0xD0000+0x8000). It demonstrates that
gpio_cherryview_unmap_intr() (sys/bus/gpio/gpio_intel/gpio_cherryview.c:380)
never clears the community interrupt MASK bit, so releasing a mapped GPIO
interrupt via GPIO_FREE_INTR without a preceding GPIO_TEARDOWN_INTR
leaves the IRQ line armed while its pin_intr_map is destroyed β
the exact precondition for an endless interrupt storm
(gpio_cherryview_intr takes the is_level==0 edge-clear path on a
level source that re-asserts immediately).
The harness does an A/B experiment in one kldload:
- A (defect path): pin 8 / INTSEL line 5 β
GPIO_ALLOC_INTRβGPIO_SETUP_INTRβGPIO_FREE_INTR(teardown skipped) - B (control): pin 15 / INTSEL line 6 β
GPIO_ALLOC_INTRβGPIO_SETUP_INTRβGPIO_TEARDOWN_INTRβGPIO_FREE_INTR
Build
sh build.sh # uploads gpiofree.c + gpio_intel_var.h, builds kmod on guest
# (expects the rig to be booted: gpio_intel0 present)
Run
sh run.sh # boots the DF-5518 rig chain, builds + loads the harness,
# dumps MASK/IS via the kernel DMAP and /dev/mem
Expected output (stock kernel β defect present)
gpiofree[A-setup]: IS=00000020 MASK=00000020 (bit5=1 ...) map5{pin=8,intidx=5,...}
gpiofree[A-free]: IS=00000020 MASK=00000020 (bit5=1 ...) map5{pin=-1,intidx=-1,...}
^^^^ LINE STILL ARMED, MAPPING DESTROYED
gpiofree: SUMMARY ... line5 armed-with-no-mapping=1 (DEFECT if 1), line6 left-armed=0
With fix.diff applied (kernel rebuilt), the same run shows:
gpiofree[A-free]: IS=00000020 MASK=00000000 (bit5=0 ...) map5{pin=-1,intidx=-1,...}
gpiofree: SUMMARY ... line5 armed-with-no-mapping=0 ...
run.log = stock baseline (full rig boot + harness), run.fixed.log =
patched kernel, fix_build.log = full untrimmed patched-kernel build.
Notes
- The QEMU rig cannot inject a real ISA IRQ 5, so the storm itself is argued from the ISR logic (gpio_cherryview.c:221-235) on top of the reproduced armed-line state; see VERDICT.md.
- In-tree caller
gpio_acpi_unmap_aei(gpio_acpi.c:450-451) always teardowns before freeing, so the defect is currently latent β it is a provider API-contract hazard (defense in depth), hence severity Info.
DF-5527 Verification Verdict
Target: sys/bus/gpio/gpio_intel/gpio_cherryview.c
Class: interrupt-lifecycle defect (missing hardware mask-clear on
free) β latent interrupt-storm DoS / hard lockup
Severity: Info (latent β no in-tree caller misuses the order today)
Verification: reproduced on the QEMU guest with the stock kernel and
the DF-5518 planted-ACPI rig (production-code harness; no kernel
modifications for the baseline); fix validated on a rebuilt kernel.
Verdict: REPRODUCED (state-level), FIX VALIDATED
gpio_cherryview_unmap_intr (gpio_cherryview.c:380-414) destroys the
mapping (map->pin = -1 at :393, map->intidx = -1 at :394,
map->is_level = 0 at :395) and restores the pad's INTCFG/GPIOCFG, but
never touches CHV_GPIO_REG_MASK (0x380). The only code that ever
clears a MASK bit is gpio_cherryview_disable_intr (:441-444), reached
solely via GPIO_TEARDOWN_INTR. The gpio_if.m contract does not
require teardown-before-free, and gpio_intel_free_intr
(gpio_intel.c:250-263) calls unmap_intr directly with no disable.
Consequence for a caller that frees without teardown: the community IRQ
line stays unmasked with no mapping. If the pad then asserts
(level source), gpio_cherryview_intr (:219-235) reads GPIIS, sees the
bit, reads mapping->is_level β which unmap_intr just zeroed β takes
the edge clear path (W1C at :225), the level source re-asserts
GPIIS immediately, the IRQ re-fires: endless interrupt storm β hard
lockup / DoS. (The QEMU rig cannot inject ISA IRQ 5, so the storm loop
itself is argued from the ISR code; the armed-line state β MASK bit
set while intrmaps[i].pin == -1 β is what was reproduced live.)
Harness
gpiofree.c (kmod, stock kernel, DF-5518 rig): finds gpio_intel0
(devclass unit 0), plants two pads in the RAM register image
(pin 8 β INTSEL line 5, pin 15 β INTSEL line 6; CTL0 = INTSEL<<28 |
GPIOEN, GPIOCFG=0; CTL1 = INTCFG 2 = edge/rising to satisfy
map_intr's sanity checks at :260-329), then:
- A (defect path):
GPIO_ALLOC_INTR(8)βGPIO_SETUP_INTRβGPIO_FREE_INTRβ teardown deliberately skipped. - B (control):
GPIO_ALLOC_INTR(15)βGPIO_SETUP_INTRβGPIO_TEARDOWN_INTRβGPIO_FREE_INTR.
State is dumped after every step from inside the kernel (PHYS_TO_DMAP
reads of IS/MASK + sc->intrmaps[]) and cross-checked from userland
with /dev/mem reads at 0xD0300/0xD0380.
Baseline run (stock kernel, run.log)
gpiofree[A-setup]: IS=00000020 MASK=00000020 (bit5=1 bit6=0) map5{pin=8,intidx=5,is_level=0}
gpiofree[A-free]: IS=00000020 MASK=00000020 (bit5=1 bit6=0) map5{pin=-1,intidx=-1,is_level=0}
gpiofree[B-done]: IS=00000040 MASK=00000020 (bit5=1 bit6=0) map6{pin=-1,...}
gpiofree: SUMMARY MASK=00000020: line5 armed-with-no-mapping=1 (DEFECT if 1), line6 left-armed=0
Userland cross-check: /dev/mem @0xD0380 = 20 00 00 00.
After GPIO_FREE_INTR on line 5 the MASK bit is still set while
intrmaps[5].pin == -1 β the orphaned armed line, verbatim. Control
arm B (teardown first) never leaves its bit set.
Fix validation (run.fixed.log, fix_build.log)
fix.diff makes unmap_intr capture map->intidx before destroying
the mapping and, after destroying it, clear the MASK bit and W1C the IS
bit for that line. Applied to the guest's /usr/src copy
(patch -p1, 2/2 hunks), kernel rebuilt
(make -j6 nativekernel KERNCONF=X86_64_GENERIC && make installkernel,
RC=0, kern.version #1: Tue Oct 6 23:06:02), rig rebooted, identical
harness re-run:
gpiofree[A-setup]: IS=00000020 MASK=00000020 (bit5=1 bit6=0) map5{pin=8,intidx=5,is_level=0}
gpiofree[A-free]: IS=00000020 MASK=00000000 (bit5=0 bit6=0) map5{pin=-1,intidx=-1,is_level=0}
gpiofree: SUMMARY MASK=00000000: line5 armed-with-no-mapping=0 (DEFECT if 1), line6 left-armed=0
Userland cross-check: /dev/mem @0xD0380 = 00 00 00 00.
The only variable between the two runs is fix.diff: baseline
armed-with-no-mapping=1 β patched 0. Guest healthy after both runs.
Why Info
The sole in-tree free path, gpio_acpi_unmap_aei (gpio_acpi.c:447-454),
always does GPIO_TEARDOWN_INTR before GPIO_FREE_INTR, so nothing in
the tree triggers the storm today; the provider's gpio_if contract
simply doesn't defend against the natural misuse. Defense-in-depth fix,
one hunk, validated.
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 guest /usr/src (patch -p1, 2/2 hunks), kernel rebuilt with make -j6 nativekernel KERNCONF=X86_64_GENERIC (RC=0, fix_build.log), rig rebooted, identical harness re-run: A-free MASK bit5=0 with map5{pin=-1}, SUMMARY armed-with-no-mapping=0, /dev/mem @0xD0380=0x00000000. Baseline had 1/0x20. Only variable between runs = fix.diff.
run.log vs run.fixed.log decisive lines: 'A-free: MASK=00000020 (bit5=1)' -> 'A-free: MASK=00000000 (bit5=0)'; SUMMARY 'line5 armed-with-no-mapping=1' -> '=0'; fix_build.log tail: 'Kernel install for X86_64_GENERIC completed'; kern.version #1: Tue Oct 6 23:06:02 UTC 2026
Confirmed kernel references
- sys/bus/gpio/gpio_intel/gpio_cherryview.c:380
- sys/bus/gpio/gpio_intel/gpio_cherryview.c:393
- sys/bus/gpio/gpio_intel/gpio_cherryview.c:395
- sys/bus/gpio/gpio_intel/gpio_cherryview.c:221
- sys/bus/gpio/gpio_intel/gpio_cherryview.c:425
- sys/bus/gpio/gpio_intel/gpio_cherryview.c:441
- sys/bus/gpio/gpio_intel/gpio_intel.c:250
- sys/bus/gpio/gpio_acpi/gpio_acpi.c:450
Detail
Evidence (decisive lines)
run.log: gpiofree[A-free]: IS=00000020 MASK=00000020 (bit5=1) map5{pin=-1,intidx=-1} on stock kernel (line armed, mapping destroyed) vs run.fixed.log: gpiofree[A-free]: MASK=00000000 (bit5=0) map5{pin=-1} with fix.diff; SUMMARY lines 'line5 armed-with-no-mapping=1' (stock) vs '=0' (patched); /dev/mem @0xD0380: 0x20 (stock) vs 0x0 (patched); fix_build.log full untrimmed kernel build RC=0
PoC changes
No seed PoC existed; harness written from scratch: gpiofree.c kmod drives the gpio_if methods directly on the live gpio_intel0 (A/B: free-without-teardown vs documented order), plants two pads (INTSEL lines 5/6) in the DF-5518 RAM register image, dumps IS/MASK/intrmaps from kernel space and cross-checks via /dev/mem.
Verified recommended fix
In gpio_cherryview_unmap_intr, capture map->intidx before destroying the mapping and clear the CHV_GPIO_REG_MASK bit plus W1C the CHV_GPIO_REG_IS bit for that line (see fix.diff).
Verdict
gpio_cherryview_unmap_intr (gpio_cherryview.c:380-414) destroys the pin_intr_map and restores pad config but never clears the community interrupt MASK bit; GPIO_FREE_INTR without a preceding GPIO_TEARDOWN_INTR leaves the IRQ line armed with no mapping. Reproduced live on the DF-5518 planted-ACPI rig with the stock kernel: after ALLOC->SETUP->FREE (no teardown) the MASK bit for the unmapped line stays set (MASK=0x20, intrmaps[5].pin=-1), while the documented teardown->free order clears it; /dev/mem cross-check confirms. On real hardware a level-asserted pad then drives gpio_cherryview_intr into the is_level==0 edge-clear path in a tight re-asserting loop = interrupt storm / hard lockup (storm argued from ISR logic; QEMU cannot inject ISA IRQ 5 to fire it end-to-end). Latent today (only in-tree free path, gpio_acpi_unmap_aei, teardowns first) -> Info severity, defense-in-depth. fix.diff (mask+IS clear in unmap_intr) validated on a rebuilt kernel: same harness flips armed-with-no-mapping from 1 to 0.
No comments yet.