1use std::path::Path;
19
20use crate::{
21 atomic::AtomicWriter,
22 dirty::DirtyBitmap,
23 envelope::Snapshot,
24 error::{Result, SnapshotError},
25 memory::{MemoryWriter, PageReader},
26 state::MicrovmState,
27};
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub enum SnapshotKind {
32 Full,
34 Diff,
36}
37
38#[derive(Debug)]
40pub struct SaveRequest<'a, R: PageReader> {
41 pub state_path: &'a Path,
43 pub memory_path: &'a Path,
45 pub kind: SnapshotKind,
47 pub state: MicrovmState,
49 pub memory: &'a R,
51 pub ram_size: u64,
53 pub memory_page_size: u64,
55 pub dirty: Option<&'a DirtyBitmap>,
57}
58
59#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61pub struct SaveReport {
62 pub kind: SnapshotKind,
64 pub pages_written: u64,
67}
68
69pub fn save<R: PageReader>(req: SaveRequest<'_, R>) -> Result<SaveReport> {
79 let SaveRequest {
80 state_path,
81 memory_path,
82 kind,
83 state,
84 memory,
85 ram_size,
86 memory_page_size,
87 dirty,
88 } = req;
89
90 if matches!(kind, SnapshotKind::Diff) && !state.vm_info.track_dirty_pages {
91 return Err(SnapshotError::InvalidPath(
92 "Diff snapshot requested but vm_info.track_dirty_pages is false".into(),
93 ));
94 }
95 state.verify_compatible()?;
96
97 let mut state_writer = AtomicWriter::open(state_path)?;
100 let envelope = Snapshot::new(state);
101 envelope.save(state_writer.file_mut())?;
102
103 let mut mem_writer = MemoryWriter::open(memory_path, ram_size, memory_page_size)?;
107 let pages_written = match kind {
108 SnapshotKind::Full => {
109 mem_writer.write_full(memory)?;
110 0
111 }
112 SnapshotKind::Diff => {
113 let Some(bitmap) = dirty else {
114 return Err(SnapshotError::InvalidPath(
115 "Diff snapshot requires track_dirty_pages=true and a dirty bitmap".into(),
116 ));
117 };
118 mem_writer.write_diff(memory, bitmap)?
119 }
120 };
121
122 state_writer.commit()?;
131 mem_writer.commit()?;
132
133 Ok(SaveReport {
134 kind,
135 pages_written,
136 })
137}
138
139pub use crate::envelope::SnapshotHdr as Header;
141
142#[cfg(test)]
143mod tests {
144 use std::path::Path;
145
146 use tempfile::TempDir;
147
148 use super::*;
149 use crate::{
150 memory::VecPageReader,
151 state::{GicState, MicrovmState, VcpuState, VmInfo},
152 };
153
154 fn build_state() -> MicrovmState {
155 MicrovmState {
156 vm_info: VmInfo {
157 mem_size_mib: 256,
158 smt: false,
159 cpu_template: "V1N1".into(),
160 kernel_image_path: "/tmp/vmlinux".into(),
161 initrd_path: None,
162 boot_args: "console=ttyAMA0 panic=1".into(),
163 track_dirty_pages: false,
164 },
165 vcpu_states: vec![VcpuState::new(0)],
166 device_states: crate::state::DeviceStates::default(),
167 gic_state: GicState::from_bytes(vec![1, 2, 3, 4, 5, 6, 7, 8]),
168 mmds_state: None,
169 }
170 }
171
172 fn dest_in(dir: &Path, name: &str) -> std::path::PathBuf {
173 dir.join(name)
174 }
175
176 #[test]
177 fn test_should_save_full_snapshot_pair_atomically() {
178 let dir = TempDir::new().unwrap();
179 let snap = dest_in(dir.path(), "x.snap");
180 let mem = dest_in(dir.path(), "x.mem");
181 let ram_size: u64 = 32 * 1024;
182 let reader = VecPageReader::new(vec![7u8; ram_size as usize]);
183 let report = save(SaveRequest {
184 state_path: &snap,
185 memory_path: &mem,
186 kind: SnapshotKind::Full,
187 state: build_state(),
188 memory: &reader,
189 ram_size,
190 memory_page_size: 16 * 1024,
191 dirty: None,
192 })
193 .unwrap();
194 assert_eq!(report.kind, SnapshotKind::Full);
195 assert!(snap.exists());
196 assert!(mem.exists());
197 assert_eq!(std::fs::metadata(&mem).unwrap().len(), ram_size);
198 }
199
200 #[test]
201 fn test_should_reject_diff_without_dirty_bitmap() {
202 let dir = TempDir::new().unwrap();
203 let snap = dest_in(dir.path(), "x.snap");
204 let mem = dest_in(dir.path(), "x.mem");
205 let mut state = build_state();
206 state.vm_info.track_dirty_pages = true;
207 let reader = VecPageReader::new(vec![0u8; 32 * 1024]);
208 let res = save(SaveRequest {
209 state_path: &snap,
210 memory_path: &mem,
211 kind: SnapshotKind::Diff,
212 state,
213 memory: &reader,
214 ram_size: 32 * 1024,
215 memory_page_size: 16 * 1024,
216 dirty: None,
217 });
218 assert!(matches!(res, Err(SnapshotError::InvalidPath(_))));
219 }
220
221 #[test]
222 fn test_should_reject_diff_when_track_dirty_is_false() {
223 let dir = TempDir::new().unwrap();
224 let snap = dest_in(dir.path(), "x.snap");
225 let mem = dest_in(dir.path(), "x.mem");
226 let bm = DirtyBitmap::new(0, 32 * 1024, 16 * 1024).unwrap();
227 let reader = VecPageReader::new(vec![0u8; 32 * 1024]);
228 let res = save(SaveRequest {
229 state_path: &snap,
230 memory_path: &mem,
231 kind: SnapshotKind::Diff,
232 state: build_state(),
233 memory: &reader,
234 ram_size: 32 * 1024,
235 memory_page_size: 16 * 1024,
236 dirty: Some(&bm),
237 });
238 assert!(matches!(res, Err(SnapshotError::InvalidPath(_))));
239 }
240
241 #[test]
242 fn test_should_save_diff_only_dirty_pages() {
243 let dir = TempDir::new().unwrap();
244 let snap = dest_in(dir.path(), "x.snap");
245 let mem = dest_in(dir.path(), "x.mem");
246 let mut state = build_state();
247 state.vm_info.track_dirty_pages = true;
248 let bm = DirtyBitmap::new(0, 32 * 1024, 16 * 1024).unwrap();
249 bm.set_dirty_by_index(1);
250 let reader = VecPageReader::new(vec![9u8; 32 * 1024]);
251 let report = save(SaveRequest {
252 state_path: &snap,
253 memory_path: &mem,
254 kind: SnapshotKind::Diff,
255 state,
256 memory: &reader,
257 ram_size: 32 * 1024,
258 memory_page_size: 16 * 1024,
259 dirty: Some(&bm),
260 })
261 .unwrap();
262 assert_eq!(report.pages_written, 1);
263 let buf = std::fs::read(&mem).unwrap();
264 assert!(buf[..16 * 1024].iter().all(|&b| b == 0));
265 assert!(buf[16 * 1024..32 * 1024].iter().all(|&b| b == 9));
266 }
267
268 #[test]
269 fn test_should_reject_save_when_state_is_incompatible() {
270 let dir = TempDir::new().unwrap();
271 let snap = dest_in(dir.path(), "x.snap");
272 let mem = dest_in(dir.path(), "x.mem");
273 let mut state = build_state();
274 state.vcpu_states.clear(); let reader = VecPageReader::new(vec![0u8; 32 * 1024]);
276 let res = save(SaveRequest {
277 state_path: &snap,
278 memory_path: &mem,
279 kind: SnapshotKind::Full,
280 state,
281 memory: &reader,
282 ram_size: 32 * 1024,
283 memory_page_size: 16 * 1024,
284 dirty: None,
285 });
286 assert!(matches!(res, Err(SnapshotError::Incompatible)));
287 assert!(
288 !snap.exists(),
289 "incompatible state must not stage temp file"
290 );
291 }
292
293 #[test]
294 fn test_should_keep_existing_pair_when_save_fails_during_state_validation() {
295 let dir = TempDir::new().unwrap();
296 let snap = dest_in(dir.path(), "x.snap");
297 let mem = dest_in(dir.path(), "x.mem");
298 std::fs::write(&snap, b"prior good state").unwrap();
299 std::fs::write(&mem, b"prior good mem").unwrap();
300 let mut state = build_state();
301 state.vm_info.smt = true; let reader = VecPageReader::new(vec![0u8; 32 * 1024]);
303 let _ = save(SaveRequest {
304 state_path: &snap,
305 memory_path: &mem,
306 kind: SnapshotKind::Full,
307 state,
308 memory: &reader,
309 ram_size: 32 * 1024,
310 memory_page_size: 16 * 1024,
311 dirty: None,
312 });
313 assert_eq!(std::fs::read_to_string(&snap).unwrap(), "prior good state");
315 assert_eq!(std::fs::read_to_string(&mem).unwrap(), "prior good mem");
316 }
317}