lapic_map() maps firmware-supplied Local APIC physical address with zero validation -> arbitrary-physical-address kernel read/write page at boot (MADT LocalApicAddr / MP apic_address never cross-checked against IA32_APICBASE)
Summary
The firmware table's LAPIC PA passes straight into pmap_mapdev_uncacheable with no range/alignment/identity check (only value 0 special-cased) and the CPU's own MSR_IA32_APICBASE - which cannot lie - is never consulted; lapic_init then does fixed-offset stores (eoi=0, icr_timer=0xffffffff, dcr, RMWs) and enumeration reads into the mapped page. Crafted MADT/MP = boot-time read of attacker-chosen physical memory + persistent kernel corruption (zero a function pointer, RMW-forge pointer halves). Stronger than DF-5341 (that was read-only); boot-gated, no unpriv runtime trigger. Guest-reproduced via KLD harness calling PRODUCTION lapic_map with (A) a marker RAM page - 'READ-PRIMITIVE CONFIRMED' + 7/7 register cells written - and (B) phys(&cpu_id_to_apic_id[3])-0x380 - BSS cell 0x3->0xffffffff 'KERNEL-BSS CORRUPTION CONFIRMED'; MSR-cross-check fix validated on rebuilt kernel (rejects both, 0/7 cells, genuine path clean).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5365 Β· 13 files| File | Type | Description | Size | |
|---|---|---|---|---|
| df5365_harness.c | β | 9.8 KB | view raw | |
| Makefile | β | 78 B | β download | |
| gen_madt.py | β | 1.8 KB | view raw | |
| build.sh | β | 126 B | view raw | |
| run.sh | β | 168 B | view raw | |
| build.log | β | 851 B | view raw | |
| run.log | β | 896 B | view raw | |
| run.2.log | β | 432 B | view raw | |
| fix_run.log | β | 1.0 KB | view raw | |
| fix_run.2.log | β | 376 B | view raw | |
| env.txt | β | 208 B | view raw | |
| fix.diff | β | 1.0 KB | view raw | |
| VERDICT.md | β | 6.5 KB | β raw |
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:
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_probeseedsarg.lapic_addr = madt->Address(acpi_madt.c:451);madt_lapic_pass1returns 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_enumeratethen callslapic_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 thelapic_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_READs
(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;versionread 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 (productionlapic_map()+ production store set; saves/restores state)gen_madt.pyβ conceptual malicious MADT (LocalApicAddr = 0x01000000)build.sh/run.shβ exact commandsbuild.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/compilerfix.diffβ validated fix (git-apply-able vs this tree; never applied to the read-only auditsys/)
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_testableSingle-fix kernel (fix.diff applied to guest /usr/src, make -j6 nativekernel KERNCONF=X86_64_GENERIC, clean reboot) rejects both attacker-chosen addresses with 'LAPIC: bogus LAPIC address ... using 0xfee00000 from APIC base MSR'; identical harness run writes 0/7 attacker-page cells (fill 0xaaaaaaaa intact) and cpu_id_to_apic_id[3] stays 0x00000003 (was 0xffffffff on baseline). Genuine boot path unaffected (normal LAPIC bring-up, no warning).
findings/poc/DF-5365/fix_run.log and fix_run.2.log (decisive lines quoted in VERDICT.md); fix.diff sha256 79b93263069493f023ed04f9dd51b46317e43607faaae06a802bf34bf9552023
Confirmed kernel references
- sys/platform/pc64/apic/lapic.c:1081
- sys/platform/pc64/apic/lapic.c:272
- sys/platform/pc64/apic/lapic.c:376
- sys/platform/pc64/apic/lapic.c:406
- sys/platform/pc64/apic/lapic.c:440
- sys/platform/pc64/apic/lapic.c:451
- sys/platform/pc64/acpica/acpi_madt.c:451
- sys/platform/pc64/acpica/acpi_madt.c:479
- sys/platform/pc64/acpica/acpi_madt.c:528
- sys/platform/pc64/x86_64/mptable.c:761
- sys/platform/pc64/x86_64/mptable.c:805
Detail
Exploit chain
attacker controls firmware table at boot (crafted MADT LocalApicAddr / MP apic_address) -> lapic_map() maps chosen PA as lapic_mem (xAPIC mode) -> enumeration reads (LAPIC_READID/version) read attacker-chosen kernel/physical memory -> lapic_init()+calibration stores write fixed offsets (+0xB0 eoi=0, +0x380 icr_timer=0xffffffff, +0x3E0 divisor, svr/lvt RMWs) into that page -> e.g. point the base so a chosen kernel text/data word receives 0 / 0xffffffff / bit-set RMW -> persistent kernel-memory corruption at boot; harness demonstrated the BSS flip on a live kernel
Evidence (decisive lines)
run.log lines 'DF5365: A: 7/7 ... ARBITRARY-PA WRITE CONFIRMED' and 'DF5365: B: cpu_id_to_apic_id[3] after =0xffffffff -> KERNEL-BSS CORRUPTION CONFIRMED' (baseline, stock kernel #0); fix_run.log/fix_run.2.log lines 'LAPIC: bogus LAPIC address 0x... using 0xfee00000 from APIC base MSR' + '0/7' + 'not-confirmed' (patched kernel #1); VERDICT.md has the full path:line trace
PoC changes
initial harness passed the modevent function as DECLARE_MODULE's data arg (moduledata_t struct required) -> module_register() strcmp'd function bytes -> GPF strcmp+0x10 at kldload; fixed with static moduledata_t df5365_mod = {"df5365", df5365_modevent, NULL} (DF-1064 pattern). Test B initially aliased an already-zero BSS cell; strengthened to alias cpu_id_to_apic_id[3] (value 3) with the 0xffffffff icr_timer store for an unambiguous 3 -> 0xffffffff flip. gen_madt.py header-length bug fixed (9-byte OEM table id). fix.diff hunk regenerated mechanically after two hand-counted hunks were malformed.
Verified recommended fix
In lapic_map(), cross-check the firmware address against rdmsr(MSR_APICBASE) & ~0xFFF and fall back to the hardware page (warn) when the firmware value is 0, misaligned, or mismatched.
Verdict
lapic_map() (lapic.c:1081-1085) maps the firmware-supplied Local APIC register-page physical address (MADT LocalApicAddr via acpi_madt.c:451/528-531, or MP cth->apic_address via mptable.c:761/805) with no validation whatsoever -- not even a cross-check against IA32_APICBASE -- so a crafted firmware table gives lapic_init()'s fixed-offset, partially-controlled 32-bit stores (eoi=0 x3 at lapic.c:406-408, icr_timer=0xffffffff at lapic.c:451, dcr_timer divisor at lapic.c:440, svr/lvt/tpr RMWs at lapic.c:272-400) an arbitrary-physical-address kernel read/write page at boot. Proven on the stock guest kernel with a root KLD harness (the established evidence class for boot-path bugs on this rig): production lapic_map(attacker_pa) + the production store set rewrote 7/7 observed register cells of an attacker-chosen RAM page and flipped live kernel BSS cpu_id_to_apic_id[3] from 0x3 to 0xffffffff via the icr_timer oneshot-arm store; state restored, guest healthy. Fix validated on a rebuilt kernel: with fix.diff's IA32_APICBASE cross-check the same harness run redirects to the real 0xfee00000 page, writes 0/7 attacker cells, leaves the BSS word untouched, and warns. Precondition is firmware-table control (same class as filed DF-1042/DF-1064) plus memory-mapped LAPIC mode (x2apic_enable==0: default QEMU CPU models / most bare metal / hw.x2apic_enable=0); no runtime userspace reachability.
No comments yet.