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

No APIC-ID dedup in MADT CPU enumeration: duplicate entries create phantom AP slots and silently wedge SMP boot

Summary

Both pass2 callbacks hand a cpu slot to every enabled entry without deduplicating APIC IDs: naps inflates and start_all_aps SIPIs phantom slots. Real injected MADT (3 entries, id 1 twice): probe accepts 'ACPI CPUS = 3', 'Warning: No next valid APICID found', then silent wedge inside start_all_aps (worse than a panic - no diagnostic). Distinct from DF-1064 (>255 needed) and DF-5366. Seen-bitmap dedup fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5433 Β· 8 files
FileTypeDescriptionSize
README.md β€” 1.9 KB ↓ raw
VERDICT.md β€” 3.1 KB ↓ raw
gen_madt.py β€” 4.8 KB view raw
build.sh β€” 110 B view raw
run.sh β€” 273 B view raw
run.log β€” 5.9 KB view raw
env.txt β€” 2.1 KB view raw
fix.diff β€” 1.3 KB view raw

DF-5433 β€” No APIC-ID dedup in MADT CPU enumeration β†’ duplicate entries create phantom AP slots, SMP boot wedges

What

madt_lapic_pass2_callback / madt_x2apic_pass2_callback (acpi_madt.c:280-346) assign a fresh cpu slot to EVERY enabled entry without checking whether that APIC ID was already assigned (and without checking the ID names a real CPU β€” there is no cross-check against anything). Duplicates inflate naps (acpi_madt.c:376) and leave two cpu slots pointing at one local APIC; nonexistent IDs leave a slot pointing at nothing. start_all_aps() (mp_machdep.c:482-541) then SIPIs the phantom slot: a duplicate ID re-INITs an already-running AP and a nonexistent ID times out β€” either way SMP startup never completes. This is the MADT sibling of mptable's DF-5343, and is DISTINCT from DF-1064: it triggers with as few as 3 entries, entirely in-bounds, no OOB write involved (DF-1064's note that "probe never deduplicates" is about enabling the counter OOB; the phantom-CPU mis-boot itself was never filed).

Rig

Identical to ../DF-5430 (hint.acpi.0.rsdp + QEMU -device loader).

Build

python3 gen_madt.py dup_cpu /tmp/madt_dup_cpu.bin

Run

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_dup_cpu.bin,addr=0x40000000,force-raw=on" vm.sh up 110
vm.sh log

Expected output (run.log)

madt_lapic_probe: lapic_count=3 x2apic_count=0
ACPI CPUS = 3
Warning: No next valid APICID found. Returning -1
SMI Frequency (worst case): 35714 Hz (28 us)
[boot stops β€” no "Initialize MI interrupts", no SMP CPU1 banner,
 ssh never comes up; guest wedged in start_all_aps]

Trigger table: LAPIC{0,0,EN}, LAPIC{1,1,EN}, LAPIC{2,1,EN} β€” APIC ID 1 appears in two enabled entries, so cpu1 and cpu2 both target LAPIC 1 while only one physical AP exists for it.

VERDICT.md
↓ download raw

DF-5433 β€” VERDICT

1. Reproduced? YES β€” deterministic SMP boot wedge (DoS)

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

madt_lapic_probe: lapic_count=3 x2apic_count=0     <- probe accepts dups
ACPI CPUS = 3                                       <- naps inflated to 2
Warning: No next valid APICID found. Returning -1   <- topology walk confused by dup id
SMI Frequency (worst case): 35714 Hz (28 us)
[nothing further β€” no "Initialize MI interrupts", no SMP CPU1 banner]

vm.sh up timed out (ssh never answered); the guest stayed powered-on but dead inside start_all_aps() β€” a silent wedge, worse to diagnose than a panic. Classification per contract table: timeout + wedged guest β‡’ reproduced / dos.

2. Why it is real (path:line)

  • madt_lapic_pass2_callback (acpi_madt.c:290-315): for every entry with ACPI_MADT_ENABLED it hands out cpu = arg->cpu++ and calls lapic_set_cpuid(cpu, lapic_ent->Id) β€” no check that Id was already given a slot, and no check that Id corresponds to a CPU the system can start. Same for x2APIC (:330-344).
  • Probe counts the same way (:399-407, :412-420) β€” duplicates and phantoms both inflate lapic_count/x2apic_count, so the table is accepted.
  • Consumer: naps = arg.cpu - 1 (:376) β†’ start_all_aps (mp_machdep.c:482): per slot x, start_ap() INIT/SIPIs CPUID_TO_APICID(x). For the duplicate: slot2's INIT lands on the already-running AP that serves slot1, so a gd slot never comes up; for a nonexistent ID the SIPI goes nowhere. start_ap failure path (mp_machdep.c:529-541) is an interactive panic y/n? prompt β€” on a serial console with no input the boot simply wedges before that.

Distinct from DF-1064 (entry-count OOB writes: needs >255 entries) β€” this manifests with 3 entries and no memory corruption. Distinct from DF-5366 (cpu_id_to_apic_id sentinel) β€” the failure is enumeration topology, not map lookup.

3. Threat model

Firmware-fed table, same vector as DF-5430/5431/5432 (demonstrated via the unvalidated hint.acpi.0.rsdp kenv + QEMU loader blob). No unprivileged in-guest path. Boot-time DoS (wedge). Low severity, DF-5343 bar.

4. Exploit chain

None β€” no memory unsafety; in-bounds indices only.

5. Fix

fix.diff adds an APIC-ID seen-bitmap to the pass2 cbarg and skips duplicate entries in both callbacks (madt_lapic_id_seen, prints and ignores). git apply --check passes. Nonexistent-ID phantoms are inherent to trusting firmware enumeration (they degrade to the existing start_ap timeout/panic path, which at least prints) β€” the dedup removes the duplicate-id class that wedges silently. Not kernel-build-validated this run (mandatory build slot spent on DF-5432; Low DoS class, DF-5339/DF-5343 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 (madt_lapic_id_seen seen-bitmap; duplicates are skipped with a kprintf in both callbacks) passes git apply --check. Not kernel-build-validated this run (mandatory slot spent on DF-5432; Low DoS class, DF-5343 precedent). Nonexistent-ID phantoms degrade to the existing start_ap timeout path which at least prints.

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

Confirmed kernel references

Detail

Evidence (decisive lines)

run.log:72-76+: 'madt_lapic_probe: lapic_count=3 x2apic_count=0', 'ACPI CPUS = 3', 'Warning: No next valid APICID found. Returning -1', log ends at 'SMI Frequency (worst case): 35714 Hz (28 us)' with vm.sh up timing out and the guest wedged in start_all_aps. Trigger: two enabled entries with APIC ID 1.

PoC changes

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

Verified recommended fix

pass2: track assigned APIC IDs (seen-bitmap) and skip duplicate entries in both LAPIC and x2APIC callbacks

Verdict

Deterministic SMP boot wedge from duplicate CPU entries in a crafted MADT. Neither pass2 callback dedups APIC IDs (acpi_madt.c:290-315, 330-344) nor checks the ID names a startable CPU; duplicates inflate naps (:376) and leave two cpu slots targeting one local APIC. Reproduced with LAPIC{0,0},{1,1},{2,1}: probe accepts (lapic_count=3), 'ACPI CPUS = 3', topology walk emits 'Warning: No next valid APICID found', then start_all_aps() wedges silently (no 'Initialize MI interrupts', no CPU1 banner, ssh never answers; guest stays powered on, dead). Distinct from DF-1064 (needs >255 entries for its OOB; this triggers with 3 entries, in-bounds) and from DF-5366 (sentinel). MADT sibling of mptable's DF-5343. Firmware-fed boot-time DoS.