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

u_int truncation of the HT-fixup APIC-id mask for ids 32..63 silently corrupts the fixup decision β€” phantom logical CPUs enumerated, boot hangs on smp_startup_mask

Summary

ht_apicid_mask is u_int but |= 1UL<<id: ids 32..63 truncate to 0 and vanish from the mask while the fixup-disable guard only covers id>=64 - {0,2,33} engages the fixup on the partial mask where the true mask aborts; cpu_count *= logical_cpus inflates naps and pass2 synthesizes fake entries with nonexistent ids -> start_all_aps SIPIs phantoms, boot spins forever. Guest verbatim-logic reproducer: engaged-with-phantoms vs true-mask-killed. The in-file 'XXX fixup for cpus >= 32' comment marks the band. Widen-to-uint64 fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5342 Β· 11 files
FileTypeDescriptionSize
htfix_mask_truncation.c β€” 5.5 KB view raw
build.sh β€” 124 B view raw
run.sh β€” 104 B view raw
README.md β€” 766 B ↓ raw
VERDICT.md β€” 4.2 KB ↓ raw
build.log β€” 10 B view raw
run.log β€” 842 B view raw
env.txt β€” 190 B view raw
fix.diff β€” 582 B view raw
manifest.json β€” 965 B view raw
verdict.json β€” 3.8 KB view raw

DF-5342 β€” u_int truncation of HT-fixup APIC-id mask for ids 32..63 -> phantom logical CPUs

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_mask_truncation htfix_mask_truncation.c

Run

./htfix_mask_truncation

Expected

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

VERDICT.md
↓ download raw

DF-5342 β€” u_int truncation of the HT-fixup APIC-id mask for ids 32..63 β†’ phantom logical CPUs β†’ boot hang

Verdict: defect certain at code level; decision divergence proven on the audited guest with verbatim fixup logic; kernel-boot manifestation untested (needs crafted firmware table on HT hardware booted via mptable)

1. Root cause

mptable_lapic_pass1_callback() β€” sys/platform/pc64/x86_64/mptable.c:644-665:

struct mptable_lapic_cbarg1 {
    int cpu_count;
    int ht_fixup;
    u_int   ht_apicid_mask;         /* mptable.c:641 β€” 32 bits */
};
...
    arg->cpu_count++;
    if (ent->apic_id < 64) {
        arg->ht_apicid_mask |= 1UL << ent->apic_id; /* mptable.c:659 */
    } else if (arg->ht_fixup) {
        kprintf("MPTABLE: lapic id > 64, disable HTT fixup\n");
        arg->ht_fixup = 0;              /* mptable.c:660-663 */
    }

On pc64 1UL << id is a 64-bit shift; for 32 <= id <= 63 the resulting bit is silently truncated to 0 when stored into the u_int β€” the id vanishes from the mask while the fixup stays ENABLED (the disable path only triggers for id >= 64). The mask then feeds the decision machinery:

CPUMASK_ASSZERO(mask);
mask.ary[0] = arg1.ht_apicid_mask;          /* mptable.c:784 */
logical_cpus = mptable_hyperthread_fixup(mask, arg1.cpu_count);
if (logical_cpus != 0)
    arg1.cpu_count *= logical_cpus;         /* mptable.c:787 */
naps = arg1.cpu_count - 1;

mptable_hyperthread_fixup() (mptable.c:400-498) guards its decision with (a) all-pairwise distances equal, (b) power-of-2, (c) boundary/sibling checks β€” but over the partial mask, so tables whose full id set would abort the fixup can pass with the truncated one. pass2 then synthesizes fake processor entries (mptable.c:691-710) for ids that do not exist, start_all_aps() SIPIs them (mp_machdep.c:482-505), and boot waits forever on smp_startup_mask (mp_machdep.c:595). The in-file comment XXX fixup for cpus >= 32 ? XXX (mptable.c:777-779) shows the author suspected this band without handling it.

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

htfix_mask_truncation.c copies mptable_hyperthread_fixup() and the pass1 mask build verbatim (CPUMASK helpers mirror sys/cpu/x86_64/include/cpumask.h: ary[4], word index & 3). Trigger table: ids {0, 2, 33}, lcpus_max = 2 (HTT CPU). run.log:

== trigger table: ids {0,2,33}, lcpus_max=2 ==
  ids present      : { 0 2 33 }  cpu_count=3 lcpus_max=2
  in-tree u_int mask=0x5 -> fixup ENGAGES: logical_cpus=2
     cpu_count *= logical_cpus -> 6, naps = 5
     pass2 fakes enumerated for EVERY entry: apic ids { 1 3 34 } (phantom ids)
  true 64-bit mask=0x200000005 -> fixup returns 0 (killed β€” the intended decision)
== control table: ids {0,2}, lcpus_max=2 ==
  in-tree u_int mask=0x5 -> fixup killed (return 0)

With the in-tree u_int mask, id 33's bit (1UL<<33) truncates to 0; the fixup sees {0,2}, computes dist=2, passes the boundary checks and engages β€” enumerating phantom ids 1, 3, 34. With a non-truncating 64-bit mask the distance set {2, 31} fails the all-distances-equal guard (mptable.c:462) and the fixup is killed β€” proving the truncation alone flips the decision.

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

Requires an MP table with APIC ids in the 32..63 band crafted to pass the partial-mask checks, on HTT hardware, on a system that enumerates CPUs via mptable (i.e. no ACPI MADT β€” the standard guest uses ACPI, so the path never runs there). Firmware/boot-loader threat class; no runtime or unprivileged reach (mptable_lapic_enumerate runs once at SI_BOOT2_PRESMP/SI_BOOT2_LAPIC).

fix.diff β€” widen ht_apicid_mask to uint64_t and use 1ULL << shifts; mask.ary[0] = ht_apicid_mask then carries every id < 64 (ids >= 64 already disable the fixup).

5. Fix validation

git apply --check passes. Patched-kernel A/B not constructible on this guest (same injection constraint as the baseline), 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-5342/fix.diff (git apply --check passes); reasoning in VERDICT.md
↓ fix.diffper-fix-DF-5342

Confirmed kernel references

Detail

Evidence (decisive lines)

run.log β€” trigger ids {0,2,33}: 'in-tree u_int mask=0x5 -> fixup ENGAGES: logical_cpus=2 ... phantom ids { 1 3 34 }' vs 'true 64-bit mask=0x200000005 -> fixup returns 0 (killed)'; control ids {0,2} and {0,33,34} killed in both variants.

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

widen ht_apicid_mask to uint64_t and use 1ULL shifts (mptable.c:641,659)

Verdict

Defect is certain at code level; decision divergence proven on the audited guest. mptable_lapic_pass1_callback() accumulates the HT-fixup APIC-id mask in a u_int: ht_apicid_mask |= 1UL << ent->apic_id for apic_id < 64 (mptable.c:658-659). On pc64, 1UL<= 64, mptable.c:660-663); mask.ary[0] = ht_apicid_mask (mptable.c:784) then feeds mptable_hyperthread_fixup(), whose distance/boundary guards run over the partial set. For a table listing ids {0,2,33} on an HTT CPU with lcpus_max=2 the verbatim-logic reproducer shows the in-tree mask (0x5) passing the guards -> logical_cpus=2 -> cpu_count*=2 -> naps=5 with fake entries enumerated for phantom apic ids 1,3,34, while the true 64-bit mask (distances 2 vs 31 mismatch) correctly kills the fixup (return 0). Phantom ids are SIPI'd by start_all_aps (mp_machdep.c:482-505) and never check in, so boot waits forever on smp_startup_mask (mp_machdep.c:595) -> boot hang on mptable (non-ACPI) HT systems with such firmware tables. End-to-end manifestation UNTESTED (needs crafted firmware table + HTT CPU; standard guest uses the ACPI MADT path). The in-file comment 'XXX fixup for cpus >= 32 ? XXX' (mptable.c:777-779) shows the author suspected this band without handling it.