# DF-5365 — `lapic_map()` adopts firmware LAPIC physical address with no validation → boot-time arbitrary-physical-address kernel read/write

## Verdict

**REPRODUCED (boot-time, firmware-data-gated kernel memory corruption; not
userspace-reachable).** Fix **VALIDATED** on a single-fix kernel: with
`fix.diff`, `lapic_map()` cross-checks the firmware value against
`IA32_APICBASE` and falls back to the hardware page; the identical harness
run then writes **0/7** cells of the attacker page and leaves
`cpu_id_to_apic_id[3]` untouched, while printing
`LAPIC: bogus LAPIC address 0x… in firmware table, using 0xfee00000 from
APIC base MSR`.

## Mechanism (every hop cited)

`lapic_map()` at `sys/platform/pc64/apic/lapic.c:1081-1085`:

```c
void
lapic_map(vm_paddr_t lapic_addr)
{
	lapic_mem = pmap_mapdev_uncacheable(lapic_addr, sizeof(struct LAPIC));
}
```

`lapic_addr` is firmware-supplied on both enumeration paths:

- **MADT**: `madt_lapic_probe` seeds `arg.lapic_addr = madt->Address`
  (`acpi_madt.c:451`); `madt_lapic_pass1` returns it, honoring
  LOCAL_APIC_ADDRESS_OVERRIDE entries (`acpi_madt.c:~250-270`); the only
  special-cased value anywhere on the path is **0**
  (`acpi_madt.c:479-487` — and only when the CPU lacks x2APIC).
  `madt_lapic_enumerate` then calls `lapic_map(lapic_addr)`
  (`acpi_madt.c:528-531`).
- **MP table**: `lapic_addr = cth->apic_address` (`mptable.c:761`,
  `KKASSERT(lapic_addr != 0)` only) → `lapic_map(lapic_addr)`
  (`mptable.c:805`).

The CPU's own `MSR_APICBASE` (0x1b, `specialreg.h:533`) always contains the
real page base (0xFEE00000 on this guest) and is **never consulted**.

After the mapping, `lapic_init()` (`lapic.c:272-408`) performs RMW stores to
`lvt_lint0/lint1/error/pcint/timer`, `tpr`, `svr`, plus three stores of 0 to
`eoi`; `lapic_timer_calibrate()` arms `icr_timer` with `APIC_TIMER_MAX_COUNT`
(0xffffffff, `lapic.c:451/:493/:542/:591`) and writes the divisor to
`dcr_timer` (`lapic.c:440`). Every store lands at a fixed offset (eoi +0xB0,
svr +0xF0, icr_timer +0x380, dcr_timer +0x3E0, lvt_* +0x320..0x370) inside
the page the firmware table selected. Reads (`LAPIC_READID` at enumeration,
`LAPIC_READ(version)` at `lapic.c:330/380`) return page contents the
attacker also controls (info side-channel / logic steering).

### What was demonstrated on the guest (stock kernel #0, Jul 2 snapshot)

KLD harness `df5365.ko` (root `kldload`; the established evidence class for
boot-path bugs on this rig — same as DF-1064, since `-acpitable` cannot
override QEMU's built-in MADT, `sdt_search()` returns the first match):

- **Test A** — `lapic_map(phys_of_fresh_RAM_page)` (production call), page
  pre-filled 0xAAAAAAAA:
  `LAPIC_MEM_READ(version)` returned **0xAAAAAAAA** (READ primitive), and the
  `lapic_init()` store set rewrote **7/7** observed register cells
  (`run.log`): eoi=0, icr_timer=0xffffffff, dcr_timer=0x00,
  svr=0xaaaaa9ff (fill|APIC_SVR_ENABLE|vector), lint0/lint1/timer RMWed.
- **Test B** — `lapic_map(phys(&cpu_id_to_apic_id[3]) - 0x380)` so the
  oneshot-arm store aliases the live BSS array:
  `cpu_id_to_apic_id[3]` went **0x00000003 → 0xffffffff**
  (`KERNEL-BSS CORRUPTION CONFIRMED`), then restored.

The guest stayed healthy through load/unload in both baseline runs
(`run.log`, `run.2.log`).

### Threat model / impact ceiling

Precondition is control of the firmware tables presented at boot (crafted
MADT via patched BIOS/pflash; MADT injection with `-acpitable` does NOT
reach the sink because the kernel picks the first APIC-signature table).
This is the same premise already accepted for DF-1042 (Medium) and DF-1064
(High). Within that premise the primitive is stronger than DF-5341
(mptable arbitrary-PA *read*): it is an arbitrary-PA **write** page whose
fixed-offset stores include full-control values 0 and 0xffffffff and
preserving RMWs — enough to corrupt kernel text/data/heap in physical
memory at boot (e.g., zero a syscall table entry, forge pointer halves with
RMW bit-sets) and to read boot-time kernel memory through `LAPIC_READ`s
(version/ID steering). Requires memory-mapped LAPIC mode
(`x2apic_enable == 0`).

### Fix validation (kernel #1, fix.diff)

`fix.diff` adds the missing cross-check in `lapic_map()`: derive
`hw_addr = rdmsr(MSR_APICBASE) & ~0xFFF` and fall back to it (with a
`kprintf`) whenever the firmware value is 0, misaligned, or differs.
Applied to the guest `/usr/src`, `make -j6 nativekernel KERNCONF=X86_64_GENERIC`,
installed, rebooted (clean boot: normal `LAPIC: enter X2APIC mode`, no
warning on the genuine path). Identical harness run (`fix_run.log`,
`fix_run.2.log`):

- Test A: `LAPIC: bogus LAPIC address 0xd9d8000 … using 0xfee00000 from
  APIC base MSR`; **0/7** cells written; attacker page still 0xAAAAAAAA;
  `version` read returns 0xffffffff (real MMIO, architecturally ignored in
  x2APIC mode — not the attacker fill).
- Test B: `LAPIC: bogus LAPIC address 0x18d9dac …`; `cpu_id_to_apic_id[3]`
  stays **0x00000003** — `not-confirmed`.

### Harness debugging notes (poc_changes)

The first harness revision passed the modevent *function* as DECLARE_MODULE's
`data` argument (must be a `moduledata_t` struct). That made
`module_register()` treat function bytes as a `moduledata_t` and strcmp a
code-bytes "pointer" → GPF `strcmp+0x10` (dump captured on the serial log).
Fixed by declaring `static moduledata_t df5365_mod = {"df5365",
df5365_modevent, NULL};` exactly as DF-1064's harness does. Test B was also
strengthened after the first successful run (initial variant aliased an
already-zero cell; final variant aliases `cpu_id_to_apic_id[3]` (value 3)
with the 0xffffffff store for an unambiguous 3 → 0xffffffff flip).

## Artifacts

- `df5365_harness.c` — KLD harness (production `lapic_map()` + production
  store set; saves/restores state)
- `gen_madt.py` — conceptual malicious MADT (LocalApicAddr = 0x01000000)
- `build.sh` / `run.sh` — exact commands
- `build.log` — full successful build (baseline)
- `run.log`, `run.2.log` — baseline reproductions (stock kernel #0)
- `fix_run.log`, `fix_run.2.log` — patched-kernel runs (fix rejected both
  attacker addresses; no corruption)
- `env.txt` — guest uname/tunables/compiler
- `fix.diff` — validated fix (git-apply-able vs this tree; never applied to
  the read-only audit `sys/`)

## 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.
