Skip to main content

squib_snapshot/
save.rs

1//! High-level save / load orchestrator.
2//!
3//! Implements the producer flow from
4//! [16-snapshots.md § 2](../../../specs/16-snapshots.md#2-state-file):
5//!
6//! ```text
7//! 1. Open <id>.snap.tmp and <id>.mem.tmp (sibling of the destinations).
8//! 2. Encode MicrovmState with bitcode + CRC64 trailer (envelope::Snapshot::save).
9//! 3. Write the memory file (Full or sparse-of-dirty).
10//! 4. fsync(3) both temp files.
11//! 5. rename(2) <id>.snap.tmp → <id>.snap and <id>.mem.tmp → <id>.mem.
12//! 6. If either rename fails, unlink any partially-renamed file.
13//! ```
14//!
15//! This module owns the high-level transaction; the building blocks (envelope,
16//! atomic-writer, memory writer, dirty bitmap) are independently testable.
17
18use 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/// Snapshot kind requested by the operator.
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub enum SnapshotKind {
32    /// Full snapshot — entire memory dumped.
33    Full,
34    /// Diff snapshot — only dirty pages since the last clean checkpoint.
35    Diff,
36}
37
38/// Inputs to a save operation.
39#[derive(Debug)]
40pub struct SaveRequest<'a, R: PageReader> {
41    /// Destination path for the state file (`<id>.snap`).
42    pub state_path: &'a Path,
43    /// Destination path for the memory file (`<id>.mem`).
44    pub memory_path: &'a Path,
45    /// Snapshot type.
46    pub kind: SnapshotKind,
47    /// State blob (vCPUs, GIC, devices, MMDS).
48    pub state: MicrovmState,
49    /// Source for memory bytes.
50    pub memory: &'a R,
51    /// Logical RAM size.
52    pub ram_size: u64,
53    /// Memory-file page size (host page; 16 KiB on Apple Silicon).
54    pub memory_page_size: u64,
55    /// Dirty bitmap — required for `Diff`, ignored for `Full`.
56    pub dirty: Option<&'a DirtyBitmap>,
57}
58
59/// Save report — emitted after a successful save for tracing / metrics.
60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61pub struct SaveReport {
62    /// Snapshot kind that produced this save.
63    pub kind: SnapshotKind,
64    /// Number of pages written for a Diff snapshot (= 0 for Full, where every page
65    /// is written via the dense write path).
66    pub pages_written: u64,
67}
68
69/// Save the state + memory pair to disk atomically (D25).
70///
71/// The two-temp-file + two-rename pattern is sequential, not transactional across
72/// the pair: a crash between the two renames leaves the operator with the previous
73/// good pair plus one stranded temp file. The load path validates both files'
74/// magic+CRC and refuses to use a mismatched pair, so this remains safe per § 2.
75///
76/// # Errors
77/// [`SnapshotError`] for any step in the pipeline.
78pub 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    // Step 1 — write the state file (small; do this first so the memory write
98    // doesn't fight for fsync bandwidth before we know the state encodes).
99    let mut state_writer = AtomicWriter::open(state_path)?;
100    let envelope = Snapshot::new(state);
101    envelope.save(state_writer.file_mut())?;
102
103    // Step 2 — write the memory file. The Diff branch's bitmap is destructured
104    // via let-else so no `expect()` lives on a path reachable from the API
105    // (D25 / 93-improvements-review.md, Phase 5).
106    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    // Step 3 — commit both atomically.
123    //
124    // Order: state first, memory second. If the memory rename fails after the
125    // state rename succeeded, the operator sees a state file pointing at a
126    // memory file that doesn't exist — they re-take the snapshot. The previous
127    // good *memory* pair is still there; only the *state* pair was overwritten,
128    // and a state file without its memory peer is detectable on load (see
129    // `load::verify_pair`).
130    state_writer.commit()?;
131    mem_writer.commit()?;
132
133    Ok(SaveReport {
134        kind,
135        pages_written,
136    })
137}
138
139/// Re-export of the on-disk header struct for `--describe-snapshot`.
140pub 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(); // 0-vCPU state is "Incompatible"
275        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; // Incompatible.
302        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        // Original files untouched.
314        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}