⬢ DragonFlyBSD Kernel Audit
DF-5502 / verdict.json
← back to finding ↓ download raw
{
  "finding_id": "DF-5502",
  "status": "reproduced",
  "reproduced": true,
  "impact": "panic",
  "confidence": "certain",
  "verdict": "ioapic_add() (ioapic.c:229-267) validates nothing about the firmware-supplied gsi_base/npin. A crafted MADT IO_APIC entry with GlobalIrqBase >= 0x80000000 (u32 truncated to int at acpi_madt.c:738) plants a negative GSI space that passes the overlap scan (ioapic.c:235-246, negative ranges never collide with positive ones) and is walked by ioapic_gsi_setup(): GSI -1 aliases the int_gsi==-1 sentinel installed at ioapic.c:97, forwarding zeroed-struct CONFORM enums into ioapic_pin_prog() -> production panic ioapic_pin_prog: trig conform is not supported (ioapic.c:520; reproduced on X86_64_NOINV, run_negpanic_noinv.log:92-93; INVARIANTS builds stop earlier at ioapic_abi.c:809, run_negpanic.log). With the sentinels overwritten (16 INTSRC overrides), the default identity mapping irq = gsi has no lower bound (ioapic.c:370-371), OOB-reads ioc_intsrc[-1] at ioapic.c:372 (lands on ioc_list.tqh_firsts high dword -- always negative) and forwards irq=-1 to ioapic_set_legacy_irqmap(): on the production kernel the KKASSERT(irq>=0) (ioapic_abi.c:812) is compiled out and the function performs out-of-bounds writes to ioapic_irqmaps[0][-1] (:843,:849-853) and ioapic_irqs[-1] including a kernel pointer (:865-871) -- proven by the bootverbose line IOAPIC: irq -1 -> gsi -1 edge/high printed from inside the OOB path with NO panic and a boot that continued normally (run_negwrite_noinv.log:107,156). Stock-kernel runs panic at the two assertions (run_negpanic.log, run_negwrite.log). fix.diff (validate gsi_base/npin in ioapic_add + proper interval-overlap test + defensive gsi<0 guard before the sentinel scan) was built into X86_64_NOINV and both scenarios then degrade to ioapic_add: invalid I/O APIC gsi base ..., ignored with clean, control-identical ioapic configuration and no panic.",
  "exploit_chain": "crafted RSDP->RSDT->MADT (hint.acpi.0.rsdp, no validation) -> MADT IO_APIC entry GlobalIrqBase=0xFFFFFFE8 + RAM-backed fake IO-APIC window (attacker-chosen npin) -> ioapic_add(-24, 24) accepted -> ioapic_gsi_setup(-1) -> OOB read ioc_intsrc[-1] (negative) -> ioapic_set_legacy_irqmap(irq=-1, gsi=-1) -> OOB writes ioapic_irqmaps[0][-1] + ioapic_irqs[-1] (flags/idx/MMIO-KVA pointer) on production kernels -> silent corruption, boot continues. Sentinel variant (base 0xFFFFFFFF, npin 1) -> deterministic production panic at ioapic.c:520. Boot-time firmware vector only (pre-SMP); no unprivileged in-guest trigger, so no uid=0 chain from this bug.",
  "evidence": "panic.txt (all four decisive signatures); run_negwrite_noinv.log lines 105-107 (OOB-path print IOAPIC: irq -1 -> gsi -1 edge/high) and 156 (ioapic stage completed); run_negpanic_noinv.log lines 92-93 (irq 0 -> gsi -1 conform/conform + panic: ioapic_pin_prog: trig conform); run_negwrite.log line 48 (stock assertion irq >= 0 ioapic_abi.c:812); run_negpanic.log (stock assertion at :809); run_negwrite_fixed.log / run_negpanic_fixed.log (rejection warnings, no OOB, no panic); VERDICT.md sections 2/3/5; env.txt (kernels); fix.diff",
  "kernel_refs": [
    "sys/platform/pc64/apic/ioapic.c:229",
    "sys/platform/pc64/apic/ioapic.c:234",
    "sys/platform/pc64/apic/ioapic.c:235",
    "sys/platform/pc64/apic/ioapic.c:277",
    "sys/platform/pc64/apic/ioapic.c:354",
    "sys/platform/pc64/apic/ioapic.c:370",
    "sys/platform/pc64/apic/ioapic.c:372",
    "sys/platform/pc64/apic/ioapic.c:396",
    "sys/platform/pc64/apic/ioapic.c:519",
    "sys/platform/pc64/apic/ioapic.c:553",
    "sys/platform/pc64/apic/ioapic_abi.c:809",
    "sys/platform/pc64/apic/ioapic_abi.c:812",
    "sys/platform/pc64/apic/ioapic_abi.c:843",
    "sys/platform/pc64/apic/ioapic_abi.c:865",
    "sys/platform/pc64/acpica/acpi_madt.c:738"
  ],
  "poc_changes": "Rig authored from scratch on the DF-5431 gen_madt.py pattern: added RAM-backed fake-IOAPIC register window (npin fully attacker-chosen via VER preload), negpanic/negwrite scenarios, 16 sentinel-killing INTSRC overrides, idempotent loader-hint handling. One build-flow fix during verification: nativekernel does not install -- explicit make installkernel KERNCONF=X86_64_NOINV added (first negwrite-on-NOINV attempt accidentally booted stock GENERIC); one source fix in fix.diff (INT_MAX -> 0x7fffffff, not available in kernel C).",
  "attempts": 8,
  "guest_uname": "DragonFly dfbsd 6.5-DEVELOPMENT #0: Tue Oct  6 12:48:45 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_NOINV x86_64 (production-behaviour build = X86_64_GENERIC minus options INVARIANTS; stock guest kernel: 6.5-DEVELOPMENT #0 Thu Jul  2 06:02:54 UTC 2026 X86_64_GENERIC)",
  "runtime_sec": 6900,
  "guest_dirty": 0,
  "build_cmd": "host: python3 gen_madt.py <scenario> /tmp/madt_<scenario>.bin ; guest kernel builds: cd /usr/src && make -j6 nativekernel KERNCONF=X86_64_NOINV && make installkernel KERNCONF=X86_64_NOINV (baseline and with fix.diff applied)",
  "run_cmd": "sh run_scenario.sh <negpanic|negwrite> 100  (loader hint.acpi.0.rsdp=0x40000000 + QEMU -device loader,file=/tmp/madt_<scenario>.bin,addr=0x40000000,force-raw=on; serial console captured to run_*.log)",
  "code_hash": "2317e27976b0ea69c97d7557151a55317d7c4a898d3a6cdaa4ac3b1792a61cf1",
  "notes": "Fix validated on QEMU: baseline NOINV shows the silent OOB write path (bootverbose marker) and the ioapic.c:520 production panic; patched NOINV rejects both crafted topologies with invalid I/O APIC gsi base ... ignored and configures the remaining legitimate IOAPIC identically to the control boot (46 legacy mapping lines). Blast radius of the OOB writes is link-layout dependent (nm -S stock kernel: ioapic_conf @0x818da540 size 0xd0, ioapic_irqs @0x818da620, ioapic_irqmaps @0x818db240 -- the -1 slots land in alignment padding on this exact build, but the odd-negative-GSI variant computes cpuid = gsi % ncpus < 0 and writes 4.6KB*|cpuid| rows below ioapic_irqmaps into arbitrary foreign .bss). Known findings not re-reported: DF-1089 (ioc_intsrc upper-bound guard), DF-5431 (overlap/dup/count panics -- this finding is the deeper u32/int class its VERDICT flagged).",
  "recommended_fix": "Validate gsi_base >= 0, npin > 0, no base+npin overflow in ioapic_add() (reject-and-warn, not panic), replace the two-sided membership overlap test with the full interval test, and guard ioapic_gsi_setup() against gsi < 0 before the sentinel scan (see fix.diff)",
  "fix_status": "fixed",
  "fix_baseline_reproduced": true,
  "fix_patched_reproduced": false,
  "fix_verdict": "fix.diff applied inside the guest /usr/src, X86_64_NOINV rebuilt and installed. Baseline: negwrite -> silent OOB-write path executed (IOAPIC: irq -1 -> gsi -1 edge/high, no panic); negpanic -> panic ioapic.c:520. Patched: negwrite -> ioapic_add: invalid I/O APIC gsi base -24, npin 24, ignored, no negative-irq line, no panic, boot proceeds like control; negpanic -> ... gsi base -1, npin 1, ignored, no panic, full normal legacy mapping byte-identical to the control run. The previously-observed bad behavior is GONE on the patched kernel.",
  "fix_evidence": "findings/poc/DF-5502/run_negwrite_fixed.log (line ~105 warning; no OOB marker), run_negpanic_fixed.log (warning; 46 normal IOAPIC: irq lines), fix.diff (git apply --check passes), VERDICT.md section 5"
}