DF-5464 / verdict.json
{ "finding_id": "DF-5464", "status": "reproduced", "reproduced": true, "impact": "panic", "confidence": "certain", "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 callers 32-int contract array (zone[32]=160 in the canary rig; the real callers 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 \u2014 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) \u2014 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.", "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 callers 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": "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)", "kernel_refs": [ "sys/platform/pc64/icu/icu_abi.c:460", "sys/platform/pc64/icu/icu_abi.c:473", "sys/platform/pc64/icu/icu_abi.c:498", "sys/platform/pc64/icu/icu_abi.c:524", "sys/platform/pc64/icu/icu_abi.c:533", "sys/bus/pci/pci.c:1952", "sys/platform/pc64/x86_64/nexus.c:595", "sys/sys/systm.h:94" ], "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).", "attempts": 3, "guest_uname": "DragonFly dfbsd 6.5-DEVELOPMENT #0: Mon Oct 5 22:59:13 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_NOINV x86_64", "runtime_sec": 2400, "guest_dirty": 0, "build_cmd": "sh build.sh (guest /usr/src/sys/modules/icu_abi_probe, in-tree kmod.mk; see build.log)", "run_cmd": "sh run.sh (kldload icu_msiabi_probe.ko / icu_msiabi_neg.ko / timeout 30 kldload icu_msiabi_livelock.ko)", "code_hash": "93d46b84d52f8b0cf20c130be4b3cc5085625f720637beb19c8b8a1293fe256e (icu_msiabi_probe.c); bedb06a46f20e035127f1994fdcebf719dbb66b8f4078dbbc102e1972e4eff3e (icu_msiabi_neg.c); 6933d6de3ca85eeb41038981cfe2f57ebd4448e8f3e8e156ba9cb227a38fb97c (icu_msiabi_livelock.c)", "notes": "ICU row fragmentation (LEGACY/RESERVED head + SYSCALL map at [96]) prevents the count=128 row-crossing window the ioapic twin (DF-5351) had; the demonstrated primitives here are the contract-array stack smear, negative scan walk, and the count=0 token-holding livelock (which wedged the whole guest, not just one cpu). Baseline runs required QEMU-monitor system_reset recovery from DDB. Guest returned to the clean-source snapshot afterwards.", "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).", "fix_status": "fixed", "fix_baseline_reproduced": 1, "fix_patched_reproduced": 0, "fix_verdict": "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.", "fix_evidence": "run_fixed.log; guest dmesg captured live during the fixed run (rc=22 lines for probe/neg/livelock; SAFE PHASE COMPLETE)" } |