From 7a7714a1285eda698d00bd7094bb820bbe8476a5 Mon Sep 17 00:00:00 2001 From: CMGS Date: Tue, 10 Nov 2026 15:10:57 +0800 Subject: [PATCH 1/7] virtio-devices: balloon: Remap released copy-on-write memory A VM restored with memory_restore_mode=copyonwrite maps the snapshot file MAP_PRIVATE over its guest RAM. MADV_DONTNEED there only drops the private copy: the next guest touch of a released page, even a write, faults the page back in from the snapshot file with read-around. With free page reporting on, every page the guest frees and reuses pays that read. Map anonymous memory over the 2 MiB-aligned part of each released range instead, so a reused page is zero-filled without I/O. The head and tail below 2 MiB keep MADV_DONTNEED, which bounds the mappings to one per 2 MiB of guest RAM, well under vm.max_map_count. The new mapping keeps the region's MAP_NORESERVE, and a failed mmap falls back to MADV_DONTNEED. The remapped range gets MADV_DONTDUMP like the rest of guest RAM, and MADV_NOHUGEPAGE: a private copy of a file page is 4 KiB today, and hugepage memory never takes the copy-on-write restore path, so a reused page keeps costing 4 KiB instead of a 2 MiB host page. An overlay region still records itself as anonymous, so the memory manager notes that the overlay was mapped and the balloon remaps only then. OnDemand restore never remaps, which would drop the userfaultfd registration. Signed-off-by: CMGS --- fuzz/fuzz_targets/balloon.rs | 9 ++ virtio-devices/src/balloon.rs | 182 ++++++++++++++++++++++++++++++++-- vmm/src/device_manager.rs | 1 + vmm/src/memory_manager.rs | 8 ++ 4 files changed, 194 insertions(+), 6 deletions(-) diff --git a/fuzz/fuzz_targets/balloon.rs b/fuzz/fuzz_targets/balloon.rs index a202b88339..695b91b528 100644 --- a/fuzz/fuzz_targets/balloon.rs +++ b/fuzz/fuzz_targets/balloon.rs @@ -96,6 +96,7 @@ fuzz_target!(|bytes: &[u8]| -> Corpus { SeccompAction::Allow, EventFd::new(EFD_NONBLOCK).unwrap(), None, + false, ) .unwrap(); balloon.set_access_platform(Arc::new(IdentityAccessPlatform {})); @@ -115,6 +116,7 @@ fuzz_target!(|bytes: &[u8]| -> Corpus { SeccompAction::Allow, EventFd::new(EFD_NONBLOCK).unwrap(), Some(state), + false, ); } } @@ -197,6 +199,7 @@ fuzz_target!(|bytes: &[u8]| -> Corpus { SeccompAction::Allow, EventFd::new(EFD_NONBLOCK).unwrap(), None, + false, ) .unwrap(); balloon_no_reporting.set_access_platform(Arc::new(IdentityAccessPlatform {})); @@ -244,6 +247,7 @@ fuzz_target!(|bytes: &[u8]| -> Corpus { SeccompAction::Allow, EventFd::new(EFD_NONBLOCK).unwrap(), None, + false, ) .unwrap(); balloon_reporting_empty.ack_features(1u64 << VIRTIO_BALLOON_F_REPORTING); @@ -454,6 +458,7 @@ fn exercise_stats_queue(bytes: &[u8], queue_data: &[u8]) { SeccompAction::Allow, EventFd::new(EFD_NONBLOCK).unwrap(), None, + false, ) { Ok(balloon) => balloon, Err(_) => return, @@ -593,6 +598,7 @@ fn run_failing_interrupt_case( SeccompAction::Allow, EventFd::new(EFD_NONBLOCK).unwrap(), None, + false, ) .unwrap(); balloon.ack_features(1u64 << VIRTIO_BALLOON_F_REPORTING); @@ -653,6 +659,7 @@ fn activate_with_too_few_queues(queue_data: &[u8]) { SeccompAction::Allow, EventFd::new(EFD_NONBLOCK).unwrap(), None, + false, ) .unwrap(); @@ -699,6 +706,7 @@ fn activate_reporting_acked_without_reporting_queue(queue_data: &[u8]) { SeccompAction::Allow, EventFd::new(EFD_NONBLOCK).unwrap(), None, + false, ) .unwrap(); balloon.ack_features(1u64 << VIRTIO_BALLOON_F_REPORTING); @@ -754,6 +762,7 @@ fn activate_reporting_available_unacked(queue_data: &[u8]) { SeccompAction::Allow, EventFd::new(EFD_NONBLOCK).unwrap(), None, + false, ) .unwrap(); diff --git a/virtio-devices/src/balloon.rs b/virtio-devices/src/balloon.rs index 7a8494b0e8..f9bbd1dfaa 100644 --- a/virtio-devices/src/balloon.rs +++ b/virtio-devices/src/balloon.rs @@ -76,6 +76,9 @@ const VIRTIO_BALLOON_PAGE_SIZE: usize = 1 << VIRTIO_BALLOON_PFN_SHIFT; // Batch the number of PFNs handled in one pass const VIRTIO_BALLOON_PFNS_PER_BATCH: usize = 256; +// Granularity at which released copy-on-write memory is remapped anonymous. +const COW_REMAP_SIZE: u64 = 2 << 20; + // Enable statistics virtqueue. const VIRTIO_BALLOON_F_STATS_VQ: u64 = 1; // Deflate balloon on OOM @@ -296,6 +299,7 @@ struct BalloonEpollHandler { pause_evt: EventFd, pbp: Option, access_platform: Option>, + cow_overlay: bool, } impl BalloonEpollHandler { @@ -331,6 +335,7 @@ impl BalloonEpollHandler { memory: &GuestMemory, range_base: GuestAddress, range_len: usize, + cow_overlay: bool, ) -> result::Result<(), Error> { let region = memory.find_region(range_base).ok_or(Error::GuestMemory( GuestMemoryError::InvalidGuestAddress(range_base), @@ -372,15 +377,89 @@ impl BalloonEpollHandler { } else { Ok(()) } + } else if cow_overlay { + Self::remap_anonymous_range( + memory, + range_base, + len, + region.flags() & libc::MAP_NORESERVE, + ) } else { Self::advise_memory_range(memory, range_base, len as usize, libc::MADV_DONTNEED) } } + // MADV_DONTNEED on a copy-on-write restored range refaults each page from + // the snapshot file, so map anonymous memory over its 2 MiB-aligned part. + fn remap_anonymous_range( + memory: &GuestMemory, + range_base: GuestAddress, + len: u64, + reserve_flag: libc::c_int, + ) -> result::Result<(), Error> { + let start = range_base.0; + let end = start + len; + let remap_start = start.next_multiple_of(COW_REMAP_SIZE).min(end); + let remap_end = (end - end % COW_REMAP_SIZE).max(remap_start); + for (base, len) in [(start, remap_start - start), (remap_end, end - remap_end)] { + if len != 0 { + Self::advise_memory_range( + memory, + GuestAddress(base), + len as usize, + libc::MADV_DONTNEED, + )?; + } + } + if remap_start == remap_end { + return Ok(()); + } + + let remap_len = (remap_end - remap_start) as usize; + let slice = memory + .get_slice(GuestAddress(remap_start), remap_len) + .map_err(Error::GuestMemory)?; + // SAFETY: the slice lies inside a live guest RAM mapping, and MAP_FIXED + // replaces exactly that window. + let addr = unsafe { + libc::mmap( + slice.ptr_guard_mut().as_ptr().cast(), + remap_len, + libc::PROT_READ | libc::PROT_WRITE, + libc::MAP_FIXED | libc::MAP_PRIVATE | libc::MAP_ANONYMOUS | reserve_flag, + -1, + 0, + ) + }; + if addr == libc::MAP_FAILED { + warn!( + "Failed to remap released memory at GPA 0x{remap_start:x}: {}", + io::Error::last_os_error() + ); + return Self::advise_memory_range( + memory, + GuestAddress(remap_start), + remap_len, + libc::MADV_DONTNEED, + ); + } + for advice in [libc::MADV_DONTDUMP, libc::MADV_NOHUGEPAGE] { + // SAFETY: addr and remap_len name the mapping installed above. + if unsafe { libc::madvise(addr, remap_len, advice) } != 0 { + warn!( + "Failed to madvise remapped memory at GPA 0x{remap_start:x}: {}", + io::Error::last_os_error() + ); + } + } + Ok(()) + } + fn release_memory_range_partial_page( pbp: &mut Option, memory: &GuestMemory, range_base: GuestAddress, + cow_overlay: bool, ) -> result::Result<(), Error> { let page_size: u64 = get_page_size(); if pbp.is_none() { @@ -399,6 +478,7 @@ impl BalloonEpollHandler { memory, vm_memory::GuestAddress(pbp.as_ref().unwrap().addr), page_size as usize, + cow_overlay, )?; pbp.as_mut().unwrap().reset(); @@ -416,9 +496,12 @@ impl BalloonEpollHandler { // if the host page size differs from that. if get_page_size() as usize != VIRTIO_BALLOON_PAGE_SIZE { for &address in addresses.iter() { - if let Err(e) = - Self::release_memory_range_partial_page(&mut self.pbp, memory, address) - { + if let Err(e) = Self::release_memory_range_partial_page( + &mut self.pbp, + memory, + address, + self.cow_overlay, + ) { warn!("Failed to release memory at address {:#x}: {e}", address.0); } } @@ -442,7 +525,9 @@ impl BalloonEpollHandler { if previous_address.checked_add(VIRTIO_BALLOON_PAGE_SIZE as u64) == Some(address) { range_len += VIRTIO_BALLOON_PAGE_SIZE; } else { - if let Err(e) = Self::release_memory_range(memory, range_base, range_len) { + if let Err(e) = + Self::release_memory_range(memory, range_base, range_len, self.cow_overlay) + { warn!("Failed to release ballooned memory range: {e}"); } range_base = address; @@ -451,7 +536,9 @@ impl BalloonEpollHandler { previous_address = address; } - if let Err(e) = Self::release_memory_range(memory, range_base, range_len) { + if let Err(e) = + Self::release_memory_range(memory, range_base, range_len, self.cow_overlay) + { warn!("Failed to release ballooned memory range: {e}"); } } @@ -652,6 +739,7 @@ impl BalloonEpollHandler { desc_chain.memory(), desc.addr(), desc.len() as usize, + self.cow_overlay, ) { warn!("Failed to release reported memory range: {e}"); } @@ -812,6 +900,7 @@ pub struct Balloon { stats_request_evt: EventFd, stats_request_sender: Option>, stats_cache: BalloonStatsCache, + cow_overlay: bool, } impl Balloon { @@ -826,6 +915,7 @@ impl Balloon { seccomp_action: SeccompAction, exit_evt: EventFd, state: Option, + cow_overlay: bool, ) -> io::Result { let mut queue_sizes = vec![QUEUE_SIZE; MIN_NUM_QUEUES as usize]; @@ -886,6 +976,7 @@ impl Balloon { stats_request_evt, stats_request_sender: None, stats_cache: Arc::new(Mutex::new(None)), + cow_overlay, }) } @@ -1068,6 +1159,7 @@ impl VirtioDevice for Balloon { pause_evt, pbp: None, access_platform: self.common.access_platform(), + cow_overlay: self.cow_overlay, }; let pause_control = Arc::clone(&self.common.pause_control); @@ -1179,6 +1271,7 @@ mod tests { pause_evt: EventFd::new(EFD_NONBLOCK).unwrap(), pbp: None, access_platform: None, + cow_overlay: false, } } @@ -1221,7 +1314,7 @@ mod tests { memory.write_slice(&[0xa5], GuestAddress(0)).unwrap(); assert_eq!(memory.read_obj::(GuestAddress(0)).unwrap(), 0xa5); - let res = BalloonEpollHandler::release_memory_range(&memory, GuestAddress(0), 4096); + let res = BalloonEpollHandler::release_memory_range(&memory, GuestAddress(0), 4096, false); assert!(res.is_ok(), "release_memory_range failed: {res:?}"); assert_eq!(memory.read_obj::(GuestAddress(0)).unwrap(), 0x5a); assert_eq!(fs::read(backing_file.as_path()).unwrap(), original); @@ -1239,6 +1332,79 @@ mod tests { test_release_private_page(false); } + #[test] + fn release_cow_overlay_remaps_aligned_part() { + use std::fs; + use std::io::Write; + use std::os::unix::io::AsRawFd; + + use vmm_sys_util::tempfile::TempFile; + + let page = get_page_size(); + let chunk = COW_REMAP_SIZE; + let size = 3 * chunk; + let memory = GuestMemory::from_ranges(&[(GuestAddress(0), size as usize)]).unwrap(); + + let backing_file = TempFile::new().unwrap(); + backing_file + .as_file() + .write_all(&vec![0x5a; size as usize]) + .unwrap(); + let original = fs::read(backing_file.as_path()).unwrap(); + let host = memory.get_host_address(GuestAddress(0)).unwrap(); + // SAFETY: the window is the whole private anonymous region created above. + let ret = unsafe { + libc::mmap( + host.cast(), + size as usize, + libc::PROT_READ | libc::PROT_WRITE, + libc::MAP_PRIVATE | libc::MAP_FIXED, + backing_file.as_file().as_raw_fd(), + 0, + ) + }; + assert_ne!(ret, libc::MAP_FAILED); + for gpa in [0, page, chunk - page, chunk, 2 * chunk, 2 * chunk + page] { + memory.write_obj(0xa5u8, GuestAddress(gpa)).unwrap(); + } + + // Without the overlay flag, a released page reads the snapshot file again. + BalloonEpollHandler::release_memory_range(&memory, GuestAddress(chunk), 4096, false) + .unwrap(); + assert_eq!(memory.read_obj::(GuestAddress(chunk)).unwrap(), 0x5a); + memory.write_obj(0xa5u8, GuestAddress(chunk)).unwrap(); + + let base = GuestAddress(page); + let len = (2 * chunk) as usize; + BalloonEpollHandler::release_memory_range(&memory, base, len, true).unwrap(); + + // Outside the range, the guest write stays. + assert_eq!(memory.read_obj::(GuestAddress(0)).unwrap(), 0xa5); + assert_eq!( + memory + .read_obj::(GuestAddress(2 * chunk + page)) + .unwrap(), + 0xa5 + ); + // The unaligned head and tail fall back to the snapshot file. + assert_eq!(memory.read_obj::(GuestAddress(page)).unwrap(), 0x5a); + assert_eq!( + memory.read_obj::(GuestAddress(chunk - page)).unwrap(), + 0x5a + ); + assert_eq!( + memory.read_obj::(GuestAddress(2 * chunk)).unwrap(), + 0x5a + ); + // The aligned chunk is anonymous memory and reads as zero. + assert_eq!(memory.read_obj::(GuestAddress(chunk)).unwrap(), 0); + assert_eq!( + memory.read_obj::(GuestAddress(2 * chunk - 1)).unwrap(), + 0 + ); + assert_eq!(fs::read(backing_file.as_path()).unwrap(), original); + } + #[test] fn process_inflate_addresses_releases_coalesced_ranges() { let page_size = get_page_size() as usize; @@ -1394,6 +1560,7 @@ mod tests { SeccompAction::Allow, EventFd::new(EFD_NONBLOCK).unwrap(), None, + false, ) .unwrap(); @@ -1411,6 +1578,7 @@ mod tests { SeccompAction::Allow, EventFd::new(EFD_NONBLOCK).unwrap(), None, + false, ) .unwrap(); balloon.common.acked_features = 1u64 << VIRTIO_BALLOON_F_STATS_VQ; @@ -1432,6 +1600,7 @@ mod tests { SeccompAction::Allow, EventFd::new(EFD_NONBLOCK).unwrap(), None, + false, ) .unwrap(); balloon.common.acked_features = 1u64 << VIRTIO_BALLOON_F_STATS_VQ; @@ -1464,6 +1633,7 @@ mod tests { SeccompAction::Allow, EventFd::new(EFD_NONBLOCK).unwrap(), None, + false, ) .unwrap(); balloon.common.acked_features = 1u64 << VIRTIO_BALLOON_F_STATS_VQ; diff --git a/vmm/src/device_manager.rs b/vmm/src/device_manager.rs index 4223ab5643..4f9ee87588 100644 --- a/vmm/src/device_manager.rs +++ b/vmm/src/device_manager.rs @@ -4092,6 +4092,7 @@ impl DeviceManager { .map_err(DeviceManagerError::EventFd)?, state_from_id(snapshot, id.as_str()) .map_err(DeviceManagerError::RestoreGetState)?, + self.memory_manager.lock().unwrap().cow_restored(), ) .map_err(DeviceManagerError::CreateVirtioBalloon)?, )); diff --git a/vmm/src/memory_manager.rs b/vmm/src/memory_manager.rs index d06ce097d1..72b7a85243 100644 --- a/vmm/src/memory_manager.rs +++ b/vmm/src/memory_manager.rs @@ -277,6 +277,7 @@ pub struct MemoryManager { diff_snapshot_ranges: Option, diff_count: u32, cow_source: Option, + cow_restored: bool, memory_zones: MemoryZones, log_dirty: bool, // Enable dirty logging for created RAM regions arch_mem_regions: Vec, @@ -1051,6 +1052,7 @@ impl MemoryManager { ))); } do_mmap_cow_saved_regions(&guest_memory, &memory_file, saved_regions, self.thp)?; + self.cow_restored = true; // Landlock applies after the restore, so the pagemap is opened here. self.cow_source = match PageMap::open() { Ok(pagemap) => Some(CowSource { @@ -1398,6 +1400,11 @@ impl MemoryManager { } } + /// True when guest RAM is a copy-on-write mapping of the snapshot file. + pub fn cow_restored(&self) -> bool { + self.cow_restored + } + /// True while an on-demand (UFFD) restore is still faulting in guest RAM. pub fn restoring(&self) -> bool { self.uffd_handler @@ -2300,6 +2307,7 @@ impl MemoryManager { diff_snapshot_ranges: None, diff_count: 0, cow_source: None, + cow_restored: false, memory_zones, guest_ram_mappings: Vec::new(), uffd_handler: None, From 2a96fb4fb8d48e1aaab480967a2340ab63b89525 Mon Sep 17 00:00:00 2001 From: CMGS Date: Tue, 10 Nov 2026 15:16:46 +0800 Subject: [PATCH 2/7] virtio-devices: balloon: Trim comments in the copy-on-write remap Signed-off-by: CMGS --- virtio-devices/src/balloon.rs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/virtio-devices/src/balloon.rs b/virtio-devices/src/balloon.rs index f9bbd1dfaa..52757f2bde 100644 --- a/virtio-devices/src/balloon.rs +++ b/virtio-devices/src/balloon.rs @@ -76,7 +76,6 @@ const VIRTIO_BALLOON_PAGE_SIZE: usize = 1 << VIRTIO_BALLOON_PFN_SHIFT; // Batch the number of PFNs handled in one pass const VIRTIO_BALLOON_PFNS_PER_BATCH: usize = 256; -// Granularity at which released copy-on-write memory is remapped anonymous. const COW_REMAP_SIZE: u64 = 2 << 20; // Enable statistics virtqueue. @@ -389,8 +388,7 @@ impl BalloonEpollHandler { } } - // MADV_DONTNEED on a copy-on-write restored range refaults each page from - // the snapshot file, so map anonymous memory over its 2 MiB-aligned part. + // DONTNEED would refault copy-on-write pages from the snapshot file. fn remap_anonymous_range( memory: &GuestMemory, range_base: GuestAddress, @@ -1368,7 +1366,6 @@ mod tests { memory.write_obj(0xa5u8, GuestAddress(gpa)).unwrap(); } - // Without the overlay flag, a released page reads the snapshot file again. BalloonEpollHandler::release_memory_range(&memory, GuestAddress(chunk), 4096, false) .unwrap(); assert_eq!(memory.read_obj::(GuestAddress(chunk)).unwrap(), 0x5a); @@ -1378,7 +1375,6 @@ mod tests { let len = (2 * chunk) as usize; BalloonEpollHandler::release_memory_range(&memory, base, len, true).unwrap(); - // Outside the range, the guest write stays. assert_eq!(memory.read_obj::(GuestAddress(0)).unwrap(), 0xa5); assert_eq!( memory @@ -1386,7 +1382,6 @@ mod tests { .unwrap(), 0xa5 ); - // The unaligned head and tail fall back to the snapshot file. assert_eq!(memory.read_obj::(GuestAddress(page)).unwrap(), 0x5a); assert_eq!( memory.read_obj::(GuestAddress(chunk - page)).unwrap(), @@ -1396,7 +1391,6 @@ mod tests { memory.read_obj::(GuestAddress(2 * chunk)).unwrap(), 0x5a ); - // The aligned chunk is anonymous memory and reads as zero. assert_eq!(memory.read_obj::(GuestAddress(chunk)).unwrap(), 0); assert_eq!( memory.read_obj::(GuestAddress(2 * chunk - 1)).unwrap(), From a3f82060cd23c8a355d9420527f76602be837a47 Mon Sep 17 00:00:00 2001 From: CMGS Date: Tue, 10 Nov 2026 15:21:56 +0800 Subject: [PATCH 3/7] virtio-devices: balloon: Test the aligned remap and rename the flag Signed-off-by: CMGS --- virtio-devices/src/balloon.rs | 131 +++++++++++++++++++++------------- 1 file changed, 81 insertions(+), 50 deletions(-) diff --git a/virtio-devices/src/balloon.rs b/virtio-devices/src/balloon.rs index 52757f2bde..6b7aa4b992 100644 --- a/virtio-devices/src/balloon.rs +++ b/virtio-devices/src/balloon.rs @@ -298,7 +298,7 @@ struct BalloonEpollHandler { pause_evt: EventFd, pbp: Option, access_platform: Option>, - cow_overlay: bool, + cow_restored: bool, } impl BalloonEpollHandler { @@ -334,7 +334,7 @@ impl BalloonEpollHandler { memory: &GuestMemory, range_base: GuestAddress, range_len: usize, - cow_overlay: bool, + cow_restored: bool, ) -> result::Result<(), Error> { let region = memory.find_region(range_base).ok_or(Error::GuestMemory( GuestMemoryError::InvalidGuestAddress(range_base), @@ -376,7 +376,7 @@ impl BalloonEpollHandler { } else { Ok(()) } - } else if cow_overlay { + } else if cow_restored { Self::remap_anonymous_range( memory, range_base, @@ -399,12 +399,12 @@ impl BalloonEpollHandler { let end = start + len; let remap_start = start.next_multiple_of(COW_REMAP_SIZE).min(end); let remap_end = (end - end % COW_REMAP_SIZE).max(remap_start); - for (base, len) in [(start, remap_start - start), (remap_end, end - remap_end)] { - if len != 0 { + for (base, part) in [(start, remap_start - start), (remap_end, end - remap_end)] { + if part != 0 { Self::advise_memory_range( memory, GuestAddress(base), - len as usize, + part as usize, libc::MADV_DONTNEED, )?; } @@ -457,7 +457,7 @@ impl BalloonEpollHandler { pbp: &mut Option, memory: &GuestMemory, range_base: GuestAddress, - cow_overlay: bool, + cow_restored: bool, ) -> result::Result<(), Error> { let page_size: u64 = get_page_size(); if pbp.is_none() { @@ -476,7 +476,7 @@ impl BalloonEpollHandler { memory, vm_memory::GuestAddress(pbp.as_ref().unwrap().addr), page_size as usize, - cow_overlay, + cow_restored, )?; pbp.as_mut().unwrap().reset(); @@ -498,7 +498,7 @@ impl BalloonEpollHandler { &mut self.pbp, memory, address, - self.cow_overlay, + self.cow_restored, ) { warn!("Failed to release memory at address {:#x}: {e}", address.0); } @@ -524,7 +524,7 @@ impl BalloonEpollHandler { range_len += VIRTIO_BALLOON_PAGE_SIZE; } else { if let Err(e) = - Self::release_memory_range(memory, range_base, range_len, self.cow_overlay) + Self::release_memory_range(memory, range_base, range_len, self.cow_restored) { warn!("Failed to release ballooned memory range: {e}"); } @@ -535,7 +535,7 @@ impl BalloonEpollHandler { } if let Err(e) = - Self::release_memory_range(memory, range_base, range_len, self.cow_overlay) + Self::release_memory_range(memory, range_base, range_len, self.cow_restored) { warn!("Failed to release ballooned memory range: {e}"); } @@ -737,7 +737,7 @@ impl BalloonEpollHandler { desc_chain.memory(), desc.addr(), desc.len() as usize, - self.cow_overlay, + self.cow_restored, ) { warn!("Failed to release reported memory range: {e}"); } @@ -898,7 +898,7 @@ pub struct Balloon { stats_request_evt: EventFd, stats_request_sender: Option>, stats_cache: BalloonStatsCache, - cow_overlay: bool, + cow_restored: bool, } impl Balloon { @@ -913,7 +913,7 @@ impl Balloon { seccomp_action: SeccompAction, exit_evt: EventFd, state: Option, - cow_overlay: bool, + cow_restored: bool, ) -> io::Result { let mut queue_sizes = vec![QUEUE_SIZE; MIN_NUM_QUEUES as usize]; @@ -974,7 +974,7 @@ impl Balloon { stats_request_evt, stats_request_sender: None, stats_cache: Arc::new(Mutex::new(None)), - cow_overlay, + cow_restored, }) } @@ -1157,7 +1157,7 @@ impl VirtioDevice for Balloon { pause_evt, pbp: None, access_platform: self.common.access_platform(), - cow_overlay: self.cow_overlay, + cow_restored: self.cow_restored, }; let pause_control = Arc::clone(&self.common.pause_control); @@ -1219,7 +1219,10 @@ impl Migratable for Balloon {} #[cfg(test)] mod tests { + use std::fs; + use vm_virtio::queue::testing::VirtQueue; + use vmm_sys_util::tempfile::TempFile; use super::*; @@ -1269,7 +1272,7 @@ mod tests { pause_evt: EventFd::new(EFD_NONBLOCK).unwrap(), pbp: None, access_platform: None, - cow_overlay: false, + cow_restored: false, } } @@ -1330,25 +1333,16 @@ mod tests { test_release_private_page(false); } - #[test] - fn release_cow_overlay_remaps_aligned_part() { - use std::fs; + fn cow_restored_memory(size: u64) -> (GuestMemory, TempFile) { use std::io::Write; use std::os::unix::io::AsRawFd; - use vmm_sys_util::tempfile::TempFile; - - let page = get_page_size(); - let chunk = COW_REMAP_SIZE; - let size = 3 * chunk; let memory = GuestMemory::from_ranges(&[(GuestAddress(0), size as usize)]).unwrap(); - let backing_file = TempFile::new().unwrap(); backing_file .as_file() .write_all(&vec![0x5a; size as usize]) .unwrap(); - let original = fs::read(backing_file.as_path()).unwrap(); let host = memory.get_host_address(GuestAddress(0)).unwrap(); // SAFETY: the window is the whole private anonymous region created above. let ret = unsafe { @@ -1362,6 +1356,32 @@ mod tests { ) }; assert_ne!(ret, libc::MAP_FAILED); + (memory, backing_file) + } + + fn vm_flags(addr: u64) -> String { + let smaps = fs::read_to_string("/proc/self/smaps").unwrap(); + let mut inside = false; + for line in smaps.lines() { + if let Some((range, _)) = line.split_once(' ') + && let Some((start, end)) = range.split_once('-') + && let (Ok(start), Ok(end)) = + (u64::from_str_radix(start, 16), u64::from_str_radix(end, 16)) + { + inside = (start..end).contains(&addr); + } else if inside && let Some(flags) = line.strip_prefix("VmFlags:") { + return flags.to_string(); + } + } + panic!("no mapping at {addr:#x}"); + } + + #[test] + fn release_cow_restored_remaps_aligned_part() { + let page = get_page_size(); + let chunk = COW_REMAP_SIZE; + let (memory, backing_file) = cow_restored_memory(3 * chunk); + let original = fs::read(backing_file.as_path()).unwrap(); for gpa in [0, page, chunk - page, chunk, 2 * chunk, 2 * chunk + page] { memory.write_obj(0xa5u8, GuestAddress(gpa)).unwrap(); } @@ -1371,32 +1391,43 @@ mod tests { assert_eq!(memory.read_obj::(GuestAddress(chunk)).unwrap(), 0x5a); memory.write_obj(0xa5u8, GuestAddress(chunk)).unwrap(); - let base = GuestAddress(page); let len = (2 * chunk) as usize; - BalloonEpollHandler::release_memory_range(&memory, base, len, true).unwrap(); + BalloonEpollHandler::release_memory_range(&memory, GuestAddress(page), len, true).unwrap(); + + for (gpa, value) in [ + (0, 0xa5), + (2 * chunk + page, 0xa5), + (page, 0x5a), + (chunk - page, 0x5a), + (2 * chunk, 0x5a), + (chunk, 0), + (2 * chunk - 1, 0), + ] { + assert_eq!(memory.read_obj::(GuestAddress(gpa)).unwrap(), value); + } + assert_eq!(fs::read(backing_file.as_path()).unwrap(), original); + } + + #[test] + fn release_cow_restored_aligned_range_is_anonymous() { + let chunk = COW_REMAP_SIZE; + let (memory, _backing_file) = cow_restored_memory(3 * chunk); + memory.write_obj(0xa5u8, GuestAddress(chunk)).unwrap(); + + BalloonEpollHandler::release_memory_range( + &memory, + GuestAddress(chunk), + chunk as usize, + true, + ) + .unwrap(); - assert_eq!(memory.read_obj::(GuestAddress(0)).unwrap(), 0xa5); - assert_eq!( - memory - .read_obj::(GuestAddress(2 * chunk + page)) - .unwrap(), - 0xa5 - ); - assert_eq!(memory.read_obj::(GuestAddress(page)).unwrap(), 0x5a); - assert_eq!( - memory.read_obj::(GuestAddress(chunk - page)).unwrap(), - 0x5a - ); - assert_eq!( - memory.read_obj::(GuestAddress(2 * chunk)).unwrap(), - 0x5a - ); assert_eq!(memory.read_obj::(GuestAddress(chunk)).unwrap(), 0); - assert_eq!( - memory.read_obj::(GuestAddress(2 * chunk - 1)).unwrap(), - 0 - ); - assert_eq!(fs::read(backing_file.as_path()).unwrap(), original); + let host = memory.get_host_address(GuestAddress(chunk)).unwrap() as u64; + let flags = vm_flags(host); + assert!(flags.split_whitespace().any(|f| f == "dd"), "{flags}"); + assert!(flags.split_whitespace().any(|f| f == "nh"), "{flags}"); + assert!(!vm_flags(host - 1).contains(" nh")); } #[test] From 8500502b61bd151a2bc7404b9ed3c11aae432806 Mon Sep 17 00:00:00 2001 From: CMGS Date: Tue, 10 Nov 2026 17:03:41 +0800 Subject: [PATCH 4/7] virtio-devices: balloon: Remap copy-on-write chunks that inflate fills Signed-off-by: CMGS --- virtio-devices/src/balloon.rs | 144 +++++++++++++++++++++++++++++++++- 1 file changed, 143 insertions(+), 1 deletion(-) diff --git a/virtio-devices/src/balloon.rs b/virtio-devices/src/balloon.rs index 6b7aa4b992..5376121464 100644 --- a/virtio-devices/src/balloon.rs +++ b/virtio-devices/src/balloon.rs @@ -14,6 +14,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +use std::collections::HashMap; use std::io::{self, Write}; use std::ops::Deref; use std::os::unix::io::AsRawFd; @@ -77,6 +78,7 @@ const VIRTIO_BALLOON_PAGE_SIZE: usize = 1 << VIRTIO_BALLOON_PFN_SHIFT; const VIRTIO_BALLOON_PFNS_PER_BATCH: usize = 256; const COW_REMAP_SIZE: u64 = 2 << 20; +const COW_CHUNK_WORDS: usize = COW_REMAP_SIZE as usize / VIRTIO_BALLOON_PAGE_SIZE / 64; // Enable statistics virtqueue. const VIRTIO_BALLOON_F_STATS_VQ: u64 = 1; @@ -299,6 +301,7 @@ struct BalloonEpollHandler { pbp: Option, access_platform: Option>, cow_restored: bool, + inflated_chunks: HashMap, } impl BalloonEpollHandler { @@ -540,6 +543,35 @@ impl BalloonEpollHandler { warn!("Failed to release ballooned memory range: {e}"); } } + + if !self.cow_restored { + return; + } + for address in addresses.iter() { + let chunk = address.0 / COW_REMAP_SIZE; + let page = (address.0 % COW_REMAP_SIZE) as usize / VIRTIO_BALLOON_PAGE_SIZE; + let bits = self.inflated_chunks.entry(chunk).or_default(); + bits[page / 64] |= 1 << (page % 64); + if bits.iter().any(|word| *word != u64::MAX) { + continue; + } + self.inflated_chunks.remove(&chunk); + let base = GuestAddress(chunk * COW_REMAP_SIZE); + let Some(region) = memory.find_region(base) else { + continue; + }; + if base.0 + COW_REMAP_SIZE > region.start_addr().0 + region.len() { + continue; + } + if let Err(e) = Self::remap_anonymous_range( + memory, + base, + COW_REMAP_SIZE, + region.flags() & libc::MAP_NORESERVE, + ) { + warn!("Failed to remap inflated memory at GPA 0x{:x}: {e}", base.0); + } + } } fn process_queue(&mut self, queue_index: u16) -> result::Result<(), Error> { @@ -598,7 +630,18 @@ impl BalloonEpollHandler { inflate_addresses.clear(); } } - DEFLATE_QUEUE => {} // Nothing to do on deflate + DEFLATE_QUEUE => { + let address = u64::from(pfn) << VIRTIO_BALLOON_PFN_SHIFT; + let chunk = address / COW_REMAP_SIZE; + if let Some(bits) = self.inflated_chunks.get_mut(&chunk) { + let page = + (address % COW_REMAP_SIZE) as usize / VIRTIO_BALLOON_PAGE_SIZE; + bits[page / 64] &= !(1 << (page % 64)); + if bits.iter().all(|word| *word == 0) { + self.inflated_chunks.remove(&chunk); + } + } + } _ => return Err(Error::InvalidQueueIndex(queue_index)), } } @@ -1158,6 +1201,7 @@ impl VirtioDevice for Balloon { pbp: None, access_platform: self.common.access_platform(), cow_restored: self.cow_restored, + inflated_chunks: HashMap::new(), }; let pause_control = Arc::clone(&self.common.pause_control); @@ -1273,6 +1317,7 @@ mod tests { pbp: None, access_platform: None, cow_restored: false, + inflated_chunks: HashMap::new(), } } @@ -1430,6 +1475,103 @@ mod tests { assert!(!vm_flags(host - 1).contains(" nh")); } + fn inflate_pfns( + handler: &mut BalloonEpollHandler, + memory: &GuestMemory, + pfns: impl Iterator, + ) { + let mut addresses: SmallVec<[GuestAddress; VIRTIO_BALLOON_PFNS_PER_BATCH]> = pfns + .map(|pfn| GuestAddress(pfn << VIRTIO_BALLOON_PFN_SHIFT)) + .collect(); + handler.process_inflate_addresses(memory, &mut addresses); + } + + #[test] + fn inflate_remaps_full_cow_chunk_across_batches() { + if get_page_size() as usize != VIRTIO_BALLOON_PAGE_SIZE { + return; + } + let chunk = COW_REMAP_SIZE; + let first = chunk >> VIRTIO_BALLOON_PFN_SHIFT; + let (memory, _backing_file) = cow_restored_memory(3 * chunk); + memory.write_obj(0xa5u8, GuestAddress(chunk)).unwrap(); + let mut handler = create_stats_handler( + &memory, + Queue::new(16).unwrap(), + BalloonStatsState::WaitingForInitialDescriptor, + ); + handler.cow_restored = true; + + inflate_pfns(&mut handler, &memory, first..first + 256); + assert_eq!(memory.read_obj::(GuestAddress(chunk)).unwrap(), 0x5a); + inflate_pfns(&mut handler, &memory, first + 256..first + 512); + + assert_eq!(memory.read_obj::(GuestAddress(chunk)).unwrap(), 0); + let host = memory.get_host_address(GuestAddress(chunk)).unwrap() as u64; + assert!(vm_flags(host).split_whitespace().any(|f| f == "nh")); + assert!(handler.inflated_chunks.is_empty()); + } + + #[test] + fn inflate_keeps_cow_chunk_with_deflated_page() { + const QUEUE_ADDRESS: GuestAddress = GuestAddress(0x1_0000); + const PFNS_ADDRESS: GuestAddress = GuestAddress(0x2_0000); + + if get_page_size() as usize != VIRTIO_BALLOON_PAGE_SIZE { + return; + } + let chunk = COW_REMAP_SIZE; + let first = chunk >> VIRTIO_BALLOON_PFN_SHIFT; + let (memory, _backing_file) = cow_restored_memory(3 * chunk); + let mut handler = create_stats_handler( + &memory, + Queue::new(16).unwrap(), + BalloonStatsState::WaitingForInitialDescriptor, + ); + handler.cow_restored = true; + inflate_pfns(&mut handler, &memory, first..first + 256); + + memory.write_obj(first as u32, PFNS_ADDRESS).unwrap(); + let deflate_queue = VirtQueue::new(QUEUE_ADDRESS, &memory, 16); + deflate_queue.dtable[0].set(PFNS_ADDRESS.0, size_of::() as u32, 0, 0); + deflate_queue.avail.ring[0].set(0); + deflate_queue.avail.idx.set(1); + handler.queues.push(deflate_queue.create_queue()); + handler.process_queue(DEFLATE_QUEUE).unwrap(); + memory.write_obj(0xa5u8, GuestAddress(chunk)).unwrap(); + + inflate_pfns(&mut handler, &memory, first + 256..first + 512); + + assert_eq!(memory.read_obj::(GuestAddress(chunk)).unwrap(), 0xa5); + assert_eq!( + memory.read_obj::(GuestAddress(chunk + 4096)).unwrap(), + 0x5a + ); + let host = memory.get_host_address(GuestAddress(chunk)).unwrap() as u64; + assert!(!vm_flags(host).split_whitespace().any(|f| f == "nh")); + assert_eq!(handler.inflated_chunks.len(), 1); + } + + #[test] + fn inflate_tracks_nothing_without_cow_restore() { + if get_page_size() as usize != VIRTIO_BALLOON_PAGE_SIZE { + return; + } + let chunk = COW_REMAP_SIZE; + let first = chunk >> VIRTIO_BALLOON_PFN_SHIFT; + let (memory, _backing_file) = cow_restored_memory(3 * chunk); + let mut handler = create_stats_handler( + &memory, + Queue::new(16).unwrap(), + BalloonStatsState::WaitingForInitialDescriptor, + ); + + inflate_pfns(&mut handler, &memory, first..first + 512); + + assert!(handler.inflated_chunks.is_empty()); + assert_eq!(memory.read_obj::(GuestAddress(chunk)).unwrap(), 0x5a); + } + #[test] fn process_inflate_addresses_releases_coalesced_ranges() { let page_size = get_page_size() as usize; From 9a3fdc6661a671bfe3249ba308c28be0642a2d70 Mon Sep 17 00:00:00 2001 From: CMGS Date: Tue, 10 Nov 2026 17:13:40 +0800 Subject: [PATCH 5/7] virtio-devices: balloon: Track only inflated chunks inside private guest RAM Signed-off-by: CMGS --- virtio-devices/src/balloon.rs | 42 ++++++++++++++++++++++++++++------- 1 file changed, 34 insertions(+), 8 deletions(-) diff --git a/virtio-devices/src/balloon.rs b/virtio-devices/src/balloon.rs index 5376121464..1c1c850e48 100644 --- a/virtio-devices/src/balloon.rs +++ b/virtio-devices/src/balloon.rs @@ -549,6 +549,15 @@ impl BalloonEpollHandler { } for address in addresses.iter() { let chunk = address.0 / COW_REMAP_SIZE; + let base = GuestAddress(chunk * COW_REMAP_SIZE); + let Some(region) = memory.find_region(base) else { + continue; + }; + if base.0 + COW_REMAP_SIZE > region.start_addr().0 + region.len() + || region.flags() & libc::MAP_SHARED != 0 + { + continue; + } let page = (address.0 % COW_REMAP_SIZE) as usize / VIRTIO_BALLOON_PAGE_SIZE; let bits = self.inflated_chunks.entry(chunk).or_default(); bits[page / 64] |= 1 << (page % 64); @@ -556,13 +565,6 @@ impl BalloonEpollHandler { continue; } self.inflated_chunks.remove(&chunk); - let base = GuestAddress(chunk * COW_REMAP_SIZE); - let Some(region) = memory.find_region(base) else { - continue; - }; - if base.0 + COW_REMAP_SIZE > region.start_addr().0 + region.len() { - continue; - } if let Err(e) = Self::remap_anonymous_range( memory, base, @@ -1566,12 +1568,36 @@ mod tests { BalloonStatsState::WaitingForInitialDescriptor, ); - inflate_pfns(&mut handler, &memory, first..first + 512); + inflate_pfns(&mut handler, &memory, first..first + 256); assert!(handler.inflated_chunks.is_empty()); assert_eq!(memory.read_obj::(GuestAddress(chunk)).unwrap(), 0x5a); } + #[test] + fn inflate_tracks_nothing_outside_guest_ram() { + if get_page_size() as usize != VIRTIO_BALLOON_PAGE_SIZE { + return; + } + let chunk = COW_REMAP_SIZE; + let pages_per_chunk = chunk >> VIRTIO_BALLOON_PFN_SHIFT; + let (memory, _backing_file) = cow_restored_memory(3 * chunk); + let mut handler = create_stats_handler( + &memory, + Queue::new(16).unwrap(), + BalloonStatsState::WaitingForInitialDescriptor, + ); + handler.cow_restored = true; + + inflate_pfns( + &mut handler, + &memory, + (4..260).map(|index| index * pages_per_chunk), + ); + + assert!(handler.inflated_chunks.is_empty()); + } + #[test] fn process_inflate_addresses_releases_coalesced_ranges() { let page_size = get_page_size() as usize; From b6457792b6b494b5f9df68894cb2d7f2d9515442 Mon Sep 17 00:00:00 2001 From: CMGS Date: Tue, 10 Nov 2026 19:03:23 +0800 Subject: [PATCH 6/7] virtio-devices: balloon: Simplify the copy-on-write release path Inflate ranges never exceed one 256 PFN batch, so they cannot hold an aligned 2 MiB chunk; pass false there and keep the partial page helper as upstream has it. Whole chunks filled by inflate are still remapped by the chunk bitmap, and free page reporting still remaps directly. Issue one MADV_DONTNEED when a range holds no full chunk instead of two adjacent ones, use advise_memory_range for the remap advice, and compare the chunk bitmap as an array. Fold the duplicate remap test into release_cow_restored_remaps_aligned_part. Signed-off-by: CMGS --- virtio-devices/src/balloon.rs | 84 +++++++++++------------------------ 1 file changed, 25 insertions(+), 59 deletions(-) diff --git a/virtio-devices/src/balloon.rs b/virtio-devices/src/balloon.rs index 1c1c850e48..b5ff69779e 100644 --- a/virtio-devices/src/balloon.rs +++ b/virtio-devices/src/balloon.rs @@ -402,6 +402,14 @@ impl BalloonEpollHandler { let end = start + len; let remap_start = start.next_multiple_of(COW_REMAP_SIZE).min(end); let remap_end = (end - end % COW_REMAP_SIZE).max(remap_start); + if remap_start == remap_end { + return Self::advise_memory_range( + memory, + range_base, + len as usize, + libc::MADV_DONTNEED, + ); + } for (base, part) in [(start, remap_start - start), (remap_end, end - remap_end)] { if part != 0 { Self::advise_memory_range( @@ -412,9 +420,6 @@ impl BalloonEpollHandler { )?; } } - if remap_start == remap_end { - return Ok(()); - } let remap_len = (remap_end - remap_start) as usize; let slice = memory @@ -445,12 +450,10 @@ impl BalloonEpollHandler { ); } for advice in [libc::MADV_DONTDUMP, libc::MADV_NOHUGEPAGE] { - // SAFETY: addr and remap_len name the mapping installed above. - if unsafe { libc::madvise(addr, remap_len, advice) } != 0 { - warn!( - "Failed to madvise remapped memory at GPA 0x{remap_start:x}: {}", - io::Error::last_os_error() - ); + if let Err(e) = + Self::advise_memory_range(memory, GuestAddress(remap_start), remap_len, advice) + { + warn!("Failed to madvise remapped memory at GPA 0x{remap_start:x}: {e}"); } } Ok(()) @@ -460,7 +463,6 @@ impl BalloonEpollHandler { pbp: &mut Option, memory: &GuestMemory, range_base: GuestAddress, - cow_restored: bool, ) -> result::Result<(), Error> { let page_size: u64 = get_page_size(); if pbp.is_none() { @@ -479,7 +481,7 @@ impl BalloonEpollHandler { memory, vm_memory::GuestAddress(pbp.as_ref().unwrap().addr), page_size as usize, - cow_restored, + false, )?; pbp.as_mut().unwrap().reset(); @@ -497,12 +499,9 @@ impl BalloonEpollHandler { // if the host page size differs from that. if get_page_size() as usize != VIRTIO_BALLOON_PAGE_SIZE { for &address in addresses.iter() { - if let Err(e) = Self::release_memory_range_partial_page( - &mut self.pbp, - memory, - address, - self.cow_restored, - ) { + if let Err(e) = + Self::release_memory_range_partial_page(&mut self.pbp, memory, address) + { warn!("Failed to release memory at address {:#x}: {e}", address.0); } } @@ -526,8 +525,7 @@ impl BalloonEpollHandler { if previous_address.checked_add(VIRTIO_BALLOON_PAGE_SIZE as u64) == Some(address) { range_len += VIRTIO_BALLOON_PAGE_SIZE; } else { - if let Err(e) = - Self::release_memory_range(memory, range_base, range_len, self.cow_restored) + if let Err(e) = Self::release_memory_range(memory, range_base, range_len, false) { warn!("Failed to release ballooned memory range: {e}"); } @@ -537,9 +535,7 @@ impl BalloonEpollHandler { previous_address = address; } - if let Err(e) = - Self::release_memory_range(memory, range_base, range_len, self.cow_restored) - { + if let Err(e) = Self::release_memory_range(memory, range_base, range_len, false) { warn!("Failed to release ballooned memory range: {e}"); } } @@ -561,7 +557,7 @@ impl BalloonEpollHandler { let page = (address.0 % COW_REMAP_SIZE) as usize / VIRTIO_BALLOON_PAGE_SIZE; let bits = self.inflated_chunks.entry(chunk).or_default(); bits[page / 64] |= 1 << (page % 64); - if bits.iter().any(|word| *word != u64::MAX) { + if *bits != [u64::MAX; COW_CHUNK_WORDS] { continue; } self.inflated_chunks.remove(&chunk); @@ -639,7 +635,7 @@ impl BalloonEpollHandler { let page = (address % COW_REMAP_SIZE) as usize / VIRTIO_BALLOON_PAGE_SIZE; bits[page / 64] &= !(1 << (page % 64)); - if bits.iter().all(|word| *word == 0) { + if *bits == [0; COW_CHUNK_WORDS] { self.inflated_chunks.remove(&chunk); } } @@ -1381,9 +1377,6 @@ mod tests { } fn cow_restored_memory(size: u64) -> (GuestMemory, TempFile) { - use std::io::Write; - use std::os::unix::io::AsRawFd; - let memory = GuestMemory::from_ranges(&[(GuestAddress(0), size as usize)]).unwrap(); let backing_file = TempFile::new().unwrap(); backing_file @@ -1406,7 +1399,7 @@ mod tests { (memory, backing_file) } - fn vm_flags(addr: u64) -> String { + fn has_vm_flag(addr: u64, flag: &str) -> bool { let smaps = fs::read_to_string("/proc/self/smaps").unwrap(); let mut inside = false; for line in smaps.lines() { @@ -1417,7 +1410,7 @@ mod tests { { inside = (start..end).contains(&addr); } else if inside && let Some(flags) = line.strip_prefix("VmFlags:") { - return flags.to_string(); + return flags.split_whitespace().any(|f| f == flag); } } panic!("no mapping at {addr:#x}"); @@ -1433,11 +1426,6 @@ mod tests { memory.write_obj(0xa5u8, GuestAddress(gpa)).unwrap(); } - BalloonEpollHandler::release_memory_range(&memory, GuestAddress(chunk), 4096, false) - .unwrap(); - assert_eq!(memory.read_obj::(GuestAddress(chunk)).unwrap(), 0x5a); - memory.write_obj(0xa5u8, GuestAddress(chunk)).unwrap(); - let len = (2 * chunk) as usize; BalloonEpollHandler::release_memory_range(&memory, GuestAddress(page), len, true).unwrap(); @@ -1453,28 +1441,10 @@ mod tests { assert_eq!(memory.read_obj::(GuestAddress(gpa)).unwrap(), value); } assert_eq!(fs::read(backing_file.as_path()).unwrap(), original); - } - - #[test] - fn release_cow_restored_aligned_range_is_anonymous() { - let chunk = COW_REMAP_SIZE; - let (memory, _backing_file) = cow_restored_memory(3 * chunk); - memory.write_obj(0xa5u8, GuestAddress(chunk)).unwrap(); - - BalloonEpollHandler::release_memory_range( - &memory, - GuestAddress(chunk), - chunk as usize, - true, - ) - .unwrap(); - - assert_eq!(memory.read_obj::(GuestAddress(chunk)).unwrap(), 0); let host = memory.get_host_address(GuestAddress(chunk)).unwrap() as u64; - let flags = vm_flags(host); - assert!(flags.split_whitespace().any(|f| f == "dd"), "{flags}"); - assert!(flags.split_whitespace().any(|f| f == "nh"), "{flags}"); - assert!(!vm_flags(host - 1).contains(" nh")); + assert!(has_vm_flag(host, "dd")); + assert!(has_vm_flag(host, "nh")); + assert!(!has_vm_flag(host - 1, "nh")); } fn inflate_pfns( @@ -1509,8 +1479,6 @@ mod tests { inflate_pfns(&mut handler, &memory, first + 256..first + 512); assert_eq!(memory.read_obj::(GuestAddress(chunk)).unwrap(), 0); - let host = memory.get_host_address(GuestAddress(chunk)).unwrap() as u64; - assert!(vm_flags(host).split_whitespace().any(|f| f == "nh")); assert!(handler.inflated_chunks.is_empty()); } @@ -1549,8 +1517,6 @@ mod tests { memory.read_obj::(GuestAddress(chunk + 4096)).unwrap(), 0x5a ); - let host = memory.get_host_address(GuestAddress(chunk)).unwrap() as u64; - assert!(!vm_flags(host).split_whitespace().any(|f| f == "nh")); assert_eq!(handler.inflated_chunks.len(), 1); } From e6221b94290dc120414f242643a632e6b6d95cfe Mon Sep 17 00:00:00 2001 From: CMGS Date: Tue, 10 Nov 2026 19:07:39 +0800 Subject: [PATCH 7/7] virtio-devices: balloon: Log the OS error when remap advice fails Error::MadviseFail displays only "Madvise fail.", so log it with Debug to keep the errno, as the other advise_memory_range callers do. Signed-off-by: CMGS --- virtio-devices/src/balloon.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/virtio-devices/src/balloon.rs b/virtio-devices/src/balloon.rs index b5ff69779e..dcf8aafea9 100644 --- a/virtio-devices/src/balloon.rs +++ b/virtio-devices/src/balloon.rs @@ -453,7 +453,7 @@ impl BalloonEpollHandler { if let Err(e) = Self::advise_memory_range(memory, GuestAddress(remap_start), remap_len, advice) { - warn!("Failed to madvise remapped memory at GPA 0x{remap_start:x}: {e}"); + warn!("Failed to madvise remapped memory at GPA 0x{remap_start:x}: {e:?}"); } } Ok(())