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

icu_abi MSI/MSI-X allocation count validated only by INVARIANTS-only KASSERTs β€” OOB write past caller's 32-int array, negative-index scan-walk panic, and count==0 livelock holding icu_irqmap_tok on production kernels

Summary

The ICU twin of DF-5351: count validated only by KASSERTs that compile out on production kernels - count=64 returns rc=0 writing intrs[j] past the caller's on-stack int irqs[32] (canary-proven 32/96 words), count=-2600000 walks the scan ~41MB below icu_irqmaps (Fatal trap 12 at the im_type load), count=0 livelocks 'i += count' holding icu_irqmap_tok (proven: kldload never returned and the guest then refused ALL new ssh until reset). ICU row fragmentation blocks the IDT-smash variant. Runtime-validation fix validated (EINVAL, no corruption, no hang).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5464 Β· 15 files
FileTypeDescriptionSize
icu_msiabi_probe.c β€” 3.2 KB view raw
icu_msiabi_neg.c β€” 1.5 KB view raw
icu_msiabi_livelock.c β€” 1.7 KB view raw
build.sh β€” 188 B view raw
run.sh β€” 213 B view raw
build.log β€” 11.2 KB view raw
run.log β€” 2.3 KB view raw
run_invkernel.log β€” 653 B view raw
run_fixed.log β€” 478 B view raw
panic_neg_count.txt β€” 435 B view raw
env.txt β€” 2.3 KB view raw
VERDICT.md β€” 5.7 KB ↓ raw
fix.diff β€” 2.2 KB view raw
manifest.json β€” 1.2 KB view raw
verdict.json β€” 5.0 KB view raw
VERDICT.md
↓ download raw

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

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/sys/platform/pc64/icu/icu_abi.c, X86_64_NOINV rebuilt (completed 23:35:53 UTC) and rebooted; identical PoCs re-run in the same session: msi_alloc(count=64) and count=-2600000 and count=0 all return EINVAL(22) immediately, zero canary words overwritten, no fault, no livelock, and the legal count=1 MSI-X round trip still succeeds (run_fixed.log). Bad behavior gone; no regression.

run_fixed.log; guest dmesg captured live during the fixed run (rc=22 lines for probe/neg/livelock; SAFE PHASE COMPLETE)
↓ fix.diffper-fix-DF-5464

Confirmed kernel references

Detail

Exploit chain

driver/KLD calls MachIntrABI.msi_alloc(count=64) (reachable in ICU-ABI configs via PCIB_ALLOC_MSI -> nexus.c:595, lapic_enable && !ioapic_enable) -> icu_abi.c:473 loop accepts -> icu_abi.c:498 intrs[j]=intr smears 32 ints past the caller's on-stack int irqs[32] (kernel stack in the real caller; values are allocated intr numbers 128..191) plus 16-byte-strided icu_irqmap writes (im_type/im_msi_base); count=0 -> permanent loop at icu_abi.c:473 holding icu_irqmap_tok -> system-wide ssh wedge (DoS); count<0 -> OOB read walk below the kernel image -> fatal trap. No unprivileged entry point; primitive requires already-privileged code or a malicious PCIe device forwarding its MMC count.

Evidence (decisive lines)

run.log (RUN1 canary 'zone[32]=160 <-- OOB write', RUN2 trap, RUN3 livelock + ssh banner timeouts); panic_neg_count.txt (fault VA 0xffffffff7f1bf020, Stopped at icu_abi_msi_alloc_intern+0x5a); run_invkernel.log ('panic: invalid count 64'); run_fixed.log (rc=22 x3, SAFE PHASE COMPLETE)

PoC changes

Rigs written fresh for the ICU ABI (family pattern from DF-5351): export-based access via 'extern struct machintr_abi MachIntrABI_ICU'; in-tree kmod.mk recipe with generated device_if.h/bus_if.h; moduledata_t DECLARE_MODULE; count=-2600000 chosen so the 2nd loop iteration lands ~41MB below icu_irqmaps (below KERNBASE, guaranteed unmapped); livelock rig relies on caller-side timeout; capture of module output via dmesg/serial (kprintf does not reach the ssh pipe).

Verified recommended fix

Replace the INVARIANTS-only KASSERT count checks in icu_abi_msi_alloc_intern()/icu_abi_msi_release_intern() with runtime validation returning EINVAL / early-return (see fix.diff).

Verdict

REPRODUCED on a production-behaviour kernel (X86_64_NOINV = X86_64_GENERIC minus INVARIANTS; stock GENERIC equally ships without INVARIANTS): icu_abi_msi_alloc_intern()/icu_abi_msi_release_intern() perform no runtime validation of the driver-supplied count (KASSERT-only, icu_abi.c:460-461/524). count=64 returns rc=0 and writes 32 ints past the caller's 32-int contract array (zone[32]=160 in the canary rig; the real caller's array is the on-stack int irqs[32] at pci.c:1952/2072); count=-2600000 walks the scan index below KERNBASE and dies with 'Fatal trap 12 ... Stopped at icu_abi_msi_alloc_intern+0x5a: movl (%r13,%rax,1),%edi' (the im_type load, icu_abi.c:476); count=0 live-locks the kernel holding icu_irqmap_tok β€” kldload never returned and the guest then stopped servicing all new ssh sessions until a hardware reset. INVARIANTS kernels panic at 'panic: invalid count 64' instead (captured) β€” the KASSERT is the only guard. Fix (runtime count checks -> EINVAL/early-return) validated on a rebuilt kernel: rc=22 for all bad counts, zero canary writes, legal msix(1) round trip unaffected.