# 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`:

```c
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:

```c
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.
