Skip to main content

squib_snapshot/
load.rs

1//! High-level load orchestrator.
2//!
3//! Symmetric to [`crate::save::save`]. Validates magic, version, and CRC; loads the
4//! state blob; verifies structural compatibility; surfaces the result to the caller
5//! who then drives `hv_gic_state_set_data` and the per-vCPU register restore.
6
7use std::{fs::File, io::BufReader, path::Path};
8
9use crate::{
10    envelope::{SNAPSHOT_VERSION, Snapshot},
11    error::{Result, SnapshotError},
12    state::MicrovmState,
13};
14
15/// What `load` returns: the state blob plus the version embedded in the file.
16#[derive(Debug, Clone, PartialEq)]
17pub struct LoadedSnapshot {
18    /// State blob.
19    pub state: MicrovmState,
20    /// Version this state file was produced by.
21    pub source_version: semver::Version,
22}
23
24/// Load and validate a state file.
25///
26/// # Errors
27/// Any [`SnapshotError`] from envelope decode + structural verification.
28pub fn load(state_path: &Path) -> Result<LoadedSnapshot> {
29    let file = File::open(state_path)?;
30    let mut reader = BufReader::new(file);
31    let snapshot = Snapshot::<MicrovmState>::load(&mut reader)?;
32    snapshot.data.verify_compatible()?;
33    Ok(LoadedSnapshot {
34        state: snapshot.data,
35        source_version: snapshot.header.version,
36    })
37}
38
39/// What `--describe-snapshot` prints. Captures the same fields a Firecracker user
40/// would expect:
41///
42/// - file path
43/// - magic + version
44/// - vCPU count
45/// - mem_size_mib
46/// - dirty-page tracking flag
47/// - whether the matching memory file looks present
48#[derive(Debug, Clone)]
49#[allow(clippy::struct_excessive_bools)] // each bool reports an independent condition
50pub struct SnapshotDescription {
51    /// Path to the state file.
52    pub state_path: std::path::PathBuf,
53    /// Magic value embedded in the file.
54    pub magic: u64,
55    /// Version embedded in the file.
56    pub version: semver::Version,
57    /// Squib's expected version (for the cross-version note).
58    pub expected_version: semver::Version,
59    /// Number of vCPUs the snapshot saved.
60    pub vcpu_count: usize,
61    /// Configured guest RAM size in MiB.
62    pub mem_size_mib: u64,
63    /// Whether dirty-page tracking was enabled at save time.
64    pub track_dirty_pages: bool,
65    /// Per-device summary: `(class, id, mmio_slot)`.
66    pub devices: Vec<(String, String, u32)>,
67    /// Whether MMDS state is present.
68    pub mmds_present: bool,
69    /// Best-effort memory-file path inferred by swapping `.snap`→`.mem`.
70    pub inferred_memory_path: Option<std::path::PathBuf>,
71    /// Whether that inferred memory file exists.
72    pub inferred_memory_exists: bool,
73    /// Inferred memory file's size in bytes (if present).
74    pub inferred_memory_size: Option<u64>,
75    /// CRC verified on load.
76    pub crc_ok: bool,
77}
78
79impl SnapshotDescription {
80    /// Convert to a `serde_json::Value` for machine consumption.
81    pub fn to_json(&self) -> serde_json::Value {
82        serde_json::json!({
83            "state_path": self.state_path.display().to_string(),
84            "magic": format!("{:#018x}", self.magic),
85            "version": self.version.to_string(),
86            "expected_version": self.expected_version.to_string(),
87            "vcpu_count": self.vcpu_count,
88            "mem_size_mib": self.mem_size_mib,
89            "track_dirty_pages": self.track_dirty_pages,
90            "devices": self
91                .devices
92                .iter()
93                .map(|(k, i, s)| serde_json::json!({"kind": k, "id": i, "mmio_slot": s}))
94                .collect::<Vec<_>>(),
95            "mmds_present": self.mmds_present,
96            "inferred_memory_path": self
97                .inferred_memory_path
98                .as_ref()
99                .map(|p| p.display().to_string()),
100            "inferred_memory_exists": self.inferred_memory_exists,
101            "inferred_memory_size": self.inferred_memory_size,
102            "crc_ok": self.crc_ok,
103        })
104    }
105
106    /// Format a human-readable summary suitable for `--describe-snapshot` stdout.
107    pub fn human(&self) -> String {
108        use std::fmt::Write;
109        let mut s = String::new();
110        let _ = writeln!(s, "snapshot: {}", self.state_path.display());
111        let _ = writeln!(s, "  magic:               {:#018x}", self.magic);
112        let _ = writeln!(s, "  version:             {}", self.version);
113        if self.version != self.expected_version {
114            let _ = writeln!(s, "  expected (squib):    {}", self.expected_version);
115        }
116        let _ = writeln!(s, "  vcpu_count:          {}", self.vcpu_count);
117        let _ = writeln!(s, "  mem_size_mib:        {}", self.mem_size_mib);
118        let _ = writeln!(s, "  track_dirty_pages:   {}", self.track_dirty_pages);
119        let _ = writeln!(s, "  devices:             {}", self.devices.len());
120        for (kind, id, slot) in &self.devices {
121            let _ = writeln!(s, "    - {kind}: {id} (slot {slot})");
122        }
123        let _ = writeln!(s, "  mmds_present:        {}", self.mmds_present);
124        if let Some(p) = &self.inferred_memory_path {
125            let _ = writeln!(
126                s,
127                "  memory file:         {} ({})",
128                p.display(),
129                if self.inferred_memory_exists {
130                    self.inferred_memory_size
131                        .map_or_else(|| "present".into(), |b| format!("{b} bytes"))
132                } else {
133                    "MISSING".into()
134                }
135            );
136        }
137        let _ = writeln!(
138            s,
139            "  crc_ok:              {}",
140            if self.crc_ok { "yes" } else { "NO" }
141        );
142        s
143    }
144}
145
146/// Build a description from a state file.
147///
148/// Always tries CRC-checked load first; on CRC failure, falls back to the
149/// no-CRC-check load so the operator can still see the version + structure of a
150/// corrupt file. Surfaces the CRC outcome via `crc_ok`.
151///
152/// # Errors
153/// [`SnapshotError`] for any decode failure that even the no-CRC path can't recover.
154pub fn describe(state_path: &Path) -> Result<SnapshotDescription> {
155    let bytes = std::fs::read(state_path)?;
156    let (snapshot, crc_ok) = match Snapshot::<MicrovmState>::load_from_slice(&bytes) {
157        Ok(snap) => (snap, true),
158        Err(SnapshotError::CrcMismatch) => {
159            // The body excluding the CRC trailer; subscribed to the same size limit.
160            if bytes.len() < 8 {
161                return Err(SnapshotError::TooShort);
162            }
163            let body = &bytes[..bytes.len() - 8];
164            (
165                Snapshot::<MicrovmState>::load_without_crc_check(body)?,
166                false,
167            )
168        }
169        Err(e) => return Err(e),
170    };
171
172    let inferred_memory_path = infer_memory_path(state_path);
173    let (inferred_memory_exists, inferred_memory_size) = match &inferred_memory_path {
174        Some(p) => match std::fs::metadata(p) {
175            Ok(m) => (true, Some(m.len())),
176            Err(_) => (false, None),
177        },
178        None => (false, None),
179    };
180
181    let devices = snapshot
182        .data
183        .device_states
184        .devices
185        .iter()
186        .map(|d| (d.kind.clone(), d.id.clone(), d.mmio_slot))
187        .collect();
188
189    Ok(SnapshotDescription {
190        state_path: state_path.to_path_buf(),
191        magic: snapshot.header.magic,
192        version: snapshot.header.version,
193        expected_version: SNAPSHOT_VERSION,
194        vcpu_count: snapshot.data.vcpu_states.len(),
195        mem_size_mib: snapshot.data.vm_info.mem_size_mib,
196        track_dirty_pages: snapshot.data.vm_info.track_dirty_pages,
197        devices,
198        mmds_present: snapshot.data.mmds_state.is_some(),
199        inferred_memory_path,
200        inferred_memory_exists,
201        inferred_memory_size,
202        crc_ok,
203    })
204}
205
206/// Best-effort `<state> → <state-with-mem-extension>` translation.
207///
208/// Swaps whatever extension the state file carries (`.snap`, `.snapshot`, …) to `.mem`.
209/// The operator can always pass `--memory-path` explicitly when their naming convention
210/// produces a hint that doesn't match an actual file on disk.
211///
212/// Returns `None` only when the state path has *no* extension at all (in which case there
213/// is no obvious translation rule to apply).
214fn infer_memory_path(state_path: &Path) -> Option<std::path::PathBuf> {
215    state_path.extension()?;
216    let mut p = state_path.to_path_buf();
217    p.set_extension("mem");
218    Some(p)
219}
220
221#[cfg(test)]
222mod tests {
223    use std::path::Path;
224
225    use tempfile::TempDir;
226
227    use super::*;
228    use crate::{
229        envelope::Snapshot,
230        memory::VecPageReader,
231        save::{SaveRequest, SnapshotKind, save},
232        state::{DeviceState, DeviceStates, GicState, MicrovmState, VcpuState, VmInfo},
233    };
234
235    fn build_state() -> MicrovmState {
236        MicrovmState {
237            vm_info: VmInfo {
238                mem_size_mib: 256,
239                smt: false,
240                cpu_template: String::new(),
241                kernel_image_path: "/k".into(),
242                initrd_path: None,
243                boot_args: String::new(),
244                track_dirty_pages: false,
245            },
246            vcpu_states: vec![VcpuState::new(0), VcpuState::new(1)],
247            device_states: DeviceStates::from_devices(vec![DeviceState {
248                kind: "virtio-block".into(),
249                id: "rootfs".into(),
250                mmio_slot: 0,
251                blob: vec![],
252            }]),
253            gic_state: GicState::from_bytes(vec![0xAA; 32]),
254            mmds_state: None,
255        }
256    }
257
258    fn dest_in(dir: &Path, name: &str) -> std::path::PathBuf {
259        dir.join(name)
260    }
261
262    fn save_pair(dir: &Path, name: &str) -> (std::path::PathBuf, std::path::PathBuf) {
263        let snap = dest_in(dir, &format!("{name}.snap"));
264        let mem = dest_in(dir, &format!("{name}.mem"));
265        let reader = VecPageReader::new(vec![0u8; 16 * 1024]);
266        save(SaveRequest {
267            state_path: &snap,
268            memory_path: &mem,
269            kind: SnapshotKind::Full,
270            state: build_state(),
271            memory: &reader,
272            ram_size: 16 * 1024,
273            memory_page_size: 16 * 1024,
274            dirty: None,
275        })
276        .unwrap();
277        (snap, mem)
278    }
279
280    #[test]
281    fn test_should_round_trip_save_then_load() {
282        let dir = TempDir::new().unwrap();
283        let (snap, _mem) = save_pair(dir.path(), "x");
284        let loaded = load(&snap).unwrap();
285        assert_eq!(loaded.state.vcpu_states.len(), 2);
286        assert_eq!(loaded.source_version, SNAPSHOT_VERSION);
287    }
288
289    #[test]
290    fn test_should_describe_state_file_in_human_form() {
291        let dir = TempDir::new().unwrap();
292        let (snap, _mem) = save_pair(dir.path(), "x");
293        let desc = describe(&snap).unwrap();
294        let h = desc.human();
295        assert!(h.contains("vcpu_count:          2"));
296        assert!(h.contains("mem_size_mib:        256"));
297        assert!(h.contains("crc_ok:              yes"));
298    }
299
300    #[test]
301    fn test_should_describe_state_file_in_json_form() {
302        let dir = TempDir::new().unwrap();
303        let (snap, _mem) = save_pair(dir.path(), "x");
304        let desc = describe(&snap).unwrap();
305        let j = desc.to_json();
306        assert_eq!(j["vcpu_count"], 2);
307        assert_eq!(j["mem_size_mib"], 256);
308        assert_eq!(j["crc_ok"], true);
309    }
310
311    #[test]
312    fn test_should_describe_corrupt_file_with_crc_warning() {
313        // Flip a byte in the trailing CRC so the body's magic + version still
314        // decode but the checksum fails — this is the case `--describe-snapshot`
315        // is designed to surface (operator gets the metadata, knows the file is
316        // damaged).
317        let dir = TempDir::new().unwrap();
318        let (snap, _mem) = save_pair(dir.path(), "x");
319        let mut bytes = std::fs::read(&snap).unwrap();
320        let last = bytes.len() - 1;
321        bytes[last] ^= 0x01;
322        std::fs::write(&snap, &bytes).unwrap();
323        let desc = describe(&snap).unwrap();
324        assert!(!desc.crc_ok);
325        assert!(desc.human().contains("crc_ok:              NO"));
326    }
327
328    #[test]
329    fn test_should_infer_memory_path_from_state_path() {
330        // Canonical `.snap` → `.mem` mapping.
331        assert_eq!(
332            infer_memory_path(Path::new("/tmp/x.snap")),
333            Some(std::path::PathBuf::from("/tmp/x.mem"))
334        );
335        // Operators with a non-default extension (`.snapshot`, `.bin`, …) get the
336        // matching `.mem` hint regardless of stem.
337        assert_eq!(
338            infer_memory_path(Path::new("/tmp/x.snapshot")),
339            Some(std::path::PathBuf::from("/tmp/x.mem"))
340        );
341        assert_eq!(
342            infer_memory_path(Path::new("/tmp/x.bin")),
343            Some(std::path::PathBuf::from("/tmp/x.mem"))
344        );
345        // Pathological case: no extension at all → no obvious translation rule,
346        // still returns None.
347        assert_eq!(infer_memory_path(Path::new("/tmp/x")), None);
348    }
349
350    #[test]
351    fn test_should_report_missing_memory_file() {
352        let dir = TempDir::new().unwrap();
353        let (snap, mem) = save_pair(dir.path(), "x");
354        std::fs::remove_file(&mem).unwrap();
355        let desc = describe(&snap).unwrap();
356        assert!(!desc.inferred_memory_exists);
357        assert!(desc.human().contains("MISSING"));
358    }
359
360    #[test]
361    fn test_should_reject_load_on_incompatible_state() {
362        let dir = TempDir::new().unwrap();
363        let snap = dest_in(dir.path(), "x.snap");
364        // Hand-craft an envelope with 0 vCPUs; will pass envelope decode but fail
365        // verify_compatible.
366        let mut bad = build_state();
367        bad.vcpu_states.clear();
368        let envelope = Snapshot::new(bad);
369        let mut buf = Vec::new();
370        envelope.save(&mut buf).unwrap();
371        std::fs::write(&snap, &buf).unwrap();
372        let res = load(&snap);
373        assert!(matches!(res, Err(SnapshotError::Incompatible)));
374    }
375
376    #[test]
377    fn test_should_surface_truncated_file_as_too_short() {
378        let dir = TempDir::new().unwrap();
379        let snap = dest_in(dir.path(), "x.snap");
380        std::fs::write(&snap, b"abc").unwrap();
381        let res = describe(&snap);
382        assert!(matches!(
383            res,
384            Err(SnapshotError::TooShort | SnapshotError::CrcMismatch)
385        ));
386    }
387}