# DF-5339 — probe/enumerate BSP-validation asymmetry → boot-time KKASSERT panic

## Verdict: **defect certain at code level; state machine proven on the audited guest; kernel-boot manifestation untested** (needs crafted firmware MP table; standard QEMU/SeaBIOS guest cannot inject one)

## 1. Root cause

Two callbacks validate the same MP-table processor entries with different rules:

**`mptable_lapic_probe_callback()`** — `sys/platform/pc64/x86_64/mptable.c:818-845`:

```c
if ((ent->cpu_flags & PROCENTRY_FLAG_EN) == 0)
	return 0;			/* mptable.c:827-829: disabled entries skipped ... */
arg->cpu_count++;
...
if (ent->cpu_flags & PROCENTRY_FLAG_BP) {	/* ...BEFORE the BSP bookkeeping */
	if (arg->found_bsp)
		return EINVAL;		/* mptable.c:838-841: duplicate-BSP check,
					   but only ENABLED entries ever reach it */
	arg->found_bsp = 1;
}
```

**`mptable_lapic_pass2_callback()`** — `sys/platform/pc64/x86_64/mptable.c:674-686`:

```c
if (ent->cpu_flags & PROCENTRY_FLAG_BP) {	/* NO EN check */
	KKASSERT(!arg->found_bsp);		/* mptable.c:684 — fires on the SECOND
					   BP-flagged entry, enabled or not */
	arg->found_bsp = 1;
}
```

`mptable_lapic_probe()` runs first and gates whether the mptable enumerator is
selected (`mptable.c:847-887`, `lapic_config()` at `lapic.c:1163-1179`).
`mptable_lapic_enumerate()` then re-walks the table with pass1/pass2.

## 2. Trigger and manifestation

MP configuration table:

| entry | cpu_flags | apic_id |
|-------|-----------|---------|
| #0    | EN \| BP  | 0       |
| #1    | BP (EN clear) | 1  |

* **probe**: entry #1 is disabled → skipped before the BP check → `cpu_count=1`,
  `found_bsp=1`, no duplicate seen → **probe passes**, mptable enumerator
  selected.
* **enumerate pass2**: entry #0 sets `found_bsp=1`; entry #1 carries the raw BP
  bit → `KKASSERT(!arg->found_bsp)` fails → **kernel panic at boot**.

`KKASSERT` is compiled in whenever `INVARIANTS` is configured
(`sys/sys/systm.h:94-101`); the stock kernel enables it
(`sys/config/X86_64_GENERIC:56`, and the running guest kernel is built from
that config). On non-INVARIANTS kernels the assert is compiled out and the
entry is otherwise ignored (processor_entry skips disabled entries,
`mptable.c:561-562`) — no effect.

Threat model: MP table bytes live in firmware-provided EBDA/base/BIOS memory;
the scanned regions and the table contents are attacker-controlled only by
malicious/compromised firmware or a compromised boot loader — the same class
and severity bar as accepted DF-1034 (malformed INT entry boot panic) and
DF-5326 (degenerate SMAP).

## 3. Reproduction (verbatim logic, on the audited guest)

`bsp_asymmetry.c` copies both callbacks' control flow verbatim (u_char flag
semantics preserved; `KKASSERT` modeled as a resumable kernel-panic path).
Built with the guest `cc 8.3 [DragonFly]`, `run.log`:

```
== trigger: [EN|BP] [BP] (2nd disabled BSP) ==
  probe : cpu_count=1 found_bsp=1 -> PASS (mptable enumerator selected)
  pass2 : KERNEL PANIC -> assertion "!arg->found_bsp" failed in mptable_lapic_pass2_callback at mptable.c:684
== control: [EN|BP] [EN|BP] (both enabled) ==
  probe : cpu_count=2 found_bsp=1 -> EINVAL (mptable enumerator rejected, ACPI path used)
== control: [EN|BP] [EN] (healthy table) ==
  probe : cpu_count=2 found_bsp=1 -> PASS
  pass2 : completed, found_bsp=1 (no panic)
```

The end-to-end kernel-boot panic was NOT armed: injecting an MP table requires
control of firmware-provided memory that stock QEMU/SeaBIOS does not expose
(no fw_cfg path builds `_MP_`/`PCMP` structures), and there is no
unprivileged in-guest path — the table is parsed exactly once at
`SI_BOOT2_PRESMP`.

## 4. Recommended fix

`fix.diff` — skip disabled entries before the BSP bookkeeping in pass2,
mirroring probe semantics (`processor_entry` already ignores them; the
`KKASSERT(arg2.found_bsp)` at `mptable.c:802` stays satisfied because probe
guarantees an *enabled* BSP exists, `mptable.c:876-882`).

## 5. Fix validation

`fix.diff` passes `git apply --check` against the pristine tree. A
patched-kernel boot test is not constructible on this guest for the same
reason the baseline is not (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.
