From 28b6b5d46743803e4c3bf2583bdf6932720a0bee Mon Sep 17 00:00:00 2001 From: Sebastien Boeuf Date: Wed, 10 Jun 2026 02:18:30 -0700 Subject: [PATCH] offload_daemon: Implement sparse snapshot/restore Copy only populated extents when writing the snapshot file and when filling the restore memfd, leaving unwritten ranges as holes. Both the on-disk snapshot and the restored guest RAM stay sparse, so that untouched guest pages cost no disk space or host memory. This brings the offload daemon closer to be at feature parity with CH's internal implementation of snapshot/restore. The only missing piece is on-demand paging at this point. Signed-off-by: Sebastien Boeuf Assisted-by: Claude:claude-opus-4-7 --- offload_daemon/src/main.rs | 45 ++++++++------------------------------ 1 file changed, 9 insertions(+), 36 deletions(-) diff --git a/offload_daemon/src/main.rs b/offload_daemon/src/main.rs index 5ca8ad2ce..66950db6a 100644 --- a/offload_daemon/src/main.rs +++ b/offload_daemon/src/main.rs @@ -17,7 +17,7 @@ use std::ffi::{CString, NulError}; use std::fs::{self, File, OpenOptions}; -use std::io::{self, Read, Seek, SeekFrom, Write}; +use std::io::{self, Read, Write}; use std::os::fd::{AsRawFd, FromRawFd}; use std::os::unix::net::{UnixListener, UnixStream}; use std::path::{Path, PathBuf}; @@ -30,6 +30,7 @@ use vm_migration::MigratableError; use vm_migration::protocol::{Command, Request, Response, Status}; use vmm::VmMigrationConfig; use vmm::migration::SNAPSHOT_STATE_FILE; +use vmm::sparse::copy_region; use vmm_sys_util::errno; use vmm_sys_util::sock_ctrl_msg::ScmSocket; @@ -308,32 +309,15 @@ fn dump_memory_slots( } fn dump_fd_to_path(file: &File, src_offset: u64, size: u64, path: &Path) -> Result<()> { - let mut out = OpenOptions::new() + let out = OpenOptions::new() .create(true) .truncate(true) .write(true) .open(path) .map_err(Error::WriteFile)?; out.set_len(size).map_err(Error::WriteFile)?; - // dup so our own offset doesn't disturb CH's view of the shared fd. - // SAFETY: dup() has no preconditions; result is checked. - let raw = unsafe { libc::dup(file.as_raw_fd()) }; - if raw < 0 { - return Err(Error::CopyMemory(io::Error::last_os_error())); - } - // SAFETY: `raw` is a fresh fd we now own. - let mut src = unsafe { File::from_raw_fd(raw) }; - src.seek(SeekFrom::Start(src_offset)) - .map_err(Error::CopyMemory)?; - let mut remaining = size; - let mut buf = vec![0u8; 1 << 20]; - while remaining > 0 { - let want = (remaining as usize).min(buf.len()); - src.read_exact(&mut buf[..want]) - .map_err(Error::CopyMemory)?; - out.write_all(&buf[..want]).map_err(Error::CopyMemory)?; - remaining -= want as u64; - } + // Keep `out` sparse: holes are left as holes (read back as zero). + copy_region(file, src_offset, &out, 0, size).map_err(Error::CopyMemory)?; out.sync_all().map_err(Error::WriteFile)?; Ok(()) } @@ -479,21 +463,10 @@ fn create_memfd_with_contents( name: &str, ) -> Result { // Size the memfd to cover the range CH maps at `file_offset`. - let mut memfd = create_empty_memfd(file_offset + size, name)?; - memfd - .seek(SeekFrom::Start(file_offset)) - .map_err(Error::CopyMemory)?; - let mut src = File::open(src_path).map_err(Error::ReadFile)?; - let mut remaining = size; - let mut buf = vec![0u8; 1 << 20]; - while remaining > 0 { - let want = (remaining as usize).min(buf.len()); - src.read_exact(&mut buf[..want]) - .map_err(Error::CopyMemory)?; - memfd.write_all(&buf[..want]).map_err(Error::CopyMemory)?; - remaining -= want as u64; - } - memfd.seek(SeekFrom::Start(0)).map_err(Error::CopyMemory)?; + let memfd = create_empty_memfd(file_offset + size, name)?; + let src = File::open(src_path).map_err(Error::ReadFile)?; + // Copy sparsely so the memfd keeps the snapshot's holes. + copy_region(&src, 0, &memfd, file_offset, size).map_err(Error::CopyMemory)?; Ok(memfd) }