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)
PoC verification
Evidence pack
findings/poc/DF-5433 Β· 8 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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.
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 withACPI_MADT_ENABLEDit hands outcpu = arg->cpu++and callslapic_set_cpuid(cpu, lapic_ent->Id)β no check thatIdwas already given a slot, and no check thatIdcorresponds 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/SIPIsCPUID_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_apfailure path (mp_machdep.c:529-541) is an interactivepanic 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_testablefix.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
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.
No comments yet.