From 65659bda6bdd873413044eac4c775c8944eb17d0 Mon Sep 17 00:00:00 2001 From: sjmiller609 <7516283+sjmiller609@users.noreply.github.com> Date: Wed, 12 Aug 2026 00:47:21 +0000 Subject: [PATCH 1/3] Add differential snapshot support --- cloud-hypervisor/src/bin/ch-remote.rs | 1 + fuzz/fuzz_targets/http_api.rs | 4 +- vmm/src/api/mod.rs | 18 +++- vmm/src/api/openapi/cloud-hypervisor.yaml | 7 ++ vmm/src/config.rs | 23 ++++- vmm/src/lib.rs | 72 ++++++++++++++- vmm/src/memory_manager.rs | 106 ++++++++++++++++++++-- vmm/src/vm.rs | 7 ++ 8 files changed, 221 insertions(+), 17 deletions(-) diff --git a/cloud-hypervisor/src/bin/ch-remote.rs b/cloud-hypervisor/src/bin/ch-remote.rs index a6110a64bc..63f22d57bb 100644 --- a/cloud-hypervisor/src/bin/ch-remote.rs +++ b/cloud-hypervisor/src/bin/ch-remote.rs @@ -925,6 +925,7 @@ fn add_vsock_config(config: &str) -> Result { fn snapshot_config(url: &str) -> String { let snapshot_config = api::VmSnapshotConfig { destination_url: String::from(url), + snapshot_type: Default::default(), }; serde_json::to_string(&snapshot_config).unwrap() diff --git a/fuzz/fuzz_targets/http_api.rs b/fuzz/fuzz_targets/http_api.rs index 1f0551acc2..573554d65c 100644 --- a/fuzz/fuzz_targets/http_api.rs +++ b/fuzz/fuzz_targets/http_api.rs @@ -15,7 +15,7 @@ use vm_migration::MigratableError; use vmm::api::http::*; use vmm::api::{ ApiRequest, RequestHandler, VmInfoResponse, VmReceiveMigrationData, VmSendMigrationData, - VmmPingResponse, + VmSnapshotConfig, VmmPingResponse, }; use vmm::config::RestoreConfig; use vmm::vm::{Error as VmError, VmState}; @@ -100,7 +100,7 @@ impl RequestHandler for StubApiRequestHandler { Ok(()) } - fn vm_snapshot(&mut self, _: &str) -> Result<(), VmError> { + fn vm_snapshot(&mut self, _: &VmSnapshotConfig) -> Result<(), VmError> { Ok(()) } diff --git a/vmm/src/api/mod.rs b/vmm/src/api/mod.rs index a3f114bc11..8fac9f8261 100644 --- a/vmm/src/api/mod.rs +++ b/vmm/src/api/mod.rs @@ -259,10 +259,24 @@ pub struct VmRemoveDeviceData { pub id: String, } +/// Type of a VM snapshot: full memory dump or dirty-pages-only delta. +#[derive(Copy, Clone, Default, Deserialize, Serialize, Debug, PartialEq, Eq)] +#[serde(rename_all = "lowercase")] +pub enum VmSnapshotType { + /// Complete guest memory dump. + #[default] + Full, + /// Only pages dirtied since the tracked baseline. + Diff, +} + #[derive(Clone, Deserialize, Serialize, Default, Debug)] pub struct VmSnapshotConfig { /// The snapshot destination URL pub destination_url: String, + /// Full dump or dirty-pages delta. + #[serde(default)] + pub snapshot_type: VmSnapshotType, } #[derive(Clone, Deserialize, Serialize, Default, Debug)] @@ -689,7 +703,7 @@ pub trait RequestHandler { fn vm_resume(&mut self) -> Result<(), VmError>; - fn vm_snapshot(&mut self, destination_url: &str) -> Result<(), VmError>; + fn vm_snapshot(&mut self, config: &VmSnapshotConfig) -> Result<(), VmError>; fn vm_restore(&mut self, restore_cfg: RestoreConfig) -> Result<(), VmError>; @@ -1810,7 +1824,7 @@ impl ApiAction for VmSnapshot { info!("API request event: VmSnapshot {config:?}"); let response = vmm - .vm_snapshot(&config.destination_url) + .vm_snapshot(&config) .map_err(ApiError::VmSnapshot) .map(|_| ApiResponsePayload::Empty); diff --git a/vmm/src/api/openapi/cloud-hypervisor.yaml b/vmm/src/api/openapi/cloud-hypervisor.yaml index 1547b8d369..81441a37c4 100644 --- a/vmm/src/api/openapi/cloud-hypervisor.yaml +++ b/vmm/src/api/openapi/cloud-hypervisor.yaml @@ -1475,6 +1475,10 @@ components: properties: destination_url: type: string + snapshot_type: + type: string + enum: [full, diff] + default: full VmCoredumpData: type: object @@ -1498,6 +1502,9 @@ components: type: boolean memory_restore_mode: $ref: "#/components/schemas/MemoryRestoreMode" + track_dirty_pages: + type: boolean + default: false resume: type: boolean diff --git a/vmm/src/config.rs b/vmm/src/config.rs index 900fd3173b..12deedc5ca 100644 --- a/vmm/src/config.rs +++ b/vmm/src/config.rs @@ -2809,6 +2809,10 @@ pub struct RestoreConfig { pub prefault: bool, #[serde(default)] pub memory_restore_mode: MemoryRestoreMode, + /// Enable dirty-page tracking after restore so a later diff snapshot can + /// use this snapshot as its baseline. + #[serde(default)] + pub track_dirty_pages: bool, #[serde(default)] pub net_fds: Option>, #[serde(default)] @@ -2818,10 +2822,11 @@ pub struct RestoreConfig { impl RestoreConfig { pub const SYNTAX: &'static str = "Restore from a VM snapshot. \ \nRestore parameters \"source_url=,prefault=on|off,memory_restore_mode=copy|ondemand,\ - net_fds=,resume=true|false\" \ + track_dirty_pages=on|off,net_fds=,resume=true|false\" \ \n`source_url` should be a valid URL (e.g file:///foo/bar or tcp://192.168.1.10/foo) \ \n`prefault` controls eager prefaulting for the copy-based restore path (disabled by default) \ \n`memory_restore_mode=copy` preserves the existing eager read-copy restore behavior, while `memory_restore_mode=ondemand` enables lazy demand paging and fails restore if userfaultfd support is unavailable \ + \n`track_dirty_pages` enables differential snapshots after restore (disabled by default) \ \n`net_fds` is a list of net ids with new file descriptors. \ Only net devices backed by FDs directly are needed as input.\ \n `resume` controls whether the VM will be directly resumed after restore "; @@ -2832,6 +2837,7 @@ impl RestoreConfig { .add("source_url") .add("prefault") .add("memory_restore_mode") + .add("track_dirty_pages") .add("net_fds") .add("resume"); parser.parse(restore).map_err(Error::ParseRestore)?; @@ -2849,6 +2855,11 @@ impl RestoreConfig { .convert::("memory_restore_mode") .map_err(Error::ParseRestore)? .unwrap_or_default(); + let track_dirty_pages = parser + .convert::("track_dirty_pages") + .map_err(Error::ParseRestore)? + .unwrap_or(Toggle(false)) + .0; let net_fds = parser .convert::>>("net_fds") .map_err(Error::ParseRestore)? @@ -2871,6 +2882,7 @@ impl RestoreConfig { source_url, prefault, memory_restore_mode, + track_dirty_pages, net_fds, resume, }) @@ -5089,18 +5101,20 @@ id=\"{id}\",pci_segment={pci_segment},queue_sizes={queue_sizes}" source_url: PathBuf::from("/path/to/snapshot"), prefault: false, memory_restore_mode: MemoryRestoreMode::Copy, + track_dirty_pages: false, net_fds: None, resume: false, } ); assert_eq!( RestoreConfig::parse( - "source_url=/path/to/snapshot,prefault=off,net_fds=[net0@[3,4],net1@[5,6,7,8]]" + "source_url=/path/to/snapshot,prefault=off,track_dirty_pages=on,net_fds=[net0@[3,4],net1@[5,6,7,8]]" )?, RestoreConfig { source_url: PathBuf::from("/path/to/snapshot"), prefault: false, memory_restore_mode: MemoryRestoreMode::Copy, + track_dirty_pages: true, net_fds: Some(vec![ RestoredNetConfig { id: "net0".to_string(), @@ -5122,6 +5136,7 @@ id=\"{id}\",pci_segment={pci_segment},queue_sizes={queue_sizes}" source_url: PathBuf::from("/path/to/snapshot"), prefault: false, memory_restore_mode: MemoryRestoreMode::OnDemand, + track_dirty_pages: false, net_fds: None, resume: false, } @@ -5132,6 +5147,7 @@ id=\"{id}\",pci_segment={pci_segment},queue_sizes={queue_sizes}" source_url: PathBuf::from("/path/to/snapshot"), prefault: false, memory_restore_mode: MemoryRestoreMode::Copy, + track_dirty_pages: false, net_fds: None, resume: true, } @@ -5233,6 +5249,7 @@ id=\"{id}\",pci_segment={pci_segment},queue_sizes={queue_sizes}" source_url: PathBuf::from("/path/to/snapshot"), prefault: false, memory_restore_mode: MemoryRestoreMode::Copy, + track_dirty_pages: false, net_fds: Some(vec![ RestoredNetConfig { id: "net0".to_string(), @@ -5309,6 +5326,7 @@ id=\"{id}\",pci_segment={pci_segment},queue_sizes={queue_sizes}" source_url: PathBuf::from("/path/to/snapshot"), prefault: false, memory_restore_mode: MemoryRestoreMode::Copy, + track_dirty_pages: false, net_fds: None, resume: false, }; @@ -5326,6 +5344,7 @@ id=\"{id}\",pci_segment={pci_segment},queue_sizes={queue_sizes}" source_url: PathBuf::from("/path/to/snapshot"), prefault: true, memory_restore_mode: MemoryRestoreMode::OnDemand, + track_dirty_pages: false, net_fds: None, resume: false, }; diff --git a/vmm/src/lib.rs b/vmm/src/lib.rs index 1af7022b24..f5312c782f 100644 --- a/vmm/src/lib.rs +++ b/vmm/src/lib.rs @@ -49,7 +49,7 @@ use vmm_sys_util::sock_ctrl_msg::ScmSocket; use crate::api::{ ApiRequest, ApiResponse, MigrationMode, RequestHandler, TimeoutStrategy, VmInfoResponse, - VmReceiveMigrationData, VmSendMigrationData, VmmPingResponse, + VmReceiveMigrationData, VmSendMigrationData, VmSnapshotConfig, VmSnapshotType, VmmPingResponse, }; use crate::config::{MemoryRestoreMode, RestoreConfig, add_to_config}; #[cfg(all(target_arch = "x86_64", feature = "guest_debug"))] @@ -690,6 +690,16 @@ pub struct Vmm { console_info: Option, no_shutdown: bool, check_migration_evt: EventFd, + // Memory layout at diff-snapshot series start; Some means dirty logging is active. + snapshot_series_layout: Option, +} + +fn same_memory_layout(a: &MemoryRangeTable, b: &MemoryRangeTable) -> bool { + a.regions().len() == b.regions().len() + && a.regions() + .iter() + .zip(b.regions()) + .all(|(x, y)| x.gpa == y.gpa && x.length == y.length) } /// Time before aborting on the page fault connection. @@ -919,6 +929,7 @@ impl Vmm { console_info: None, no_shutdown, check_migration_evt, + snapshot_series_layout: None, }) } @@ -1904,6 +1915,7 @@ impl Vmm { vm_config: Arc>, prefault: bool, memory_restore_mode: MemoryRestoreMode, + track_dirty_pages: bool, ) -> result::Result<(), VmError> { match &self.vm { VmOwnership::Owned(_) => Err(VmError::VmAlreadyCreated), @@ -1958,6 +1970,16 @@ impl Vmm { Some(memory_restore_mode), )?; + let snapshot_series_layout = if track_dirty_pages { + let layout = vm.memory_range_table().map_err(VmError::Restore)?; + // Arm tracking before restore starts vCPUs or activates devices so + // restore-side writes are included in the later delta. + vm.start_dirty_log().map_err(VmError::Restore)?; + Some(layout) + } else { + None + }; + if self .vm_config .as_ref() @@ -1973,6 +1995,7 @@ impl Vmm { // Now we can restore the rest of the VM. // PANIC: won't panic, we just checked that the VM is there. vm.restore()?; + self.snapshot_series_layout = snapshot_series_layout; self.vm = VmOwnership::Owned(vm); Ok(()) } @@ -2315,20 +2338,53 @@ impl RequestHandler for Vmm { } } - fn vm_snapshot(&mut self, destination_url: &str) -> result::Result<(), VmError> { + fn vm_snapshot(&mut self, config: &VmSnapshotConfig) -> result::Result<(), VmError> { match self.vm { VmOwnership::Owned(ref mut vm) => { if vm.restoring() { return Err(VmError::VmRestoring); } + + let requested_diff = config.snapshot_type == VmSnapshotType::Diff; + let effective_diff = requested_diff && self.snapshot_series_layout.is_some(); + + if requested_diff { + let layout = vm.memory_range_table().map_err(VmError::Snapshot)?; + if let Some(baseline) = &self.snapshot_series_layout { + if !same_memory_layout(baseline, &layout) { + self.snapshot_series_layout = None; + let _ = vm.stop_dirty_log(); + return Err(VmError::Snapshot(MigratableError::Snapshot(anyhow!( + "memory layout changed since dirty tracking started" + )))); + } + } else { + vm.start_dirty_log().map_err(VmError::Snapshot)?; + self.snapshot_series_layout = Some(layout); + } + } else if self.snapshot_series_layout.take().is_some() { + vm.stop_dirty_log().map_err(VmError::Snapshot)?; + } + // Drain console_info so that FDs are not reused let _ = self.console_info.take(); - vm.snapshot() + let result = vm + .snapshot() .map_err(VmError::Snapshot) .and_then(|snapshot| { - vm.send(&snapshot, destination_url) + if effective_diff { + // Capture pages dirtied while snapshotting devices too. + let dirty = vm.dirty_log().map_err(VmError::Snapshot)?; + vm.set_diff_snapshot_ranges(dirty); + } + vm.send(&snapshot, &config.destination_url) .map_err(VmError::SnapshotSend) - }) + }); + + if result.is_err() && self.snapshot_series_layout.take().is_some() { + let _ = vm.stop_dirty_log(); + } + result } VmOwnership::Migration { .. } => Err(VmError::VmMigrating), VmOwnership::None => Err(VmError::VmNotRunning), @@ -2340,6 +2396,7 @@ impl RequestHandler for Vmm { VmOwnership::Owned(_) => Err(VmError::VmAlreadyCreated), VmOwnership::Migration { .. } => Err(VmError::VmMigrating), VmOwnership::None => { + self.snapshot_series_layout = None; if self.vm_config.is_some() { return Err(VmError::VmAlreadyCreated); } @@ -2379,6 +2436,7 @@ impl RequestHandler for Vmm { vm_config, restore_cfg.prefault, restore_cfg.memory_restore_mode, + restore_cfg.track_dirty_pages, ) .and_then(|()| { if restore_cfg.resume { @@ -2412,6 +2470,7 @@ impl RequestHandler for Vmm { } fn vm_shutdown(&mut self) -> result::Result<(), VmError> { + self.snapshot_series_layout = None; let mut vm = self.vm.take_owned_or(VmError::VmNotRunning)?; // Drain console_info so that the FDs are not reused let _ = self.console_info.take(); @@ -2426,6 +2485,7 @@ impl RequestHandler for Vmm { fn vm_reboot(&mut self) -> result::Result<(), VmError> { event!("vm", "rebooting"); + self.snapshot_series_layout = None; // Drop VM early to release disk locks and free other resources before // we reboot. @@ -2551,6 +2611,7 @@ impl RequestHandler for Vmm { } fn vm_delete(&mut self) -> result::Result<(), VmError> { + self.snapshot_series_layout = None; if self.vm_config.is_none() { return Ok(()); } @@ -3200,6 +3261,7 @@ impl RequestHandler for Vmm { // Take VM ownership. This also means that API events can no longer // change the VM (e.g. net device hotplug). + self.snapshot_series_layout = None; let vm = self .vm .take_owned_or(VmError::VmNotRunning) diff --git a/vmm/src/memory_manager.rs b/vmm/src/memory_manager.rs index d04fcbb35e..919c7371fe 100644 --- a/vmm/src/memory_manager.rs +++ b/vmm/src/memory_manager.rs @@ -226,6 +226,8 @@ pub struct MemoryManager { thp: bool, user_provided_zones: bool, snapshot_memory_ranges: MemoryRangeTable, + // Dirty ranges written by the next snapshot transport as a sparse delta. + diff_snapshot_ranges: Option, memory_zones: MemoryZones, log_dirty: bool, // Enable dirty logging for created RAM regions arch_mem_regions: Vec, @@ -1860,6 +1862,7 @@ impl MemoryManager { reserve: config.reserve, user_provided_zones, snapshot_memory_ranges: MemoryRangeTable::default(), + diff_snapshot_ranges: None, memory_zones, guest_ram_mappings: Vec::new(), uffd_handler: None, @@ -3194,12 +3197,77 @@ pub struct MemoryManagerSnapshotData { next_hotplug_slot: usize, } +// Writing a little clean memory between nearby dirty ranges sharply reduces +// sparse-file extent count and the metadata I/O needed to merge a delta. +const DIFF_SNAPSHOT_COALESCE_GAP: u64 = 64 * 1024; + +fn push_diff_write_range(plan: &mut Vec<(u64, MemoryRange)>, file_offset: u64, range: MemoryRange) { + if let Some((last_file_offset, last)) = plan.last_mut() { + let last_end = last.gpa + last.length; + let range_end = range.gpa + range.length; + if range.gpa >= last_end + && range.gpa - last_end <= DIFF_SNAPSHOT_COALESCE_GAP + && file_offset == *last_file_offset + (range.gpa - last.gpa) + { + last.length = range_end - last.gpa; + return; + } + } + plan.push((file_offset, range)); +} + +#[cfg(test)] +mod diff_snapshot_tests { + use super::*; + + #[test] + fn dirty_write_plan_coalesces_nearby_ranges() { + let mut plan = Vec::new(); + push_diff_write_range( + &mut plan, + 0, + MemoryRange { + gpa: 0x1000, + length: 0x1000, + }, + ); + push_diff_write_range( + &mut plan, + 0x4000, + MemoryRange { + gpa: 0x5000, + length: 0x1000, + }, + ); + push_diff_write_range( + &mut plan, + 0x20000, + MemoryRange { + gpa: 0x21000, + length: 0x1000, + }, + ); + + assert_eq!(plan.len(), 2); + assert_eq!(plan[0].0, 0); + assert_eq!(plan[0].1.gpa, 0x1000); + assert_eq!(plan[0].1.length, 0x5000); + } +} + +impl MemoryManager { + pub fn set_diff_snapshot_ranges(&mut self, table: MemoryRangeTable) { + self.diff_snapshot_ranges = Some(table); + } +} + impl Snapshottable for MemoryManager { fn id(&self) -> String { MEMORY_MANAGER_SNAPSHOT_ID.to_string() } fn snapshot(&mut self) -> result::Result { + self.diff_snapshot_ranges = None; let memory_ranges = self.memory_range_table(true)?; // Store locally this list of ranges as it will be used through the @@ -3229,7 +3297,11 @@ impl Transportable for MemoryManager { } let mut memory_file_path = url_to_path(destination_url)?; - memory_file_path.push(String::from(SNAPSHOT_FILENAME)); + memory_file_path.push(if self.diff_snapshot_ranges.is_some() { + format!("{SNAPSHOT_FILENAME}.diff") + } else { + String::from(SNAPSHOT_FILENAME) + }); let mut memory_file = OpenOptions::new() .read(true) @@ -3255,10 +3327,34 @@ impl Transportable for MemoryManager { // write path which never writes past the growing EOF. let sparse_layout = memory_file.set_len(total_len).is_ok(); - let guest_memory = self.guest_memory.memory(); - let mut file_cursor: u64 = 0; + let mut write_plan: Vec<(u64, MemoryRange)> = Vec::new(); + let mut layout_cursor = 0u64; + for full in self.snapshot_memory_ranges.regions() { + match &self.diff_snapshot_ranges { + None => write_plan.push((layout_cursor, full.clone())), + Some(dirty) => { + for range in dirty.regions() { + let start = range.gpa.max(full.gpa); + let end = (range.gpa + range.length).min(full.gpa + full.length); + if start < end { + push_diff_write_range( + &mut write_plan, + layout_cursor + (start - full.gpa), + MemoryRange { + gpa: start, + length: end - start, + }, + ); + } + } + } + } + layout_cursor += full.length; + } + debug_assert_eq!(layout_cursor, total_len); - for range in self.snapshot_memory_ranges.regions() { + let guest_memory = self.guest_memory.memory(); + for (file_cursor, range) in write_plan { let mut wrote_sparse = false; if sparse_layout && let Some(region) = guest_memory.find_region(GuestAddress(range.gpa)) @@ -3310,10 +3406,8 @@ impl Transportable for MemoryManager { } } - file_cursor += range.length; } - debug_assert_eq!(file_cursor, total_len); Ok(()) } } diff --git a/vmm/src/vm.rs b/vmm/src/vm.rs index 3bcb5a34d3..12a0dee8b3 100644 --- a/vmm/src/vm.rs +++ b/vmm/src/vm.rs @@ -3036,6 +3036,13 @@ impl Vm { .memory_range_table(false) } + pub fn set_diff_snapshot_ranges(&mut self, table: MemoryRangeTable) { + self.memory_manager + .lock() + .unwrap() + .set_diff_snapshot_ranges(table); + } + pub fn guest_memory(&self) -> GuestMemoryAtomic { self.memory_manager.lock().unwrap().guest_memory() } From f3f00c4361d405efd53bf96daf785737699946db Mon Sep 17 00:00:00 2001 From: sjmiller609 <7516283+sjmiller609@users.noreply.github.com> Date: Wed, 12 Aug 2026 02:20:06 +0000 Subject: [PATCH 2/3] Support snapshots with file-backed hotplug memory --- virtio-devices/src/mem.rs | 10 ++-- vmm/src/lib.rs | 4 +- vmm/src/memory_manager.rs | 109 ++++++++++++++++++++++++++++++-------- vmm/src/vm.rs | 4 ++ 4 files changed, 100 insertions(+), 27 deletions(-) diff --git a/virtio-devices/src/mem.rs b/virtio-devices/src/mem.rs index be8d6c54f8..934cab15ee 100644 --- a/virtio-devices/src/mem.rs +++ b/virtio-devices/src/mem.rs @@ -810,9 +810,13 @@ impl Mem { (avail_features, 0, config, false) }; - let host_fd = region - .file_offset() - .map(|f_offset| f_offset.file().as_raw_fd()); + let host_fd = if region.flags() & libc::MAP_SHARED == libc::MAP_SHARED { + region + .file_offset() + .map(|f_offset| f_offset.file().as_raw_fd()) + } else { + None + }; Ok(Mem { common: VirtioCommon { diff --git a/vmm/src/lib.rs b/vmm/src/lib.rs index f5312c782f..bdd4d5ad7b 100644 --- a/vmm/src/lib.rs +++ b/vmm/src/lib.rs @@ -1971,7 +1971,7 @@ impl Vmm { )?; let snapshot_series_layout = if track_dirty_pages { - let layout = vm.memory_range_table().map_err(VmError::Restore)?; + let layout = vm.mapping_range_table(); // Arm tracking before restore starts vCPUs or activates devices so // restore-side writes are included in the later delta. vm.start_dirty_log().map_err(VmError::Restore)?; @@ -2349,7 +2349,7 @@ impl RequestHandler for Vmm { let effective_diff = requested_diff && self.snapshot_series_layout.is_some(); if requested_diff { - let layout = vm.memory_range_table().map_err(VmError::Snapshot)?; + let layout = vm.mapping_range_table(); if let Some(baseline) = &self.snapshot_series_layout { if !same_memory_layout(baseline, &layout) { self.snapshot_series_layout = None; diff --git a/vmm/src/memory_manager.rs b/vmm/src/memory_manager.rs index 919c7371fe..89a1378ea8 100644 --- a/vmm/src/memory_manager.rs +++ b/vmm/src/memory_manager.rs @@ -226,6 +226,7 @@ pub struct MemoryManager { thp: bool, user_provided_zones: bool, snapshot_memory_ranges: MemoryRangeTable, + snapshot_fixed_file_layout: bool, // Dirty ranges written by the next snapshot transport as a sparse delta. diff_snapshot_ranges: Option, memory_zones: MemoryZones, @@ -786,7 +787,9 @@ impl MemoryManager { for zone_config in zones_config { if guest_ram_mapping.zone_id == zone_config.id { let region = MemoryManager::create_ram_region( - if guest_ram_mapping.virtio_mem { + if guest_ram_mapping.virtio_mem + && (zone_config.file.is_none() || zone_config.shared) + { &None } else { &zone_config.file @@ -1740,8 +1743,16 @@ impl MemoryManager { // will create ram region with `prefault` option in // restore config rather than same option in zone let region = MemoryManager::create_ram_region( - &None, - 0, + if zone.file.is_some() && !zone.shared { + &zone.file + } else { + &None + }, + if zone.file.is_some() && !zone.shared { + zone.size + } else { + 0 + }, start_addr, hotplug_size as usize, prefault.unwrap_or(zone.prefault), @@ -1837,6 +1848,9 @@ impl MemoryManager { let end_of_ram_area = start_of_device_area.unchecked_sub(1); let ram_allocator = AddressAllocator::new(GuestAddress(0), start_of_device_area.0).unwrap(); + let snapshot_fixed_file_layout = + user_provided_zones && zones.len() == 1 && zones[0].file.is_some() && !zones[0].shared; + #[allow(unused_mut)] let mut memory_manager = MemoryManager { boot_guest_memory, @@ -1862,6 +1876,7 @@ impl MemoryManager { reserve: config.reserve, user_provided_zones, snapshot_memory_ranges: MemoryRangeTable::default(), + snapshot_fixed_file_layout, diff_snapshot_ranges: None, memory_zones, guest_ram_mappings: Vec::new(), @@ -2686,6 +2701,17 @@ impl MemoryManager { &mut self.memory_zones } + pub fn mapping_range_table(&self) -> MemoryRangeTable { + let mut table = MemoryRangeTable::default(); + for mapping in &self.guest_ram_mappings { + table.push(MemoryRange { + gpa: mapping.gpa, + length: mapping.size, + }); + } + table + } + pub fn memory_range_table( &self, snapshot: bool, @@ -3259,6 +3285,26 @@ impl MemoryManager { pub fn set_diff_snapshot_ranges(&mut self, table: MemoryRangeTable) { self.diff_snapshot_ranges = Some(table); } + + fn snapshot_file_offset(&self, range: &MemoryRange) -> Option { + self.guest_ram_mappings.iter().find_map(|mapping| { + let mapping_end = mapping.gpa.checked_add(mapping.size)?; + let range_end = range.gpa.checked_add(range.length)?; + if range.gpa >= mapping.gpa && range_end <= mapping_end { + mapping.file_offset.checked_add(range.gpa - mapping.gpa) + } else { + None + } + }) + } + + fn snapshot_file_len(&self) -> Option { + let mut max = 0; + for mapping in &self.guest_ram_mappings { + max = max.max(mapping.file_offset.checked_add(mapping.size)?); + } + Some(max) + } } impl Snapshottable for MemoryManager { @@ -3311,27 +3357,45 @@ impl Transportable for MemoryManager { .with_context(|| format!("Error creating memory snapshot file {memory_file_path:?}")) .map_err(MigratableError::MigrateSend)?; - let total_len: u64 = self - .snapshot_memory_ranges - .regions() - .iter() - .map(|r| r.length) - .sum(); - - // Pre-size the file so per-region write_at lands at the dense-layout - // offset. On filesystems that support sparse files unwritten bytes - // become real holes; on others the kernel zero-fills the allocation, - // which is still byte-correct. If extending the file is not - // supported by the destination filesystem (some FUSE backends - // reject ftruncate-extend with EOPNOTSUPP), fall back to the dense - // write path which never writes past the growing EOF. - let sparse_layout = memory_file.set_len(total_len).is_ok(); + let total_len: u64 = if self.snapshot_fixed_file_layout { + self.snapshot_file_len().ok_or_else(|| { + MigratableError::MigrateSend(anyhow!("invalid fixed snapshot memory layout")) + })? + } else { + self.snapshot_memory_ranges + .regions() + .iter() + .map(|range| range.length) + .sum() + }; + + // Diff and fixed-layout snapshots require the declared length so holes + // keep stable offsets. Full dense snapshots can fall back to growing + // the file through sequential writes. + let sparse_layout = + if self.diff_snapshot_ranges.is_some() || self.snapshot_fixed_file_layout { + memory_file + .set_len(total_len) + .map_err(|e| MigratableError::MigrateSend(e.into()))?; + true + } else { + memory_file.set_len(total_len).is_ok() + }; let mut write_plan: Vec<(u64, MemoryRange)> = Vec::new(); let mut layout_cursor = 0u64; for full in self.snapshot_memory_ranges.regions() { + let full_file_offset = if self.snapshot_fixed_file_layout { + self.snapshot_file_offset(full).ok_or_else(|| { + MigratableError::MigrateSend(anyhow!( + "snapshot range is outside the fixed memory layout" + )) + })? + } else { + layout_cursor + }; match &self.diff_snapshot_ranges { - None => write_plan.push((layout_cursor, full.clone())), + None => write_plan.push((full_file_offset, full.clone())), Some(dirty) => { for range in dirty.regions() { let start = range.gpa.max(full.gpa); @@ -3339,7 +3403,7 @@ impl Transportable for MemoryManager { if start < end { push_diff_write_range( &mut write_plan, - layout_cursor + (start - full.gpa), + full_file_offset + (start - full.gpa), MemoryRange { gpa: start, length: end - start, @@ -3351,7 +3415,9 @@ impl Transportable for MemoryManager { } layout_cursor += full.length; } - debug_assert_eq!(layout_cursor, total_len); + if !self.snapshot_fixed_file_layout { + debug_assert_eq!(layout_cursor, total_len); + } let guest_memory = self.guest_memory.memory(); for (file_cursor, range) in write_plan { @@ -3405,7 +3471,6 @@ impl Transportable for MemoryManager { } } } - } Ok(()) diff --git a/vmm/src/vm.rs b/vmm/src/vm.rs index 12a0dee8b3..cef6d7bf25 100644 --- a/vmm/src/vm.rs +++ b/vmm/src/vm.rs @@ -3036,6 +3036,10 @@ impl Vm { .memory_range_table(false) } + pub fn mapping_range_table(&self) -> MemoryRangeTable { + self.memory_manager.lock().unwrap().mapping_range_table() + } + pub fn set_diff_snapshot_ranges(&mut self, table: MemoryRangeTable) { self.memory_manager .lock() From 5a3ca835c10c825ac4b41809eb4a37f8c3e15784 Mon Sep 17 00:00:00 2001 From: sjmiller609 <7516283+sjmiller609@users.noreply.github.com> Date: Wed, 12 Aug 2026 13:43:59 +0000 Subject: [PATCH 3/3] Build Kernel prereleases from tags --- .github/workflows/release.yaml | 36 ++++++++++++++++++++-------------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 6edf274b39..8f0638152d 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -1,5 +1,11 @@ name: Cloud Hypervisor Release -on: [create, merge_group] +on: + push: + tags: + - "v*-kernel.*" + merge_group: +permissions: + contents: write concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}-${{ github.event_name }} cancel-in-progress: true @@ -8,7 +14,7 @@ env: jobs: release: - if: (github.event_name == 'create' && github.event.ref_type == 'tag') || github.event_name == 'merge_group' + if: github.event_name == 'push' || github.event_name == 'merge_group' name: Release ${{ matrix.platform.target }} strategy: fail-fast: false @@ -35,9 +41,9 @@ jobs: run: sudo apt install -y musl-tools - name: Create release directory if: | - github.event_name == 'create' && github.event.ref_type == 'tag' && + github.event_name == 'push' && matrix.platform.target == 'x86_64-unknown-linux-gnu' - run: rsync -rv --exclude=.git . ../cloud-hypervisor-${{ github.event.ref }} + run: rsync -rv --exclude=.git . ../cloud-hypervisor-${{ github.ref_name }} - name: Build ${{ matrix.platform.target }} uses: houseabsolute/actions-rust-cross@v1 with: @@ -47,13 +53,13 @@ jobs: strip: true toolchain: "1.89.0" - name: Copy Release Binaries - if: github.event_name == 'create' && github.event.ref_type == 'tag' + if: github.event_name == 'push' shell: bash run: | cp target/${{ matrix.platform.target }}/release/cloud-hypervisor ./${{ matrix.platform.name_ch }} cp target/${{ matrix.platform.target }}/release/ch-remote ./${{ matrix.platform.name_ch_remote }} - name: Upload Release Artifacts - if: github.event_name == 'create' && github.event.ref_type == 'tag' + if: github.event_name == 'push' uses: actions/upload-artifact@v7 with: name: Artifacts for ${{ matrix.platform.target }} @@ -62,9 +68,9 @@ jobs: ./${{ matrix.platform.name_ch_remote }} - name: Vendor if: | - github.event_name == 'create' && github.event.ref_type == 'tag' && + github.event_name == 'push' && matrix.platform.target == 'x86_64-unknown-linux-gnu' - working-directory: ../cloud-hypervisor-${{ github.event.ref }} + working-directory: ../cloud-hypervisor-${{ github.ref_name }} run: | mkdir ../vendor-cargo-home export CARGO_HOME=$(realpath ../vendor-cargo-home) @@ -72,24 +78,24 @@ jobs: cargo vendor > .cargo/config.toml - name: Create vendored source archive if: | - github.event_name == 'create' && github.event.ref_type == 'tag' && + github.event_name == 'push' && matrix.platform.target == 'x86_64-unknown-linux-gnu' - run: tar cJf cloud-hypervisor-${{ github.event.ref }}.tar.xz ../cloud-hypervisor-${{ github.event.ref }} + run: tar cJf cloud-hypervisor-${{ github.ref_name }}.tar.xz ../cloud-hypervisor-${{ github.ref_name }} - name: Upload cloud-hypervisor vendored source archive if: | - github.event_name == 'create' && github.event.ref_type == 'tag' && + github.event_name == 'push' && matrix.platform.target == 'x86_64-unknown-linux-gnu' id: upload-release-cloud-hypervisor-vendored-sources uses: actions/upload-artifact@v7 with: - path: cloud-hypervisor-${{ github.event.ref }}.tar.xz - name: cloud-hypervisor-${{ github.event.ref }}.tar.xz + path: cloud-hypervisor-${{ github.ref_name }}.tar.xz + name: cloud-hypervisor-${{ github.ref_name }}.tar.xz - name: Create GitHub Release - if: github.event_name == 'create' && github.event.ref_type == 'tag' + if: github.event_name == 'push' uses: softprops/action-gh-release@v3 with: draft: true files: | ./${{ matrix.platform.name_ch }} ./${{ matrix.platform.name_ch_remote }} - ./cloud-hypervisor-${{ github.event.ref }}.tar.xz + ./cloud-hypervisor-${{ github.ref_name }}.tar.xz