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)
PoC verification
Evidence pack
findings/poc/DF-5342 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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).
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).
4. Recommended fix
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_testablefix.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
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<
No comments yet.