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

MADT BSP-entry omission passes madt_lapic_probe, then KKASSERT(arg.bsp_found) panics the boot (production kernels silently mis-map cpu0)

Summary

The probe aggregates only counts, never comparing entries against the hardware LAPIC_READID; enumerate asserts bsp_found. Real injected MADT (hint.acpi.0.rsdp kenv -> unvalidated RSDP -> QEMU loader blob): 'panic: assertion arg.bsp_found failed at acpi_madt.c:375' after the probe accepted lapic_count=2; on non-INVARIANTS kernels slot 0 silently keeps BSS-zero cpu_id_to_apic_id[0] (DF-5366), mis-addressing every cpu-0 IPI. MADT sibling of mptable DF-5339. Assert-to-EINVAL + propagate-error fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5430 Β· 9 files
FileTypeDescriptionSize
README.md β€” 2.3 KB ↓ raw
VERDICT.md β€” 3.4 KB ↓ raw
gen_madt.py β€” 4.8 KB view raw
build.sh β€” 112 B view raw
run.sh β€” 274 B view raw
run.log β€” 6.1 KB view raw
run.2.log β€” 6.1 KB view raw
env.txt β€” 2.1 KB view raw
fix.diff β€” 837 B view raw

DF-5430 β€” MADT BSP-entry omission passes probe, trips KKASSERT(arg.bsp_found) boot panic

What

sys/platform/pc64/acpica/acpi_madt.c β€” madt_lapic_probe() never verifies that the hardware BSP's APIC ID (from LAPIC_READID) appears among the enabled CPU entries; madt_lapic_pass2() then KKASSERT(arg.bsp_found) (acpi_madt.c:375). A crafted MADT whose enabled entries all have Id != bsp_apic_id sails through probe and panics the kernel at boot (INVARIANTS kernels). On production (non-INVARIANTS) kernels the assert is compiled out (sys/sys/systm.h:118) and the BSP silently keeps the BSS-zero cpu_id_to_apic_id[0] mapping (DF-5366's missing sentinel), mis-addressing cpu 0 whenever the hardware BSP ID is non-zero.

Rig (real kernel-boot manifestation, not a userland model)

The standard QEMU guest accepts a kenv hint that redirects the entire ACPI root: loader.conf hint.acpi.0.rsdp=0x40000000 is consumed by sdt_probe() (acpi_sdt.c:71) with no signature or checksum validation, and QEMU's -device loader,file=...,addr=0x40000000,force-raw=on plants an attacker-crafted RSDP→RSDT→MADT chain in guest RAM before boot. (-acpitable does NOT work for this: it appends after the built-in tables and sdt_search_xsdt() takes the first signature match, acpi_sdt.c:211-221.)

Build

python3 gen_madt.py bsp_omit /tmp/madt_bsp_omit.bin

Run

# guest booted normally once, then:
vm.sh run_root 'printf "hint.acpi.0.rsdp=0x40000000\n" >> /boot/loader.conf; sync'
vm.sh down
QEMU_EXTRA="-device loader,file=/tmp/madt_bsp_omit.bin,addr=0x40000000,force-raw=on" vm.sh up
vm.sh log   # watch serial console

Expected output (see run.log / run.2.log)

madt_lapic_probe: lapic_count=2 x2apic_count=0     <- probe ACCEPTS the table
panic: assertion "arg.bsp_found" failed in madt_lapic_pass2 at \
    /usr/src/sys/platform/pc64/acpica/acpi_madt.c:375

Reproduced on both guest kernel builds (Jul 2 "with-src" build and the Jun 29 clean-install build). Controls: control scenario (BSP entry present) boots through the full MADT→SMP path; see ../DF-5432 pack for a control run.

Trigger table (bsp_omit)

MADT with Address=0xFEE00000, Flags=1, and exactly two enabled LOCAL_APIC entries: {ProcessorId=1, Id=1, EN}, {ProcessorId=2, Id=2, EN} β€” no entry with Id == 0 (the KVM BSP's hardware APIC ID).

VERDICT.md
↓ download raw

DF-5430 β€” VERDICT

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

run kernel result
run.log 6.5-DEVELOPMENT #0 Thu Jul 2 06:02:54 (with-src build) panic at acpi_madt.c:375
run.2.log v6.5.0.1712.g89e6a #1 Mon Jun 29 (clean-install build) panic at acpi_madt.c:375

Decisive lines (run.log:43-44, run.2.log:43-44):

madt_lapic_probe: lapic_count=2 x2apic_count=0
panic: assertion "arg.bsp_found" failed in madt_lapic_pass2 at /usr/src/sys/platform/pc64/acpica/acpi_madt.c:375

2. Why it is real (path:line)

  • Probe aggregates only entry COUNTS: madt_lapic_probe_callback (acpi_madt.c:391-407) increments lapic_count for every enabled entry and rejects only Id == APICID_MAX. madt_lapic_probe (acpi_madt.c:453-488) checks lapic_count == 0 and the LAPIC address β€” it never compares any entry against the hardware BSP APIC ID that madt_lapic_enumerate obtains from LAPIC_READID (acpi_madt.c:533).
  • Enumerate then runs pass2, where the only BSP sanity is KKASSERT(arg.bsp_found) (acpi_madt.c:375). On the guest's INVARIANTS kernel (sys/config/X86_64_GENERIC:56) that is an unconditional panic("assertion ...") (sys/sys/systm.h:98).
  • On non-INVARIANTS kernels KKASSERT compiles to nothing (sys/sys/systm.h:118); naps is computed from the table and slot 0 keeps the BSS-zero cpu_id_to_apic_id[0] == 0 (lapic.c:122; init hole documented by DF-5366) β€” a hardware BSP with ID != 0 then gets every cpu-0-targeted IPI delivered to the wrong local APIC.

This is the MADT sibling of mptable's DF-5339 (probe/enumerate BSP-validation asymmetry) β€” same defect shape, different table.

3. Threat model / preconditions

The MADT is firmware-fed. Arming it requires control of the ACPI tables presented at boot: malicious/compromised hypervisor host, crafted UEFI firmware, a bootloader-kenv override (hint.acpi.0.rsdp=<phys>, the exact vector used here β€” it performs zero validation, acpi_sdp.c:71), or any console/loader access. No unprivileged in-guest path exists. Deterministic boot-time DoS (panic) on INVARIANTS kernels; silent cpu-0 mis-addressing on production kernels. Low severity per the project's accepted bar for this class (DF-1034, DF-5339).

4. Exploit chain

None β€” boot-time firmware-fed DoS. No memory-corruption primitive: the panic is a controlled assertion failure before any OOB write (the cpu counter stays small; the OOB-counter variant is DF-1064, not re-reported here).

5. Fix

fix.diff converts the assert into graceful enumeration failure: madt_lapic_pass2 returns EINVAL, madt_lapic_enumerate propagates it, and lapic_config() (lapic.c:1268-1272) already handles enumerate errors ("LAPIC: enumeration failed", tries the next enumerator / ENXIO) β€” the machine degrades instead of panicking. git apply --check passes against the read-only sys/ tree. Fix not kernel-build-validated in this run: the single mandatory build slot was spent on the memory-corruption finding DF-5432 (same-run budget; this finding is a Low boot-time panic).

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 (KKASSERT -> graceful EINVAL + propagation; lapic_config already handles enumerate errors at lapic.c:1268-1272) passes git apply --check against the read-only sys/ tree. Not kernel-build-validated: the run's single mandatory build slot was spent on the reproduced memory-corruption finding DF-5432; this finding is a Low firmware-fed boot panic (DF-5339 precedent).

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

Confirmed kernel references

Detail

Evidence (decisive lines)

run.log:43-44 and run.2.log:43-44: 'madt_lapic_probe: lapic_count=2 x2apic_count=0' followed by 'panic: assertion "arg.bsp_found" failed in madt_lapic_pass2 at .../acpi_madt.c:375'. Trigger table: two enabled non-BSP LAPIC entries, no Id==0 entry (KVM BSP id 0).

PoC changes

No seed PoC. gen_madt.py authored from scratch; discovers that -acpitable is useless here (sdt_search first-match shadows appended tables) and uses the hint.acpi.0.rsdp + QEMU loader-device rig instead for a real kernel-boot manifestation.

Verified recommended fix

pass2: return EINVAL when bsp_found is unset; propagate from madt_lapic_enumerate so lapic_config() falls back gracefully instead of KKASSERT panic

Verdict

Deterministic boot-time kernel panic, reproduced on two independent stock kernel builds via a real MADT injection (unvalidated hint.acpi.0.rsdp kenv + QEMU -device loader RSDP->RSDT->MADT chain). madt_lapic_probe() (acpi_madt.c:453-488) aggregates only entry counts and never checks that the hardware BSP APIC ID (LAPIC_READID, acpi_madt.c:533) appears among enabled entries; madt_lapic_pass2() then trips KKASSERT(arg.bsp_found) (acpi_madt.c:375) -> panic on INVARIANTS kernels (X86_64_GENERIC:56). On non-INVARIANTS builds the assert compiles out (systm.h:118) and slot 0 keeps the BSS-zero cpu_id_to_apic_id[0] (DF-5366), silently mis-addressing cpu0 when the hardware BSP ID is non-zero. mptable sibling of DF-5339. Firmware/bootloader-fed; no unprivileged in-guest path.