//! Snapshot/Restore for Volt VMM //! //! Provides serializable state types and functions to create and restore //! VM snapshots. The snapshot format consists of: //! //! - `state.json`: Serialized VM state (vCPU registers, IRQ chip, devices, metadata) //! - `memory.snap`: Raw guest memory dump (mmap'd on restore for lazy loading) //! //! # Architecture //! //! ```text //! ┌──────────────────────────────────────────────────┐ //! │ Snapshot Files │ //! │ ┌──────────────────┐ ┌───────────────────────┐ │ //! │ │ state.json │ │ memory.snap │ │ //! │ │ - VcpuState[] │ │ (raw memory dump) │ │ //! │ │ - IrqchipState │ │ │ │ //! │ │ - ClockState │ │ Restored via mmap │ │ //! │ │ - DeviceState │ │ MAP_PRIVATE for CoW │ │ //! │ │ - Metadata+CRC │ │ demand-paged by OS │ │ //! │ └──────────────────┘ └───────────────────────┘ │ //! └──────────────────────────────────────────────────┘ //! ``` pub mod cas; pub mod create; pub mod inmem; pub mod restore; // Re-export CAS types pub use cas::{CasManifest, CasChunk, CasDumpResult, CAS_CHUNK_SIZE, CAS_MANIFEST_FILENAME}; // Re-export restore types pub use restore::MemoryMapping; use serde::{Deserialize, Serialize}; // ============================================================================ // Snapshot Metadata // ============================================================================ /// Snapshot format version pub const SNAPSHOT_VERSION: u32 = 1; /// Snapshot metadata with integrity check #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SnapshotMetadata { /// Snapshot format version pub version: u32, /// Total guest memory size in bytes pub memory_size: u64, /// Number of vCPUs pub vcpu_count: u8, /// Snapshot creation timestamp (Unix epoch seconds) pub created_at: u64, /// CRC-64 of the state JSON (excluding this field) pub state_crc64: u64, /// Memory file size (for validation) pub memory_file_size: u64, } // ============================================================================ // vCPU State // ============================================================================ /// Complete vCPU state captured from KVM #[derive(Debug, Clone, Serialize, Deserialize)] pub struct VcpuState { /// vCPU index pub id: u8, /// General purpose registers (KVM_GET_REGS) pub regs: SerializableRegs, /// Special registers (KVM_GET_SREGS) pub sregs: SerializableSregs, /// FPU state (KVM_GET_FPU) pub fpu: SerializableFpu, /// Model-specific registers (KVM_GET_MSRS) pub msrs: Vec, /// CPUID entries (KVM_GET_CPUID2) pub cpuid_entries: Vec, /// Local APIC state (KVM_GET_LAPIC) pub lapic: SerializableLapic, /// Extended control registers (KVM_GET_XCRS) pub xcrs: Vec, /// Multiprocessor state (KVM_GET_MP_STATE) pub mp_state: u32, /// vCPU events (KVM_GET_VCPU_EVENTS) pub events: SerializableVcpuEvents, } /// Serializable general-purpose registers (maps to kvm_regs) #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SerializableRegs { pub rax: u64, pub rbx: u64, pub rcx: u64, pub rdx: u64, pub rsi: u64, pub rdi: u64, pub rsp: u64, pub rbp: u64, pub r8: u64, pub r9: u64, pub r10: u64, pub r11: u64, pub r12: u64, pub r13: u64, pub r14: u64, pub r15: u64, pub rip: u64, pub rflags: u64, } /// Serializable segment register #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SerializableSegment { pub base: u64, pub limit: u32, pub selector: u16, pub type_: u8, pub present: u8, pub dpl: u8, pub db: u8, pub s: u8, pub l: u8, pub g: u8, pub avl: u8, pub unusable: u8, } /// Serializable descriptor table register #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SerializableDtable { pub base: u64, pub limit: u16, } /// Serializable special registers (maps to kvm_sregs) #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SerializableSregs { pub cs: SerializableSegment, pub ds: SerializableSegment, pub es: SerializableSegment, pub fs: SerializableSegment, pub gs: SerializableSegment, pub ss: SerializableSegment, pub tr: SerializableSegment, pub ldt: SerializableSegment, pub gdt: SerializableDtable, pub idt: SerializableDtable, pub cr0: u64, pub cr2: u64, pub cr3: u64, pub cr4: u64, pub cr8: u64, pub efer: u64, pub apic_base: u64, /// Interrupt bitmap (256 bits = 4 x u64) pub interrupt_bitmap: [u64; 4], } /// Serializable FPU state (maps to kvm_fpu) #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SerializableFpu { /// x87 FPU registers (8 x 16 bytes = 128 bytes) pub fpr: Vec>, /// FPU control word pub fcw: u16, /// FPU status word pub fsw: u16, /// FPU tag word (abridged) pub ftwx: u8, /// Last FPU opcode pub last_opcode: u16, /// Last FPU instruction pointer pub last_ip: u64, /// Last FPU data pointer pub last_dp: u64, /// SSE/AVX registers (16 x 16 bytes = 256 bytes) pub xmm: Vec>, /// SSE control/status register pub mxcsr: u32, } /// Serializable MSR entry #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SerializableMsr { pub index: u32, pub data: u64, } /// Serializable CPUID entry #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SerializableCpuidEntry { pub function: u32, pub index: u32, pub flags: u32, pub eax: u32, pub ebx: u32, pub ecx: u32, pub edx: u32, } /// Serializable LAPIC state (256 x 4 = 1024 bytes, base64-encoded) #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SerializableLapic { /// Raw LAPIC register data (1024 bytes) pub regs: Vec, } /// Serializable XCR entry #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SerializableXcr { pub xcr: u32, pub value: u64, } /// Serializable vCPU events (maps to kvm_vcpu_events) #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SerializableVcpuEvents { // Exception state pub exception_injected: u8, pub exception_nr: u8, pub exception_has_error_code: u8, pub exception_error_code: u32, // Interrupt state pub interrupt_injected: u8, pub interrupt_nr: u8, pub interrupt_soft: u8, pub interrupt_shadow: u8, // NMI state pub nmi_injected: u8, pub nmi_pending: u8, pub nmi_masked: u8, // SMI state pub smi_smm: u8, pub smi_pending: u8, pub smi_smm_inside_nmi: u8, pub smi_latched_init: u8, // Flags pub flags: u32, } // ============================================================================ // IRQ Chip State // ============================================================================ /// Complete IRQ chip state (PIC + IOAPIC + PIT) #[derive(Debug, Clone, Serialize, Deserialize)] pub struct IrqchipState { /// 8259 PIC master (IRQ chip 0) pub pic_master: SerializablePicState, /// 8259 PIC slave (IRQ chip 1) pub pic_slave: SerializablePicState, /// IOAPIC state (IRQ chip 2) pub ioapic: SerializableIoapicState, /// PIT state (KVM_GET_PIT2) pub pit: SerializablePitState, } /// Serializable 8259 PIC state #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SerializablePicState { /// Raw chip data from KVM_GET_IRQCHIP (512 bytes) pub raw_data: Vec, } /// Serializable IOAPIC state #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SerializableIoapicState { /// Raw chip data from KVM_GET_IRQCHIP (512 bytes) pub raw_data: Vec, } /// Serializable PIT state #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SerializablePitState { /// PIT counter channels (3 channels) pub channels: Vec, /// PIT flags pub flags: u32, } /// Serializable PIT channel state #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SerializablePitChannel { pub count: u32, pub latched_count: u16, pub count_latched: u8, pub status_latched: u8, pub status: u8, pub read_state: u8, pub write_state: u8, pub write_latch: u8, pub rw_mode: u8, pub mode: u8, pub bcd: u8, pub gate: u8, pub count_load_time: i64, } // ============================================================================ // Clock State // ============================================================================ /// KVM clock state #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ClockState { /// KVM clock value (nanoseconds) pub clock: u64, /// Flags from kvm_clock_data pub flags: u32, } // ============================================================================ // Device State // ============================================================================ /// Combined device state for all emulated devices #[derive(Debug, Clone, Serialize, Deserialize)] pub struct DeviceState { /// Serial console state pub serial: SerializableSerialState, /// Virtio-blk device state (if present) pub virtio_blk: Option, /// Virtio-net device state (if present) pub virtio_net: Option, /// MMIO transport state for each device pub mmio_transports: Vec, } /// Serializable serial console state #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SerializableSerialState { pub dlab: bool, pub ier: u8, pub lcr: u8, pub mcr: u8, pub lsr: u8, pub msr: u8, pub scr: u8, pub dll: u8, pub dlh: u8, pub thr_interrupt_pending: bool, pub input_buffer: Vec, } /// Serializable virtio-blk queue state #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SerializableVirtioBlkState { /// Features acknowledged by the driver pub acked_features: u64, /// Whether the device is activated pub activated: bool, /// Queue state pub queues: Vec, /// Read-only flag pub read_only: bool, /// Backend path (for re-opening on restore) pub backend_path: Option, } /// Serializable virtio-net queue state #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SerializableVirtioNetState { /// Features acknowledged by the driver pub acked_features: u64, /// Whether the device is activated pub activated: bool, /// Queue state pub queues: Vec, /// MAC address pub mac: [u8; 6], /// TAP device name pub tap_name: String, } /// Serializable virtqueue state #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SerializableQueueState { pub max_size: u16, pub size: u16, pub ready: bool, pub desc_table: u64, pub avail_ring: u64, pub used_ring: u64, pub next_avail: u16, pub next_used: u16, } /// Serializable MMIO transport state #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SerializableMmioTransportState { /// Device type pub device_type: u32, /// Current device status register pub device_status: u32, /// Driver features pub driver_features: u64, /// Device features selector pub device_features_sel: u32, /// Driver features selector pub driver_features_sel: u32, /// Selected queue index pub queue_sel: u32, /// Interrupt status pub interrupt_status: u32, /// Configuration generation counter pub config_generation: u32, /// MMIO base address pub base_addr: u64, /// IRQ number pub irq: u32, /// Per-queue addresses pub queue_desc: Vec, pub queue_avail: Vec, pub queue_used: Vec, pub queue_num: Vec, pub queue_ready: Vec, } // ============================================================================ // Complete Snapshot // ============================================================================ /// Complete VM snapshot (serialized to state.json) #[derive(Debug, Clone, Serialize, Deserialize)] pub struct VmSnapshot { /// Snapshot metadata pub metadata: SnapshotMetadata, /// Per-vCPU state pub vcpu_states: Vec, /// IRQ chip state pub irqchip: IrqchipState, /// KVM clock state pub clock: ClockState, /// Device state pub devices: DeviceState, /// Memory region layout pub memory_regions: Vec, } /// Serializable memory region descriptor #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SerializableMemoryRegion { /// Guest physical address pub guest_addr: u64, /// Size in bytes pub size: u64, /// Offset into the memory snapshot file pub file_offset: u64, } // ============================================================================ // VmSnapshot Implementation // ============================================================================ impl VmSnapshot { /// Deserialize a VmSnapshot from a byte buffer. /// /// This allows loading snapshot state from memory (e.g., CAS blob cache) /// instead of reading from a file on disk. /// /// # Arguments /// /// * `data` - JSON-encoded snapshot state bytes /// /// # Returns /// /// The deserialized `VmSnapshot`, or an error if deserialization fails. /// /// # Example /// /// ```ignore /// // Load state from CAS blob cache /// let state_bytes = blob_cache.get("vm-snapshot-state")?; /// let snapshot = VmSnapshot::from_bytes(&state_bytes)?; /// ``` pub fn from_bytes(data: &[u8]) -> Result { let snapshot: VmSnapshot = serde_json::from_slice(data)?; // Verify version if snapshot.metadata.version != SNAPSHOT_VERSION { return Err(SnapshotError::VersionMismatch { expected: SNAPSHOT_VERSION, actual: snapshot.metadata.version, }); } // Verify CRC-64 let saved_crc = snapshot.metadata.state_crc64; let mut check_snapshot = snapshot.clone(); check_snapshot.metadata.state_crc64 = 0; let check_json = serde_json::to_string_pretty(&check_snapshot)?; let computed_crc = compute_crc64(check_json.as_bytes()); if saved_crc != computed_crc { return Err(SnapshotError::CrcMismatch { expected: saved_crc, actual: computed_crc, }); } Ok(snapshot) } /// Serialize the VmSnapshot to bytes. /// /// This is the inverse of `from_bytes()` and allows storing snapshot /// state in memory (e.g., for CAS blob cache). /// /// # Returns /// /// The JSON-encoded snapshot state as bytes. pub fn to_bytes(&self) -> Result> { // Create a snapshot with zeroed CRC for computation let mut snapshot = self.clone(); snapshot.metadata.state_crc64 = 0; let json = serde_json::to_string_pretty(&snapshot)?; // Compute CRC and update let crc = compute_crc64(json.as_bytes()); snapshot.metadata.state_crc64 = crc; // Re-serialize with correct CRC let final_json = serde_json::to_string_pretty(&snapshot)?; Ok(final_json.into_bytes()) } } // ============================================================================ // Error types // ============================================================================ /// Snapshot operation errors #[derive(Debug, thiserror::Error)] pub enum SnapshotError { #[error("IO error: {0}")] Io(#[from] std::io::Error), #[error("Serialization error: {0}")] Serialization(#[from] serde_json::Error), #[error("KVM error: {0}")] Kvm(String), #[error("CRC mismatch: expected {expected:#x}, got {actual:#x}")] CrcMismatch { expected: u64, actual: u64 }, #[error("Version mismatch: expected {expected}, got {actual}")] VersionMismatch { expected: u32, actual: u32 }, #[error("Memory size mismatch: expected {expected}, got {actual}")] MemorySizeMismatch { expected: u64, actual: u64 }, #[error("Memory file size mismatch: expected {expected}, got {actual}")] MemoryFileSizeMismatch { expected: u64, actual: u64 }, #[error("Missing snapshot file: {0}")] MissingFile(String), #[error("Invalid snapshot: {0}")] Invalid(String), #[error("mmap failed: {0}")] Mmap(String), } pub type Result = std::result::Result; // ============================================================================ // CRC-64 helper // ============================================================================ /// Compute CRC-64/ECMA for integrity checking pub fn compute_crc64(data: &[u8]) -> u64 { use crc::{Crc, CRC_64_ECMA_182}; const CRC64: Crc = Crc::::new(&CRC_64_ECMA_182); CRC64.checksum(data) } #[cfg(test)] mod tests { use super::*; /// Create a minimal valid snapshot for testing fn create_test_snapshot() -> VmSnapshot { VmSnapshot { metadata: SnapshotMetadata { version: SNAPSHOT_VERSION, memory_size: 128 * 1024 * 1024, vcpu_count: 1, created_at: 1234567890, state_crc64: 0, // Will be computed memory_file_size: 128 * 1024 * 1024, }, vcpu_states: vec![VcpuState { id: 0, regs: SerializableRegs { rax: 0, rbx: 0, rcx: 0, rdx: 0, rsi: 0, rdi: 0, rsp: 0x7fff_0000, rbp: 0, r8: 0, r9: 0, r10: 0, r11: 0, r12: 0, r13: 0, r14: 0, r15: 0, rip: 0x0010_0000, rflags: 0x0002, }, sregs: SerializableSregs { cs: SerializableSegment { base: 0, limit: 0xffff_ffff, selector: 0x10, type_: 11, present: 1, dpl: 0, db: 0, s: 1, l: 1, g: 1, avl: 0, unusable: 0, }, ds: SerializableSegment { base: 0, limit: 0xffff_ffff, selector: 0x18, type_: 3, present: 1, dpl: 0, db: 1, s: 1, l: 0, g: 1, avl: 0, unusable: 0, }, es: SerializableSegment { base: 0, limit: 0xffff_ffff, selector: 0x18, type_: 3, present: 1, dpl: 0, db: 1, s: 1, l: 0, g: 1, avl: 0, unusable: 0, }, fs: SerializableSegment { base: 0, limit: 0xffff_ffff, selector: 0x18, type_: 3, present: 1, dpl: 0, db: 1, s: 1, l: 0, g: 1, avl: 0, unusable: 0, }, gs: SerializableSegment { base: 0, limit: 0xffff_ffff, selector: 0x18, type_: 3, present: 1, dpl: 0, db: 1, s: 1, l: 0, g: 1, avl: 0, unusable: 0, }, ss: SerializableSegment { base: 0, limit: 0xffff_ffff, selector: 0x18, type_: 3, present: 1, dpl: 0, db: 1, s: 1, l: 0, g: 1, avl: 0, unusable: 0, }, tr: SerializableSegment { base: 0, limit: 0, selector: 0, type_: 11, present: 1, dpl: 0, db: 0, s: 0, l: 0, g: 0, avl: 0, unusable: 0, }, ldt: SerializableSegment { base: 0, limit: 0, selector: 0, type_: 2, present: 1, dpl: 0, db: 0, s: 0, l: 0, g: 0, avl: 0, unusable: 1, }, gdt: SerializableDtable { base: 0, limit: 0 }, idt: SerializableDtable { base: 0, limit: 0 }, cr0: 0x8000_0011, cr2: 0, cr3: 0x0010_0000, cr4: 0x20, cr8: 0, efer: 0x500, apic_base: 0xfee0_0900, interrupt_bitmap: [0; 4], }, fpu: SerializableFpu { fpr: vec![vec![0u8; 16]; 8], fcw: 0x37f, fsw: 0, ftwx: 0, last_opcode: 0, last_ip: 0, last_dp: 0, xmm: vec![vec![0u8; 16]; 16], mxcsr: 0x1f80, }, msrs: vec![], cpuid_entries: vec![], lapic: SerializableLapic { regs: vec![0u8; 1024] }, xcrs: vec![], mp_state: 0, events: SerializableVcpuEvents { exception_injected: 0, exception_nr: 0, exception_has_error_code: 0, exception_error_code: 0, interrupt_injected: 0, interrupt_nr: 0, interrupt_soft: 0, interrupt_shadow: 0, nmi_injected: 0, nmi_pending: 0, nmi_masked: 0, smi_smm: 0, smi_pending: 0, smi_smm_inside_nmi: 0, smi_latched_init: 0, flags: 0, }, }], irqchip: IrqchipState { pic_master: SerializablePicState { raw_data: vec![0u8; 512] }, pic_slave: SerializablePicState { raw_data: vec![0u8; 512] }, ioapic: SerializableIoapicState { raw_data: vec![0u8; 512] }, pit: SerializablePitState { channels: vec![ SerializablePitChannel { count: 0, latched_count: 0, count_latched: 0, status_latched: 0, status: 0, read_state: 0, write_state: 0, write_latch: 0, rw_mode: 0, mode: 0, bcd: 0, gate: 0, count_load_time: 0, }; 3 ], flags: 0, }, }, clock: ClockState { clock: 1_000_000_000, flags: 0 }, devices: DeviceState { serial: SerializableSerialState { dlab: false, ier: 0, lcr: 0, mcr: 0, lsr: 0x60, msr: 0, scr: 0, dll: 0, dlh: 0, thr_interrupt_pending: false, input_buffer: vec![], }, virtio_blk: None, virtio_net: None, mmio_transports: vec![], }, memory_regions: vec![SerializableMemoryRegion { guest_addr: 0, size: 128 * 1024 * 1024, file_offset: 0, }], } } #[test] fn test_snapshot_to_bytes_from_bytes_roundtrip() { let original = create_test_snapshot(); // Serialize to bytes let bytes = original.to_bytes().expect("to_bytes should succeed"); // Deserialize from bytes let restored = VmSnapshot::from_bytes(&bytes).expect("from_bytes should succeed"); // Verify key fields match assert_eq!(original.metadata.version, restored.metadata.version); assert_eq!(original.metadata.memory_size, restored.metadata.memory_size); assert_eq!(original.metadata.vcpu_count, restored.metadata.vcpu_count); assert_eq!(original.vcpu_states.len(), restored.vcpu_states.len()); assert_eq!(original.vcpu_states[0].regs.rip, restored.vcpu_states[0].regs.rip); assert_eq!(original.vcpu_states[0].regs.rsp, restored.vcpu_states[0].regs.rsp); assert_eq!(original.clock.clock, restored.clock.clock); } #[test] fn test_snapshot_from_bytes_version_mismatch() { let mut snapshot = create_test_snapshot(); snapshot.metadata.version = 999; // Invalid version let bytes = serde_json::to_vec(&snapshot).unwrap(); let result = VmSnapshot::from_bytes(&bytes); assert!(matches!(result, Err(SnapshotError::VersionMismatch { .. }))); } #[test] fn test_snapshot_from_bytes_crc_mismatch() { let mut snapshot = create_test_snapshot(); // Serialize normally first let bytes = snapshot.to_bytes().unwrap(); // Corrupt the bytes (modify some content while keeping valid JSON) let mut json: serde_json::Value = serde_json::from_slice(&bytes).unwrap(); json["clock"]["clock"] = serde_json::json!(12345); // Change clock value let corrupted = serde_json::to_vec(&json).unwrap(); let result = VmSnapshot::from_bytes(&corrupted); assert!(matches!(result, Err(SnapshotError::CrcMismatch { .. }))); } #[test] fn test_snapshot_from_bytes_invalid_json() { let invalid_bytes = b"{ this is not valid json }"; let result = VmSnapshot::from_bytes(invalid_bytes); assert!(matches!(result, Err(SnapshotError::Serialization(_)))); } #[test] fn test_crc64_consistency() { let data1 = b"hello world"; let data2 = b"hello world"; let data3 = b"hello worle"; // Different let crc1 = compute_crc64(data1); let crc2 = compute_crc64(data2); let crc3 = compute_crc64(data3); assert_eq!(crc1, crc2); assert_ne!(crc1, crc3); } }