Bare Metal Gotchas
Hard-won lessons from real hardware debugging. Violating any of these will crash on bare metal while appearing to work fine in VMs. CLAUDE.md carries the short-form list as a per-turn reminder; the full incident histories live here.
ISR / IDT
- No LAPIC TPR writes in ISR path. The IDT
isr_handlermust track IRQL in software only -- nolapic_write(LAPIC_REG_TPR, ...)on interrupt entry/exit. The LAPIC hardware handles vector priority masking via ISR/PPR. TPR writes break emulated LAPIC on WHPX/VBox/TCG. - The LAPIC TPR has exactly three writers -- do not add a fourth without updating
current_irql.KeRaiseIrql/KeLowerIrql(irql.c) skip the TPR MMIO store when an IRQL transition does not change the TPR class (PASSIVE<->APC, the 0xFF system band, nested same-level). This is only safe because the hardware TPR always mirrorsirql_to_tpr(current_irql): the sole writers areirql_write_tpr_value()and the twolapic.cinit writes (bothTPR=0=PASSIVE). A new out-of-bandlapic_write(LAPIC_REG_TPR, ...)that sets the TPR without updatingcurrent_irqlwould make a later same-classKeLowerIrqlskip a now-necessary write and under-mask interrupts.test_sched.cpins the mapping half of the invariant. - No CLAC/STAC without SMAP CPUID.
clacandstaccause #UD on CPUs without SMAP in CPUID -- including QEMU TCG and VirtualBox NEM. The ISR common stub must NOT useclac/stacuntil SMAP is actually enabled via per-process page tables. This was the root cause of the 2026-03-28 "hardware interrupts crash on bare metal/TCG" issue. - GS_BASE must be set before any interrupt fires.
smp_early_bsp_init()is called as the first thing inboot_phase0()-- before serial init. On bare metal,GS_BASEdefaults to 0;smp_this_cpu()reads garbage from the real-mode IVT at physical address 0 instead of NULL, crashing the IRQL tracking inisr_handler.
MMU / Memory
- No SMEP/SMAP until per-process page tables. The shared identity-mapped address space uses 2 MiB pages; user stacks are
kmalloc'd from the kernel heap, so user and kernel data share the same 2 MiB pages. Clearing User bit from "kernel" pages also blocks user-mode stack access.hv_supports_cr4_smep_smap()returns 0 forPLATFORM_BARE_METAL. - No MMIO through WB-cached pages. HPET, ECAM, NVMe BARs, and future GPU BARs must use
vmm_map_mmio_uc()with UC (uncacheable) attributes. The bootloader identity-maps everything as WB. LAPIC/IOAPIC work only because MTRRs override those ranges to UC. HPET calibration usesvmm_map_mmio_uc()(fixed 2026-03-29). - CR3 reloads on WHPX can reset per-vCPU MSRs.
write_cr3(read_cr3())(used byvmm_flush_tlb_all) triggers a VMEXIT on WHPX that can reset the PAT MSR (and potentially other per-vCPU MSRs) to Intel defaults. Any MSR that must persist across TLB flushes must be re-programmed after CR3 reloads. Discovered 2026-04-13:cpu_configure_pat()wrote WC to PAT entry 1, thenvmm_promote_to_1g()flushed TLB via CR3 reload, and PAT readback showed WT (Intel default). Fix: re-program PAT after page table modifications that flush TLB. - User-mode ELF range (0x800000--0x900000). Constants defined in
include/kernel/mm/user_range.h(single source of truth). Three C files include it:vmm.c,pmm.c,task.c.user/user.ldmust be updated manually if the base changes. Static asserts, runtime PMM bitmap verify, guard page at 0x900000, and unit test enforce sync.
SMP Startup
- No Init Level De-Assert IPI. The broadcast Init Level De-Assert (ICR: INIT | ALL | LEVEL_DEASSERT) was deprecated since Intel P6 (1995) and is a hardware no-op on all x86-64 CPUs. On WHPX with 2+ vCPUs it hangs because the hypervisor traps the broadcast and stalls waiting for the not-yet-booted AP. Removed entirely 2026-04-01. The per-AP INIT->de-assert->SIPI sequence in
lapic_send_init()is unrelated and required.
Devices
- NVMe I/O unreliable on QEMU WHPX. QEMU's emulated NVMe controller processes doorbell MMIO writes asynchronously through its event loop under WHPX. The vCPU polls the CQ at native speed before the main thread processes the command, causing intermittent admin and I/O timeouts. Not a driver bug -- the NVMe driver has correct barriers (
wmb/rmb). Real NVMe hardware handles PCIe DMA synchronously with cache snooping. (This line previously also credited the driver withclflush, which it has never contained: until the cross-boot durability work below there was noclflushanywhere in the tree, and the only cache maintenance today lives insrc/kernel/cache.cand its cross-boot evidence callers. A doc claim that a subsystem performs cache maintenance is the kind that survives precisely because a repo-wide grep for the instruction now returns hits somewhere else.) NVMe test uses TCG; normal boot (SATA) uses WHPX fine.
Cross-boot durability
- A record that must survive a reset needs a cache writeback, not a barrier.
mb()/wmb()/rmb()order VISIBILITY between CPUs and say nothing about durability: on x86 every CPU already sees a store through the coherent cache, so a modified line can sit dirty indefinitely while satisfying every fence in the tree. Intel SDM Vol. 3A Table 9-1 footnote 6 states that internal caches are invalid after power-up and RESET, and nothing in the SDM writes modified lines back first -- so a crash record published with a plain store into write-back memory and followed by a platform reset can never reach DRAM. Every emulator this repo gates on models no cache hierarchy, so QEMU, KVM and WHPX all pass the sequence whether or not the bug exists; it appears only on real silicon. Usecache_writeback_range()frominclude/kernel/cache.h. Found 2026-08-15 on the cross-boot evidence page at0x80000and the klog crash region, both of which had exactly this defect. - Publish a cross-boot record by storing its publication word LAST, not by ordering flushes. Durably un-publish, write and flush the whole body while the publication word reads zero, then store and flush that word. Ordering flushes behind an already-stored publication word (body lines first, publication line last) looks equivalent and is not: it holds only where CLFLUSH gives line granularity and collapses on the WBINVD fallback, which commits many lines with no ordering boundary and can land a valid magic and CRC over a still-stale body.
- Durably clearing a valid record requires an owner. The un-publish flush is safe only where something arbitrates who may replace the record, because a writer that dies between the clear and the republish has destroyed the old record and written nothing. The panic evidence page has that arbitration and does flush its un-publish; the klog crash region has none and deliberately does not, letting the body flush carry the clear instead. Both fail closed on CRC; only one of them can also lose a complete record to a nested panic.
- CLFLUSH is CPUID-gated and its fencing is vendor-dependent. Gate on CPUID.01H:EDX[19] and take the line size from CPUID.01H:EBX[15:8] (in 8-byte units), on the EXECUTING CPU -- a cached BSP-built feature table is the wrong shape for a panic path that can run on an AP or before enumeration. Intel documents CLFLUSH as ordered against prior writes; AMD documents it as ordered by MFENCE and explicitly NOT by SFENCE, so bracket a batch with MFENCE rather than SFENCE.
- Never bound a line-flush loop by an end address.
p += linewraps to 0 at the top of the address space and ap < endloop then restarts from the bottom and never terminates. The kernel image window ends atUINT64_MAX, so this is reachable. Count the lines instead, and decide "does this range wrap" from the LAST BYTE, never the exclusive end -- a range ending exactly atUINT64_MAXhas an exclusive end of 0 and an end-based test misreads it as a wrap.
FPU / SIMD
- FXSAVE/XSAVE buffer: set BOTH FCW and MXCSR defaults.
task_alloc_xsave()zeroes the buffer, then must set FCW at offset 0 to0x037F(all x87 exceptions masked, Intel SDM reset value) and MXCSR at offset 24 to0x1F80(all SIMD exceptions masked). Zeroed FCW causes #MF (vector 16) on the first imprecise x87 FP op; zeroed MXCSR causes #XM (vector 19) on the first SSE instruction. On WHPX (has XSAVE), XRSTOR init optimization masked the FCW=0 bug. On TCG (uses FXRSTOR), FCW=0 was loaded directly, freezing boot at the font renderer (2026-04-13). MXCSR=0 caused WHPX BSOD the same day. - CR0.TS must be cleared before ALL FPU instructions. XSAVE, XRSTOR, FXSAVE, and FXRSTOR all fault with #NM when CR0.TS=1 -- not just restores. Both the preemptive schedule() and cooperative schedule_now() must CLTS before any save or restore. Missing CLTS before XRSTOR caused the original WHPX freeze (2026-04-13).
- Never compile SSE2 fallback code with
-mavx2. The compiler emits VEX-encoded instructions (vmovdquinstead ofmovdqu) for all SSE operations when-mavx2is active, even around inline asm and in scalar tails. On CPUs without AVX (TCGqemu64, pre-Sandy Bridge bare metal), VEX instructions cause #UD. Split SIMD files: AVX2 functions in a-mavx2translation unit, SSE2 fallbacks and dispatch in a-msse2translation unit. This crashed TCG boot whenmemops_sse.cwas compiled as part ofmemops.cwith-mavx2(2026-04-13).
MSR Probe
msr_try_read()is a no-crash guarantee, not an existence check. WHPX silently absorbs reads of unknown MSRs (returns 0, no #GP), somsr_try_read()returning 0 does NOT prove the MSR exists. Always gate oncpu_has()or CPUID first; usemsr_try_read()only as a secondary safety net. Pattern:if (!cpu_has(FEATURE)) return; if (msr_try_read(MSR, &val) != 0) { fallback; }. Discovered 2026-04-13 whenmsr_try_read(0xFFFFFFFF)returned success on WHPX.msr_try_read()requires the kernel IDT to be loaded. The probe installs a custom #GP handler in a C-levelhandlers[]table consulted only by our ISR stubs, which only dispatch afteridt_init()haslidt-ed the kernel IDT. Before that, IDTR still points at the UEFI IDT, and any real #GP (KVM trapping IA32_MPERF, real hardware trapping an unavailable MSR) is caught by UEFI's handler and halts the system.msr_try_read()now gates onidt_is_loaded()and returns-1pre-IDT; callers must have a fallback path.simd_enable_avx512()was moved from boot_phase0 to boot_phase1 (afteridt_init()) so its throttle guard keeps working on bare metal. Discovered 2026-04-17 when the KVM smoke test crashed at MPERF probe in phase 0. Under virtualization CPUID lies (KVM with-cpu hostpasses AVX512F through but traps MPERF); always pair CPUID gate withmsr_try_read()fallback.
Init Ordering
- No thread_create for deferred init.
boot_run_deferred()MUST run inline in Phase 3, not on a background thread.compositor_run()is an infinite event loop on the BSP that starves any kernel thread created just before it. Moving deferred inits to a thread caused VirtIO input and VBox mouse drivers to never initialize, breaking absolute cursor positioning on QEMU/VBox (2026-04-05). Per-thread fault isolation requires SEH (TODO-10).
Disk-Sourced Config
- Disk-sourced config files get dynamic buffers + hard-fail overflow.
resources/boot/boot.confwas originally read into a 4096-byte stack buffer with a silent[WARN] truncatingon overflow. On 2026-04-21 doc-comment growth pushed the file to 4.5 KiB; the truncation dropped the patch-appendedtest=1line at EOF and everytest=1boot ran zero tests without any visible error. Fixed by switchingbootx64.c boot_conf_load()togBS->AllocatePool(FileSize + 1)via aGetInfo(&EFI_FILE_INFO_ID, ...)size probe, with a 1 MiB sanity cap thatboot_fatal()s on hit (never[WARN]+ continue). Codified inboot-code-qualityGate 14. Any future parser reading disk/user input should follow the same pattern: dynamic-size first, sanity cap second, silent truncation never.
Boot Handoff Trust
- A producer-set
BOOT_PAYLOAD_FLAG_RESERVEDbit is NOT proof the reservation pass pinned the range. The Phase-0 pass now declines to pin a payload whose declaredlengthfalls outside its type's contract (boot_seed_length_reservable()forBOOT_PAYLOAD_RANDOM_SEED,src/kernel/mm/boot_reserved.c), because the alternative is worse: reserving an attacker-declared gigabyte span permanently starves the PMM and the boot dies inheap_initrather than degrading. The flag stays SET on the descriptor it skipped, so any consumer that readsFLAG_RESERVEDas "the PMM owns this" dereferences memory nothing reserved. Every consumer of a bounded type must apply the SAME predicate the pass applied. Shipped 2026-08-29 with01-boot-platform/TODO-12section 11, and the follow-on[high]was found in the very next review round:canary_seed_desc_ok()still used its ownlength >= 16and had to be routed through the shared predicate. The remaining eight payload types are still unbounded (01-boot-platform/TODO-01section 28). canary_init()dereferences a loader-published range BEFORE the IDT exists. It runs fromkernel_mainbetween Phase 0 and Phase 1, afterpmm/vmm/heapinit and after the loader-owned boot stack's guard page is live, but with no interrupt handlers installed. A fault there is a triple fault with no diagnostic, so the pre-IDT peek must refuse anything it cannot prove is pinned rather than trusting a bound check of its own. No emulator produces a malformed descriptor, so this whole class is invisible under QEMU, WHPX and VirtualBox alike -- OVMF always hands over a well-formed one.
Uninitialised .bss in the Loader
- UEFI does not zero a loader image's
.bss, so an early static reads FIRMWARE POISON, not 0. Edk2-derived firmware fills freed pool memory with0xAFbytes, and BOOTX64's.bssis ordinary pool memory the firmware never scrubs; on a warm reboot it can instead hold whatever the previous boot left at that address. So a static consulted before its initialiser has run holds0xAFAFAFAFor last boot's value. This is invisible under every emulator: OVMF under QEMU happens to hand over zeroed pages, and the class only appears on real firmware. Two live defects came from it, both shipped 2026-09-03 with01-boot-platform/TODO-14sections 18 and 20:serial_early_printdrove port0xAFAFas if it were a configured UART, andboot_entries_parser.cskipped its CRC table build, computed every CRC over a poisoned table, and rejected a VALID boot store asCRC_MISMATCH-- sending the loader to its invalid-store fallback, which can select a different kernel than the store asked for. - Reset first; the cookie is defence in depth, and a BOOLEAN buys nothing.
early_diag_reset()runs as the first statement ofefi_main, ahead of any code that can emit a diagnostic (src/boot/uefi/bootx64.c:505), and the parser resets at its own entry -- that reset is the load-bearing half, because a constant cookie discriminates poison from ready and can never discriminate THIS boot from the last one. Readiness is then a wide exact-match cookie (EARLY_DIAG_READYatbootx64.c:422,BOOT_ENTRIES_CRC32_READYatboot_entries_parser.c:59), tested with!=, never with!x:0xAFis non-zero and reads as "configured", so a boolean flag is exactly the wrong shape. A path that somehow runs ahead of the reset then degrades to silence instead of to port I/O on an address the poison spells. - The cookie must be
volatileor the compiler deletes it. These objects have internal linkage and never have their address taken, so clang may assume nothing outside the program writes them and narrow the representation to the single bit it can prove is needed. Measured 2026-09-03 on the real-O2build objects: the non-volatilestatic int g_crc32_readywas emitted at SIZE 1, and both bootx64 cookies became 1-byte objects withcmpb $1consumers, the 32-bit constant nowhere in the image -- silently reducing the wide compare back to the boolean it exists to avoid. That narrowing is sound only under the assumption firmware poison violates.volatilestates the truth and keeps the wide compare. A plain= 0initialiser is not a third option: it is documentation, since.bssis what the initialiser nominally targets.