# DF-5464 VERDICT — reproduced (OOB write past contract array + panic + livelock), driver-facing MSI ABI, Low severity

## Bottom line

REPRODUCED on the guest, on a kernel built exactly like production
(X86_64_GENERIC minus `options INVARIANTS`; stock GENERIC ships without
INVARIANTS too): `icu_abi_msi_alloc_intern()` (icu_abi.c:452-513)
performs **no runtime validation** of the driver-supplied `count`. A KLD
(the driver-facing ABI, identical in reach to `PCIB_ALLOC_MSI` →
`nexus_alloc_msi` → `MachIntrABI.msi_alloc`, active on ICU-ABI configs
where `lapic_enable && !ioapic_enable`) passing `count=64` gets a clean
success return and **32 kernel words written past the caller's 32-int
contract array** (icu_abi.c:498 `intrs[j] = intr`; the reference caller's
array is the on-stack `int irqs[32]` at sys/bus/pci/pci.c:1952/2072).
`count=-2600000` walks the scan index below the kernel image:
`Fatal trap 12 ... fault virtual address = 0xffffffff7f1bf020` (below
KERNBASE 0xffffffff80200000), `Stopped at icu_abi_msi_alloc_intern+0x5a:
movl (%r13,%rax,1),%edi` — the `im_type` load at icu_abi.c:476.
`count=0` never advances `i` and **live-locks the kernel while holding
icu_irqmap_tok** (icu_abi.c:463/:473): kldload never returned and the
guest then stopped servicing new ssh sessions entirely for minutes
("Connection timed out during banner exchange" on every attempt) — a
system-wide wedge needing a hardware reset. On the INVARIANTS kernel
(stock X86_64_GENERIC) the same trigger panics `panic: invalid count 64`
at the KASSERT (icu_abi.c:460) — the KASSERT is the only guard.

## How it was reproduced (baseline X86_64_NOINV, full logs in run.log)

RUN 1 — safe canary demo (icu_msiabi_probe.ko):

    icu_msiabi_probe: msix_alloc(cpu0) rc=0 irq=16        <- legal path OK
    icu_msiabi_probe: msi_alloc rc=0                      <- count=64 ACCEPTED
    icu_msiabi_probe: zone[0..7] = 128 129 ... 135        <- alloc at i=128
    icu_msiabi_probe: 32/96 canary words past zone[31] overwritten
    icu_msiabi_probe:   zone[32]=160  <-- OOB write past 32-int contract
    icu_msiabi_probe: msi_release(64) done / SAFE PHASE COMPLETE

Guest healthy afterwards — this is a clean OOB-write primitive, not a
blanket failure.

RUN 2 — negative count (icu_msiabi_neg.ko) → panic_neg_count.txt:

    Fatal trap 12: page fault while in kernel mode
    fault virtual address = 0xffffffff7f1bf020  (below KERNBASE)
    Stopped at icu_abi_msi_alloc_intern+0x5a: movl (%r13,%rax,1),%edi

RUN 3 — zero count (icu_msiabi_livelock.ko): kldload never returned
(caller-side timeout expired); afterwards the guest refused all new ssh
sessions (banner timeout) until a QEMU-monitor hardware reset.

ICU-vs-IOAPIC note: unlike the ioapic twin (DF-5351), ICU map rows are
fragmented ([0..15] LEGACY/RESERVED, [96] SYSCALL, next row's head
RESERVED), so count=128 cannot complete a row-crossing window here; the
demonstrated primitives are the contract-array stack smear, the negative
scan walk, and the token-holding livelock.

## Primitive characterization (why Low severity, not higher)

Writes: for each j in [0,count): `intrs[j] = i+j` (semi-controlled small
ints, past the caller's array — kernel stack in the real caller) and
`icu_irqmaps[cpuid][i+j] = {im_type=MSI/MSIX, im_msi_base=i}` (16-byte
strided map corruption). Reach requires a driver calling the ICU MSI ABI
with a bad count — already-privileged code (kldload) or a malicious PCIe
device paired with a driver that forwards the device-advertised MMC
count. No unprivileged entry point exists (no sysctl/ioctl/dev-node
surface in this file). Hence Low severity — but the primitive is real
memory corruption past an attacker-influenced bound plus a trivial
system-wide livelock.

## Fix validation (fix.diff, applied to guest /usr/src, incremental rebuild)

Baseline reproduced → apply fix.diff (runtime count/powerof2 checks
returning EINVAL/early-return in msi_alloc_intern/msi_release_intern)
→ rebuild X86_64_NOINV → reboot → identical PoCs (run_fixed.log):

    probe:     msi_alloc rc=22 (EINVAL), "alloc failed, no canary test",
               legal msix count=1 round trip still OK
    neg:       SURVIVED, rc=22            (was: fatal trap)
    livelock:  returned rc=22             (was: permanent hang + ssh wedge)

Bad behavior gone; no regression on legal allocations.

## What I had to fix vs a naive PoC (poc_changes)

- Same KLD plumbing lessons as DF-5351: in-tree kmod.mk recipe, generated
  device_if.h/bus_if.h, moduledata_t-based DECLARE_MODULE.
- The count=-2600000 value was chosen so the second loop iteration reads
  ~41 MB below icu_irqmaps (0xffffffff81a00420) — below KERNBASE,
  guaranteed unmapped — instead of stepping through mapped BSS first.
- The livelock rig expects the caller side to time out; the guest wedge
  afterwards (all ssh dead) is stronger than predicted and documented.

## Kernel references

- sys/platform/pc64/icu/icu_abi.c:460-461 (KASSERT-only count checks)
- sys/platform/pc64/icu/icu_abi.c:473-507 (scan loop, intrs[j]=intr, msi_setup)
- sys/platform/pc64/icu/icu_abi.c:524-533 (release count check + intrs[i] read)
- sys/sys/systm.h:94-122 (KASSERT/KKASSERT compiled out without INVARIANTS)
- sys/bus/pci/pci.c:1952,2072 (caller's 32-int stack arrays)
- sys/platform/pc64/x86_64/nexus.c:591-596 (nexus_alloc_msi gating)
- sys/config/GENERIC (no INVARIANTS) vs sys/config/X86_64_GENERIC:56

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