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

IOAPIC topology (overlapping GSI bases, duplicate addresses, >16 entries) unvalidated at probe -> unconditional ioapic_add/ioapic_config boot panics

Summary

Probe checks only count>=1/zero-base/nonzero-address/Id!=255; enumerate feeds raw firmware fields to ioapic_add which panic()s on GSI overlap and duplicate address, and ioapic_config panics above 16 IOAPICs. Real injected MADT: 'panic: ioapic_add: overlapped gsi, base 0 npin 1, hit base 0, npin 24' after the full LAPIC/SMP stage - unconditional (no INVARIANTS needed). Probe-time topology validation fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5431 Β· 8 files
FileTypeDescriptionSize
README.md β€” 1.6 KB ↓ raw
VERDICT.md β€” 3.0 KB ↓ raw
gen_madt.py β€” 4.8 KB view raw
build.sh β€” 124 B view raw
run.sh β€” 298 B view raw
run.log β€” 8.3 KB view raw
env.txt β€” 2.1 KB view raw
fix.diff β€” 1.5 KB view raw

DF-5431 β€” MADT IOAPIC topology never validated at probe β†’ deterministic ioapic_add boot panic

What

madt_ioapic_probe() (acpi_madt.c:638-666) validates only: β‰₯1 IOAPIC entry, some entry with GlobalIrqBase == 0, non-zero addresses, Id != 255. It never checks that the described GSI ranges are disjoint, that addresses are unique, or that there are ≀16 IOAPICs. madt_ioapic_enum_callback (acpi_madt.c:722-739) then feeds the raw entries to ioapic_add(), which panic()s unconditionally on overlap (ioapic.c:240), duplicate address (ioapic.c:245), and ioapic_config panics on >16 IOAPICs (ioapic.c:137-138).

Rig

Identical to ../DF-5430: hint.acpi.0.rsdp=0x40000000 + QEMU -device loader blob with crafted RSDP→RSDT→MADT.

Build

python3 gen_madt.py ioapic_overlap /tmp/madt_ioapic_overlap.bin

Run

vm.sh run_root 'printf "hint.acpi.0.rsdp=0x40000000\nboot_verbose=YES\n" >> /boot/loader.conf; sync'
vm.sh down
QEMU_EXTRA="-device loader,file=/tmp/madt_ioapic_overlap.bin,addr=0x40000000,force-raw=on" vm.sh up
vm.sh log

Expected output (run.log:87-89)

ACPI MADT: IOAPIC addr 0xfec00000, apic id 0, gsi base 0
ACPI MADT: IOAPIC addr 0xfec88000, apic id 1, gsi base 0
panic: ioapic_add: overlapped gsi, base 0 npin 1, hit base 0, npin 24

Trigger table: valid BSP+AP LAPIC entries plus two IO_APIC entries both declaring GSI base 0 (first at the real QEMU IOAPIC 0xFEC00000, second on unmapped MMIO whose VER read yields npin=1 β€” any overlap works).

Sibling findings: DF-1034 (mptable INT panic, Low), DF-5339 family (probe/enumerate asymmetry). Same firmware-fed boot-time-DoS class.

VERDICT.md
↓ download raw

DF-5431 β€” VERDICT

1. Reproduced? YES β€” real kernel-boot panic

run.log:87-89 (kernel v6.5.0.1712.g89e6a #1 Mon Jun 29 clean-install build):

ACPI MADT: IOAPIC addr 0xfec00000, apic id 0, gsi base 0
ACPI MADT: IOAPIC addr 0xfec88000, apic id 1, gsi base 0
panic: ioapic_add: overlapped gsi, base 0 npin 1, hit base 0, npin 24

Boot completed the entire LAPIC/SMP stage (ACPI CPUS = 2, AP started) and panicked in ioapic_config at SI_BOOT2_IOAPIC.

2. Why it is real (path:line)

  • Probe side accepts the table: madt_ioapic_probe_callback (acpi_madt.c:615-633) checks only Address != 0 (:619-622) and Id != APICID_MAX (:623-628); madt_ioapic_probe (:652-662) adds only ioapic_cnt != 0 and "some entry has GlobalIrqBase 0". The crafted table satisfies all of these.
  • Enumerate side: madt_ioapic_enum_callback (acpi_madt.c:728-738) calls ioapic_map/ioapic_read/ioapic_add with the raw firmware fields. ioapic_add() panics on GSI overlap (ioapic.c:233-243) and duplicate address (ioapic.c:244-245); ioapic_config panics above 16 IOAPICs (ioapic.c:136-138). All three are panic(), present in production kernels β€” this does NOT depend on INVARIANTS (unlike DF-5430).

Two more members of the same class demonstrated by inspection (not booted): two identical IO_APIC addresses β†’ "duplicated addr" panic; 17 IO_APIC entries pass probe (count never capped) β†’ "more than 16 I/O APIC" panic.

Related secondary robustness gap noted (not separately booted): GlobalIrqBase is a raw UINT32 truncated to int gsi_base in ioapic_add(void *, int, int) β€” bases β‰₯ 2^31 overflow gsi_end (ioapic.c:231), which can defeat the overlap check arithmetic.

3. Threat model

Same as DF-5430: firmware/bootloader-controlled ACPI tables (hint.acpi.0.rsdp vector demonstrated); no unprivileged in-guest path. Deterministic boot-time panic = DoS, unconditional on kernel build. Low severity, matching the accepted DF-1034/DF-5339 bar.

4. Exploit chain

None β€” controlled panic() before any memory unsafety.

5. Fix

fix.diff adds probe-side topology sanity within acpi_madt.c: duplicate IOAPIC address, duplicate GSI base (two IOAPICs sharing a base always overlap, since every IOAPIC has β‰₯1 pin), and a 16-entry cap β€” each returns EINVAL so the enumerator is rejected gracefully and ioapic_config falls back. Full npin-aware overlap detection needs MMIO reads at probe time and belongs with ioapic.c's panicβ†’rejection conversion (noted in the finding). git apply --check passes. Not kernel-build-validated this run (single mandatory build slot spent on DF-5432; this is a Low boot panic β€” DF-5339 precedent).

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

not_testable
baseline reproduced→ patch + rebuild →patched clean

fix.diff (cbarg gains address/base tracking arrays; duplicates, shared GSI base and count cap return EINVAL) passes git apply --check. Not kernel-build-validated this run (mandatory slot spent on DF-5432; Low boot-panic class, DF-5339/DF-1034 precedent). Full npin-aware overlap needs MMIO reads at probe and belongs with converting ioapic.c panics to rejections.

findings/poc/DF-5431/fix.diff (git apply --check passes); VERDICT.md section 5
↓ fix.diffper-fix-DF-5431

Confirmed kernel references

Detail

Evidence (decisive lines)

run.log:87-89: two 'ACPI MADT: IOAPIC addr ... gsi base 0' entries then 'panic: ioapic_add: overlapped gsi, base 0 npin 1, hit base 0, npin 24'. Trigger table: valid LAPIC entries + IOAPIC(0xfec00000, base 0) + IOAPIC(0xfec88000, base 0).

PoC changes

No seed PoC; gen_madt.py scenario ioapic_overlap authored from scratch.

Verified recommended fix

probe: reject duplicate IOAPIC addresses, duplicate GSI bases and >16 IOAPIC entries (EINVAL) so ioapic_config falls back instead of panic()ing

Verdict

Deterministic unconditional boot panic from a crafted MADT IOAPIC topology: probe validates only count>=1, a GSI base of zero, non-zero addresses and Id!=255 (acpi_madt.c:615-662); enumerate feeds raw entries to ioapic_add() which panic()s on GSI overlap (ioapic.c:240), duplicate address (ioapic.c:245), and ioapic_config panics >16 IOAPICs (ioapic.c:137). Reproduced with two IO_APIC entries sharing GSI base 0: 'panic: ioapic_add: overlapped gsi, base 0 npin 1, hit base 0, npin 24' after the full LAPIC/SMP stage completed. panic() is unconditional (no INVARIANTS needed, unlike DF-5430). MADT sibling of DF-1034/DF-5339. Firmware-fed.