⬢ DragonFlyBSD Kernel Audit
DF-5464 / verdict.json
← back to finding ↓ download raw
{
  "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)"
}