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

HT-fixup synthetic processor entries inherit the BSP flag: lapic_set_cpuid(0, fake_id) clobbers cpu_id_to_apic_id[0] and unconditionally burned CPU slots leave phantom SIPI targets β€” SMP boot hang / permanent cpu-0 IPI mis-addressing

Summary

Fake-entry synthesis copies ent->cpu_flags INCLUDING BP into synthetic entries (the fixup engages exactly on under-reporting tables, so the BSP entry derives fakes) - BSP-derived fakes take processor_entry's BSP branch: lapic_set_cpuid(0, fake_id) clobbers the real cpu0 mapping, the fake never registers, burned slots leave phantom ids -> start_all_aps SIPIs phantoms (boot hang) and later IPIs to cpu0 are mis-addressed. Guest verbatim-logic reproducer both scenarios. strip-BP-bit + advance-on-registration fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5343 Β· 11 files
FileTypeDescriptionSize
htfix_bp_flag_inherit.c β€” 5.7 KB view raw
build.sh β€” 124 B view raw
run.sh β€” 104 B view raw
README.md β€” 780 B ↓ raw
VERDICT.md β€” 4.8 KB ↓ raw
build.log β€” 10 B view raw
run.log β€” 1.0 KB view raw
env.txt β€” 190 B view raw
fix.diff β€” 691 B view raw
manifest.json β€” 1.1 KB view raw
verdict.json β€” 4.1 KB view raw

DF-5343 β€” HT-fixup fake processor entries inherit the BSP flag -> cpu0 APIC mapping clobbered, boot hang

Boot-time (firmware/loader-gated) defect in sys/platform/pc64/x86_64/mptable.c. The kernel-boot manifestation cannot be armed on the standard QEMU/SeaBIOS guest (no MP-table injection path, no unprivileged in-guest trigger), so per the DF-5326 precedent this pack carries a verbatim-logic reproducer that was built and executed ON the audited guest and proves the exact code-level divergence (see run.log). Kernel impact classification: untested.

Build (guest or host)

cc -O2 -Wall -Wextra -o htfix_bp_flag_inherit htfix_bp_flag_inherit.c

Run

./htfix_bp_flag_inherit

Expected

See run.log (decisive lines quoted in verdict.json:evidence).

VERDICT.md
↓ download raw

DF-5343 β€” HT-fixup fake processor entries inherit the BSP flag β†’ cpu_id_to_apic_id[0] clobbered β†’ boot hang

Verdict: defect certain at code level; mapping corruption proven on the audited guest with verbatim pass2/processor_entry logic; kernel-boot manifestation untested (needs an under-reporting MP table on HT hardware booted via mptable)

1. Root cause

mptable_lapic_pass2_callback() β€” sys/platform/pc64/x86_64/mptable.c:691-710:

bzero(&proc, sizeof(proc));
proc.type = 0;
proc.cpu_flags = (force_enable) ? PROCENTRY_FLAG_EN : ent->cpu_flags;
                          /* mptable.c:702 β€” copies BP along with EN */
proc.apic_id = ent->apic_id;

for (i = 1; i < arg->logical_cpus; i++) {
    proc.apic_id++;
    processor_entry(&proc, arg->cpu);
    arg->cpu++;               /* mptable.c:708 β€” unconditional */
}

processor_entry() β€” mptable.c:556-577:

if (!(entry->cpu_flags & PROCENTRY_FLAG_EN))
    return 0;
if (entry->cpu_flags & PROCENTRY_FLAG_BP) {
    lapic_set_cpuid(0, entry->apic_id);   /* mptable.c:566 β€” CPU 0 remapped! */
    return 0;                 /* never registers in its slot */
} else if (cpu < MAXCPU) {
    lapic_set_cpuid(cpu, entry->apic_id);
    return 1;
}

Every MP table must contain a BSP entry (mptable_lapic_probe requires it, mptable.c:876-882), and that entry is necessarily EN|BP. The HT fixup engages precisely on under-reporting tables β€” the cpu_count == 1 single-BSP case (mptable.c:433-435) is the canonical one β€” so the fake entries synthesized from the BSP entry carry the BP bit, take the BSP branch of processor_entry(), and:

  1. lapic_set_cpuid(0, fake_apic_id) runs for each fake β†’ cpu_id_to_apic_id[0] is clobbered to bsp_id + logical_cpus - 1 (last fake wins; arrays at lapic.c:122-123), and every IPI addressed to CPU 0 goes to a wrong/phantom APIC id from then on;
  2. the fake returns 0 (not registered), but the caller's unconditional arg->cpu++ burns the CPU slot anyway β€” the slot keeps its BSS value cpu_id_to_apic_id[slot] == 0;
  3. start_all_aps() (mp_machdep.c:482-505) SIPIs CPUID_TO_APICID(x) for x = 1..naps β€” which now includes slots mapped to apic id 0 (the BSP itself) or phantom ids β†’ the AP never checks in β†’ boot spins forever on smp_startup_mask (mp_machdep.c:595).

The force_enable arm of the ternary (PROCENTRY_FLAG_EN only, no BP) shows the intended semantics for fakes β€” the non-force arm simply forgot to strip the flag.

2. Reproduction (verbatim logic, on the audited guest)

htfix_bp_flag_inherit.c copies processor_entry(), the fake-entry loop and lapic_set_cpuid() verbatim over the kernel's BSS arrays. run.log:

scenario 1: table = { BSP id0 }, HTT, lcpus_max=2 -> logical_cpus=2
  after fake loop: arg->cpu=2 (slot 1 burned, nothing registered)
    cpu->apic: [0]=1   [1]=0   [2]=0   [3]=0     naps=1
    start_all_aps SIPI targets: cpu1->apic0(== BSP id, phantom: never starts)
      RESULT: cpu0 remapped to apic 1 (BSP id clobbered); slot 1 -> BSS 0;
          start_all_aps SIPIs apic 0 for cpu1 -> mask never completes -> BOOT HANG

scenario 2: table = { BSP id0, AP id2 }, logical_cpus=2 -> naps=3
    cpu->apic: [0]=1   [1]=0   [2]=2   [3]=3     naps=3
      RESULT: cpu0 -> apic 1 (clobbered), cpu1 -> BSS 0 (unregistered slot)

scenario 3 (intent/fix): force_enable=1 -> fakes carry EN only
    cpu->apic: [0]=0   [1]=1   [2]=2   [3]=3     naps=3
      RESULT: cpu0 -> apic 0 (correct), cpu1 -> apic 1, cpu2 -> apic 2, cpu3 -> apic 3

Scenario 3 doubles as the fix validation: identical inputs, fakes without the BP bit and registration-gated cpu++ produce the correct 0/1/2/3 mapping.

3. Threat model / why kernel-boot manifestation is untested

Engages on any mptable-enumerated (non-ACPI) HT system whose firmware table under-reports logical CPUs β€” buggy-but-benign legacy firmware or crafted malicious tables (same class as DF-1034). The standard guest boots via ACPI MADT, so the path does not run there, and there is no runtime or unprivileged reach (SI_BOOT2_PRESMP/SI_BOOT2_LAPIC one-shot).

fix.diff β€” synthetic logical CPUs are never the BSP: strip PROCENTRY_FLAG_BP from the copied flags, and advance arg->cpu only when processor_entry() actually registered the fake.

5. Fix validation

git apply --check passes; scenario 3 of the reproducer validates the fix's semantics at logic level. Patched-kernel A/B boot test not constructible on this guest (no table injection), hence fix_status: not_testable.

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 no→ patch + rebuild →patched clean

fix.diff is authored against the read-only sys/ tree and passes 'git apply --check', but could not be kernel-build-validated: the bug's kernel manifestation cannot be armed on this guest (needs injected firmware tables), so there is no baseline to compare a patched kernel against.

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

Confirmed kernel references

Detail

Evidence (decisive lines)

run.log β€” scenario 1: 'cpu->apic: [0]=1 [1]=0 ... cpu1->apic0(== BSP id, phantom: never starts) -> BOOT HANG'; scenario 2: '[0]=1 [1]=0 [2]=2 [3]=3'; scenario 3 (force_enable): '[0]=0 [1]=1 [2]=2 [3]=3 (correct)'.

PoC changes

No seed PoC existed; authored from scratch. Reproducer copies the cited mptable.c logic verbatim (u_int/u_char semantics preserved); KKASSERT is modeled as a kernel-panic path (setjmp-resumable in the harness), and pmap_mapdev/MMIO access is elided where userland cannot express it (clearly marked in the source header).

Verified recommended fix

clear PROCENTRY_FLAG_BP on synthetic entries and gate arg->cpu++ on registration (mptable.c:702-709)

Verdict

Defect is certain at code level; mapping corruption proven on the audited guest with verbatim logic. mptable_lapic_pass2_callback() synthesizes fake logical CPUs with proc.cpu_flags = ent->cpu_flags (mptable.c:702) and advances arg->cpu unconditionally (mptable.c:705-709). The BSP entry is mandatory in every table, is necessarily EN|BP, and the fixup engages precisely on under-reporting tables (cpu_count==1 single-BSP case, mptable.c:433-435, or spaced ids), so its fake entries carry the BP bit, take processor_entry()'s BSP branch (mptable.c:565-568) and call lapic_set_cpuid(0, fake_apic_id) β€” cpu_id_to_apic_id[0] is clobbered to bsp_id+logical_cpus-1, the fake never registers in its slot, and the burned slot later reads apic id 0 from BSS. Reproducer scenario 1 (table = {BSP id0}, lcpus_max=2): cpu0->apic1, slot1 unregistered, naps=1 -> start_all_aps SIPIs CPUID_TO_APICID(1)=0 (the BSP itself) -> smp_startup_mask never completes -> BOOT HANG (mp_machdep.c:595); all IPIs to cpu0 are mis-addressed thereafter. Scenario 2 (ids {0,2}, naps=3) shows the same clobber plus a skipped slot. Scenario 3 with force_enable=1 (fakes get EN only) produces the correct 0/1/2/3 mapping β€” proving both the intent of the ternary and the fix. Engages on ANY mptable (non-ACPI) HT system whose fixup activates with default tunables; end-to-end kernel manifestation UNTESTED (needs crafted/legacy firmware table on HT hardware; the standard guest boots via ACPI MADT).