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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5527 Β· 12 files
FileTypeDescriptionSize
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.
VERDICT.md
↓ download raw

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

fixed
baseline reproduced→ patch + rebuild →patched clean

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

Confirmed kernel references

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.