# 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)

1. **No lower bound, even under INVARIANTS.** `KASSERT(irq <
   ISA_IRQ_CNT)` is a strictly-less-than check on a signed int: `irq=-1`
   passes. `elcr_read_trigger(-1)` and `elcr_read_trigger(-22)` returned
   normally (`run.log` E1). No panic occurred — the negative irq flowed
   straight into `elcr_status & (1 << irq)` (elcr.c:57, elcr.c:90).

2. **The UB shift aliases a real bit — ALIAS-PROVEN.** x86 `shl` takes
   the count mod 32, so `1 << -22` materializes as `1 << 10`.
   `elcr_read_trigger(-22)` returned the same value as
   `elcr_read_trigger(10)` (both `L`, since this rig's ELCR is 0x0c00 =
   irq10/11 level). The negative irq read/writes a *real* ISA IRQ's
   trigger bit.

3. **A negative irq PROGRAMS LIVE HARDWARE.** `elcr_write_trigger(-27,
   INTR_TRIGGER_LEVEL)` — `-27 < 16` passes 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`).

4. **The upper bound is a debug assert, not a guard.** Loading
   `elcrboom.ko` on this INVARIANTS guest panics at exactly
   `elcr.c:106`: `panic: elcr_write_trigger: invalid IRQ 16`, trace
   `elcr_write_trigger+0x8c <- elcrboom_modevent` (panic.txt). On a
   production (non-INVARIANTS) kernel `sys/sys/systm.h:117` compiles
   the KASSERT to `do { } while (0)`: the same call silently executes
   `elcr_status |= 1 << 16` — shadow corruption past the 16-bit
   register domain, no error, no port protection (the `outb` at
   elcr.c:115 truncates to 8 bits, so bits 16-31 poison only the
   shadow; they resurface in `elcr_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()` checks
  `KKASSERT(irq >= 0 && irq < IDT_HWI_VECTORS)` — always compiled in —
  plus `KKASSERT(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 is `machintr_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_config` KKASSERT-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.
