Skip to content

fix(windows): stabilize WHPX and reproducible firmware builds#4

Draft
ZhiXiao-Lin wants to merge 31 commits into
mainfrom
fix/windows-whpx-main-20260721
Draft

fix(windows): stabilize WHPX and reproducible firmware builds#4
ZhiXiao-Lin wants to merge 31 commits into
mainfrom
fix/windows-whpx-main-20260721

Conversation

@ZhiXiao-Lin

Copy link
Copy Markdown

What changed

  • stabilizes native Windows WHPX boot, virtio, vsock, and guest logging paths
  • persists and restores the VM/device state required by snapshot forks
  • makes Windows firmware/kernel bundle builds reproducible and validates the archived corresponding source
  • adds focused Windows tests and build documentation

Why

The Windows backend had several coupled portability and lifecycle failures: incomplete WHPX device behavior, snapshot state that omitted live virtio/kernel data, path-dependent build metadata, and firmware artifacts without a strict source-validation chain. Together these caused flaky or non-restorable guests and made release provenance difficult to verify.

Impact

The Box runtime can use this branch as a fetchable libkrun revision for reliable WHPX workloads and snapshot operations. Release builds also carry deterministic binaries and corresponding-source evidence.

Validation

  • native Windows WHPX guest regression suite (53 scenarios)
  • Windows firmware/kernel bundle build and archive validation
  • focused virtio/vsock/snapshot unit coverage

RoyLin and others added 29 commits June 12, 2026 15:29
Phase A-1 of native snapshot-fork. When KRUN_SNAPSHOT_MEM_FILE is set (unix,
non-TEE), guest RAM regions are backed by that file with MAP_SHARED (vm-memory
from_ranges_with_files → MAP_NORESERVE|MAP_SHARED), so the file always holds
live RAM: snapshot = pause + msync; restore will MAP_PRIVATE the same file for
page-level CoW. Backing file + region layout registered in the new vmm
snapshot module for the later snapshot step.
Phase A-2. Vmm::pause_vcpus (wires the existing Pause event) and
Vmm::snapshot_to: collect per-vCPU KVM state via a new VcpuEvent::SaveState
(answered from the paused vcpu thread through a channel — only the vcpu thread
may touch its VcpuFd), save VM state (existing PIT/clock/irqchip code), fsync
the MAP_SHARED RAM file, and write a bincode state file (vm+vcpu states + RAM
layout; kvm-bindings serde feature). krun_start_enter spawns a listener on
KRUN_SNAPSHOT_SOCK serving 'snapshot <state_path>' (linux x86_64, non-TEE).
vstate running() now ignores SaveState (only valid when paused). CpuId/Msrs are
FamStructWrapper which kvm-bindings serde does not cover; add #[serde(with)]
modules serializing their entry slices (entries derive Serialize) and rebuilding
via from_entries on restore.
…e (Phase A-3)

Restore mode triggers when KRUN_RESTORE_FROM (state file) + KRUN_SNAPSHOT_MEM_FILE
(RAM file) are both set:
- create_guest_memory_regions maps each region MAP_PRIVATE from the snapshot RAM
  file at the saved layout — kernel page-level CoW (this is the fork)
- create_guest_memory skips load_payload + firmware write (kernel already in RAM)
- build_microvm skips load_cmdline + configure_system and instead restores the
  saved Vm + per-vCPU KVM state before start_vcpus
- Vcpu::restore_state made pub for the builder
…riant

- vm_memory::mmap::MmapRegionBuilder (not re-exported at crate root)
- add StartMicrovmError::RestoreState(VstateError) for Vm/Vcpu restore_state
  (returns vstate::Error, not vmm::Error — Internal didn't fit)
Phase A (RAM+CPU snapshot/restore) is plumbing-complete and CPU-state byte-perfect.
Root-caused the restore failure to lost virtio device-ring state (host devices
reset to index 0 while guest RAM holds advanced indices -> deterministic wild jump,
KVM INTERNAL_ERROR DELIVERY_EV #PF). Documents the exact Phase B save/restore surface
(MmioTransport device_status/acked_features + per-Queue state + re-activation) and the
path to the 20ms/100-VM target.
…se B foundation)

Plain-data snapshot of a Queue's driver-programmed ring state (indices, ring addrs,
ready, event_idx) with Queue::save_state/restore_state, so a freshly-built device can
resume at the guest's indices. First building block of Phase B device-state restore.
…ate (Phase B)

Per-device snapshot of transport regs (device_status/features/queue_select) + acked
features + per-queue state, with restore that re-applies and re-activates the device
(mirrors the DRIVER_OK activation path) so a fresh host device resumes at the guest's
ring indices. Next: enumerate devices in the device manager and wire save/restore into
the snapshot state + build_microvm restore branch.
…estore (Phase B)

- MMIODeviceManager::save_virtio_states/restore_virtio_states (enumerate bus devices,
  downcast to MmioTransport)
- SnapshotState gains device_states; serde mirrors (QueueStateSer/MmioDeviceStateSer)
- snapshot_to collects device states; build_microvm restore re-applies + re-activates
  them before start_vcpus
- new StartMicrovmError::RestoreDeviceState + mmio Error variants
…captures it

ROOT CAUSE of the deterministic 0x24470e0 restore fault (found via guest page-table
walk on ram.img + source analysis): the kernel image region [kernel_guest_addr,
+kernel_size) sits in the hole arch_memory_regions punches out of the file-backed
RAM, and Payload::KernelMmap inserts it as a raw mmap over libkrunfw host pages AFTER
create_guest_memory_regions. The snapshot only file-backs arch_mem_regions, so the
kernel region — which holds the guest's live early page tables, IDT/GDT and kernel
.data/.bss — was never captured. On restore that hole had no KVM slot, so the first
interrupt (CPU reads IDT at gpa 0x2447000, inside the hole) faulted with
KVM_INTERNAL_ERROR DELIVERY_EV at IDT+#PF-slot = 0x24470e0.

Fix: in snapshot mode, Payload::KernelMmap copies the kernel into the snapshot RAM
file, maps it MAP_SHARED (capturing the guest's runtime writes), and appends it to
MEM_BACKING.layout. Restore re-maps it MAP_PRIVATE via the existing mem_layout loop —
no restore-side change needed.
…nded last)

from_regions requires address-sorted regions; the file-backed kernel region is
pushed to mem_layout last (gpa 0x1000000 after 0x100000000), so sort before building
GuestMemoryMmap. Fixes UnsortedMemoryRegions on restore.
…lf.queues

ROOT CAUSE of exec-into-restored-guest hang (found via save-time index dump): vsock
clones rx/tx queues into the muxer thread at activate(); the muxer advances ITS copies
(reflected in guest RAM: avail_idx=256 used_idx=7) while self.queues stays frozen at
activate time (0,0). save_state read self.queues, so the snapshot captured next_used=0
while the guest had last-seen used_idx=7. On restore the muxer re-emitted used entries
from index 0 → the guest saw the used ring jump backwards and ignored every host
OP_REQUEST → no OP_RESPONSE → exec timed out.

Fix: add VirtioDevice::save_queue_states() (default = self.queues) and override it in
vsock to read the muxer's live queue_rx/queue_tx under their mutex. MmioTransport::
save_state now uses it. Restore is unchanged — it sets self.queues then activate()
re-clones them into the muxer, so the correct indices flow through. Includes the
VSOCK_RESTORE_DIAGNOSIS.md from the diagnostic workflow.
… all devices

virtio-fs (like vsock) hands a CLONED queue to its worker thread, so save_state read
stale self.queues indices (next_used=0 vs guest RAM used_idx=162) — the restored fs
worker then read the ring at the wrong position and failed every FUSE message with
DecodeMessage(UnexpectedEof). Generalize the vsock fix: in MmioTransport::save_state,
reconcile each queue's next_used/next_avail up to the guest-RAM used_ring.idx, which is
authoritative (the device writes it on every completion and it can never lag). Catches
every worker-thread device without per-device handling.
… on restored guest)

ROOT CAUSE (workflow-confirmed via FUSE errno trace 'do_getattr MAP-MISS inode=1 ->
EBADF, map has 0 inodes'): the host virtio-fs PassthroughFs is rebuilt fresh on
restore with an EMPTY inode map. The root inode (nodeid=1) is only inserted by the
FUSE_INIT handler, which the restored (already-mounted) guest never re-sends — so every
rootfs op the guest makes (execve reading /bin/sh) resolves a cached nodeid the host
has forgotten and returns EBADF, surfacing as 'Failed to spawn command sh: Bad file
descriptor'.

Fix: serialize the inode map (nodeid -> host path, recovered via readlink(/proc/self/fd/N)
on the live O_PATH fds while the template is paused-but-alive) and rehydrate it on
restore by reopening each path under its original nodeid. Plumbing: VirtioDevice gains
save_device_state/restore_device_state (opaque blob); Fs shares its PassthroughFs with
the worker via Arc (Server now holds Arc<F>) so the device can snapshot it; the blob
rides MmioDeviceState through the snapshot state and is reapplied after activate()
before vCPUs run. Open file handles deferred (a fresh exec opens its own). macOS/Windows
stubs no-op.
…n on restore

The template and each fork mount their rootfs at different host overlay dirs, and the
template's dir is removed before restore — so absolute saved paths failed to reopen (0
inodes restored). Store each path relative to the root inode's canonical path; on
restore rejoin against this fork's root_dir.
VcpuState (KVM x86 vcpu state) and Vcpu::save_state/restore_state are already
#[cfg(target_arch = "x86_64")], but the VcpuEvent::SaveState variant that
carries Box<VcpuState> and the running()-state match arm that referenced it
were not — so the vmm failed to compile on aarch64 (E0425: cannot find type
VcpuState). Gate both to x86_64. Snapshot-fork is x86_64-only; aarch64 builds
cold-boot only. Verified: cargo check --target aarch64-unknown-linux-gnu -p vmm
now passes.
…a-20260722

fix(windows): preserve virtiofs POSIX metadata
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant