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)
PoC verification
Evidence pack
findings/poc/DF-5430 Β· 9 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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).
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) incrementslapic_countfor every enabled entry and rejects onlyId == APICID_MAX.madt_lapic_probe(acpi_madt.c:453-488) checkslapic_count == 0and the LAPIC address β it never compares any entry against the hardware BSP APIC ID thatmadt_lapic_enumerateobtains fromLAPIC_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 unconditionalpanic("assertion ...")(sys/sys/systm.h:98). - On non-INVARIANTS kernels
KKASSERTcompiles to nothing (sys/sys/systm.h:118);napsis computed from the table and slot 0 keeps the BSS-zerocpu_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_testablefix.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
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.
No comments yet.