elcr_read_trigger/elcr_write_trigger: irq bounds enforced only by an INVARIANTS-only KASSERT with no signed lower bound β negative irq executes a C-undefined shift that aliases a real ISA bit and programs the live ELCR port; irq>=16 silently corrupts the shadow on production kernels
Summary
The only validation is a strict-less-than assert with no lower bound, compiled to nothing on production kernels. Live-verified on the INVARIANTS guest: negatives PASS the assert (read(-1)/(-22) accepted, -22 aliased real bit 10 via mod-32 shift), elcr_write_trigger(-27, LEVEL) programmed the LIVE port 0x4d0 (bit5 set on real hardware), and write(16) panics on the assert while production kernels would silently execute elcr_status |= 1<<16 (shadow corruption past the 16-bit domain resurfacing in elcr_resume's byte writes). DF-5616 sibling; wild-irq source is a DF-5465-class driver bug where elcr is the final sink. signed-bound + runtime-guard fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5652 Β· 17 files| File | Type | Description | Size | |
|---|---|---|---|---|
| elcrx.c | β | 9.6 KB | view raw | |
| elcrboom.c | β | 1.4 KB | view raw | |
| Makefile | β | 78 B | β download | |
| build.sh | β | 136 B | view raw | |
| run.sh | β | 227 B | view raw | |
| run_boom.sh | β | 214 B | view raw | |
| build.log | β | 6.7 KB | view raw | |
| run.log | β | 2.7 KB | view raw | |
| boom.log | β | 0 B | β download | |
| panic.txt | β | 896 B | view raw | |
| panic_raw.log | β | 3.1 KB | view raw | |
| wedge_v1.log | β | 2.5 KB | view raw | |
| env.txt | β | 358 B | view raw | |
| fix.diff | β | 1.3 KB | view raw | |
| VERDICT.md | β | 4.4 KB | β raw | |
| manifest.json | β | 1.2 KB | view raw | |
| verdict.json | β | 5.8 KB | view raw |
DF-5652 β VERDICT
Finding: elcr_read_trigger() / elcr_write_trigger() validate the
caller-supplied irq only with KASSERT(irq < ISA_IRQ_CNT) β
sys/platform/pc64/icu/elcr.c:89, elcr.c:106.
Verdict: REPRODUCED (live), impact none for an unprivileged attacker β defense-in-depth gap closing the family perimeter (DF-5616/5464/5465 class). Severity Info.
What was proven live (X86_64_GENERIC, INVARIANTS kernel, i440fx)
-
No lower bound, even under INVARIANTS.
KASSERT(irq < ISA_IRQ_CNT)is a strictly-less-than check on a signed int:irq=-1passes.elcr_read_trigger(-1)andelcr_read_trigger(-22)returned normally (run.logE1). No panic occurred β the negative irq flowed straight intoelcr_status & (1 << irq)(elcr.c:57, elcr.c:90). -
The UB shift aliases a real bit β ALIAS-PROVEN. x86
shltakes the count mod 32, so1 << -22materializes as1 << 10.elcr_read_trigger(-22)returned the same value aselcr_read_trigger(10)(bothL, since this rig's ELCR is 0x0c00 = irq10/11 level). The negative irq read/writes a real ISA IRQ's trigger bit. -
A negative irq PROGRAMS LIVE HARDWARE.
elcr_write_trigger(-27, INTR_TRIGGER_LEVEL)β-27 < 16passes the assert β set shadow bit 5 (new_status = elcr_status | (1 << -27)β1 << 5) and wrote port 0x4d0:0x00 -> 0x20 (bit5 SET)(run.log E2). The kernel then believes irq5 is edge (stale intent) while hardware is level β or vice versa after a further in-range write. State restored exactly (ok=YES). -
The upper bound is a debug assert, not a guard. Loading
elcrboom.koon this INVARIANTS guest panics at exactlyelcr.c:106:panic: elcr_write_trigger: invalid IRQ 16, traceelcr_write_trigger+0x8c <- elcrboom_modevent(panic.txt). On a production (non-INVARIANTS) kernelsys/sys/systm.h:117compiles the KASSERT todo { } while (0): the same call silently executeselcr_status |= 1 << 16β shadow corruption past the 16-bit register domain, no error, no port protection (theoutbat elcr.c:115 truncates to 8 bits, so bits 16-31 poison only the shadow; they resurface inelcr_resume()'s byte writes).
Reachability (why Info)
- In-tree callers are bounded:
icu_abi_initmap()loops i=0..15 (icu_abi.c:342-344);icu_abi_legacy_intr_config()checksKKASSERT(irq >= 0 && irq < IDT_HWI_VECTORS)β always compiled in β plusKKASSERT(im_type == ICU_IMT_LEGACY)which pins irq to 0..15 in practice (icu_abi.c:373-376). No unprivileged-user path exists: the config chain ismachintr_legacy_intr_config<- nexus_config_intr (BUS_CONFIG_INTR, driver probe) / acpi_fadt SCI setup / pci tunable β all kernel/boot/root reach. - The realistic in-tree route for a wild irq is a driver bug of the
DF-5465 class (
icu_abi_legacy_intr_configKKASSERT-only bounds on a production kernel) β where elcr is the final sink. Sibling finding DF-5616 (icu.c:167, same signed-lower-bound gap feeding the 8259 IMR) closes the same perimeter on the mask side. - No memory unsafety: no array indexing anywhere in elcr.c β the blast radius is the 16-bit ELCR register domain and the 32-bit shadow.
Harness
elcrx.c (shared 3-finding harness; E0/E1/E2 phases here) and
elcrboom.c (upper-bound probe, run last). Build clean with -Werror
(build.log). v1 of the harness wedged the guest with a spin-barrier
starvation bug (documented in wedge_v1.log, poc_changes) β not a
finding; v2 removed the barrier. Every destructive phase restores state
(ok=YES, E9: coherent=YES ... guest HEALTHY); the boom panic is
followed by vm.sh reset with-src (guest returned clean).
Fix
Runtime signed bounds in both functions (fix.diff): the KASSERTs gain
irq >= 0 &&, and a real if (irq < 0 || irq >= ISA_IRQ_CNT) guard
returns a safe value before any shift or port write β the write path
must never touch 0x4d0/0x4d1 with an out-of-contract irq. Behavioral
delta vs the live P-runs: negative/OOB irqs return EDGE / no-op instead
of aliasing a real bit (E2) or corrupting the shadow (boom).
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
not_testableInfo/hardening finding β single-fix kernel rebuild not warranted (matches DF-5616 precedent). fix.diff adds the signed lower bound to both KASSERTs plus real runtime guards (read: return INTR_TRIGGER_EDGE; write: return before touching elcr_status or the ports). Its entire behavioral delta vs the live runs is: trigger(-1)/(-22) return EDGE instead of aliasing real bits, write_trigger(-27,...) no-ops instead of setting hw bit5, and write_trigger(16,...) returns instead of panicking (INVARIANTS) or corrupting shadow bit 16 (production) β verified against run.log/panic.txt by inspection; no memory-corruption baseline exists to regression-test.
fix.diff
Confirmed kernel references
- sys/platform/pc64/icu/elcr.c:57
- sys/platform/pc64/icu/elcr.c:89
- sys/platform/pc64/icu/elcr.c:106
- sys/platform/pc64/icu/elcr.c:108
- sys/platform/pc64/icu/elcr.c:114
- sys/platform/pc64/icu/elcr.c:117
- sys/sys/systm.h:116
- sys/platform/pc64/icu/icu_abi.c:342
- sys/platform/pc64/icu/icu_abi.c:373
- sys/platform/pc64/x86_64/nexus.c:487
Detail
Evidence (decisive lines)
run.log E1 lines: 'elcr_read_trigger(-1)=E (no panic: negative irq ACCEPTED...)' and 'elcr_read_trigger(-22)=L vs elcr_read_trigger(10)=L -> ALIAS-PROVEN (1<<-22 materialized as 1<<10 on x86)'; run.log E2: 'elcr_write_trigger(-27, LEVEL): hw0x4d0 0x00 -> 0x20 (bit5 SET), shadow trigger(5)=L' then 'restored ... ok=YES'; panic.txt: 'panic: elcr_write_trigger: invalid IRQ 16' with trace 'elcr_write_trigger+0x8c <- elcrboom_modevent' (INVARIANTS-only enforcement); E0: 'hw ELCR=0x0c00 shadow LEVEL-bits=0x0c00 coherent=YES' (rig baseline); wedge_v1.log documents the v1 harness starvation bug (not a finding)
PoC changes
Harness authored from scratch (no seed). v1 wedged the guest: the two race kthreads spun on a start-barrier flag while pinning cpu0/cpu1, and the kldload thread that had to set the flag (on cpu0, per E0 mycpuid=0) was starved by the never-yielding spin thread β an LWKT same-priority starvation deadlock, documented in wedge_v1.log and fixed in v2 (barrier removed, 300k iters, lwkt_yield() every 16k). E1/E2 evidence from the wedged run was salvaged from the serial log and re-confirmed identically in the clean v2 run. The Makefile needed SRCS+= device_if.h bus_if.h (sys/bus.h) and machine/atomic.h (not sys/atomic.h); DEV_MODULE replaced with DECLARE_MODULE.
Verified recommended fix
Runtime signed bounds in both functions: KASSERT gains irq >= 0 && and a real 'if (irq < 0 || irq >= ISA_IRQ_CNT) return safe-value;' guard before any shift or port write (elcr.c:89, elcr.c:106)
Verdict
Live-verified on the QEMU/i440fx guest (X86_64_GENERIC, INVARIANTS kernel, ELCR=0x0c00 coherent): the ELCR trigger API validates its irq only with KASSERT(irq < ISA_IRQ_CNT) (elcr.c:89,106) β a strictly-less-than check on a signed int with NO lower bound, compiled out entirely on production kernels (systm.h:116-122). elcr_read_trigger(-1) and (-22) were accepted without panic even under INVARIANTS, and -22 provably aliased real bit 10 (1<<-22 materializes as 1<<10 on x86; trigger(-22)==trigger(10)==LEVEL). The write path is worse: elcr_write_trigger(-27, LEVEL) passed the assert, set shadow bit 5 via the UB shift, and PROGRAMMED THE LIVE PORT 0x4d0 (0x00 -> 0x20, bit5 SET) β an out-of-contract negative irq reconfigures a real ISA IRQ's trigger mode in hardware. The upper bound is assert-only: elcr_write_trigger(16, LEVEL) panics at elcr.c:106 on this INVARIANTS kernel ('panic: elcr_write_trigger: invalid IRQ 16', trace elcr_write_trigger+0x8c) β i.e. the sole enforcement is a debug assertion; on production kernels the same call silently does elcr_status |= 1<<16 (shadow corruption past the 16-bit register domain). Reach is kernel-code-only (root/KLD or a DF-5465-class driver bug feeding a wild irq through icu_abi_legacy_intr_config); in-tree callers are KKASSERT- or loop-bounded. No memory unsafety (no arrays in elcr.c), no unprivileged path β Info/hardening, closing the family perimeter with DF-5616 (icu.c:167). Every destructive phase restored state exactly (E2 ok=YES, E9 coherent=YES); boom run followed by vm.sh reset with-src.
No comments yet.