openvmm_helpers/
snapshot.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! Snapshot manifest types and I/O functions for saving/restoring VM snapshots.
5
6use anyhow::Context;
7use mesh::payload::Protobuf;
8use mesh::payload::Timestamp;
9use std::path::Path;
10
11/// Current manifest format version. Bump when making incompatible changes.
12pub const MANIFEST_VERSION: u32 = 1;
13
14/// Manifest describing a VM snapshot.
15#[derive(Clone, Protobuf)]
16#[mesh(package = "openvmm.snapshot")]
17pub struct SnapshotManifest {
18    /// Manifest format version.
19    #[mesh(1)]
20    pub version: u32,
21    /// When the snapshot was created.
22    #[mesh(2)]
23    pub created_at: Timestamp,
24    /// OpenVMM version that created the snapshot.
25    #[mesh(3)]
26    pub openvmm_version: String,
27    /// Guest RAM size in bytes.
28    #[mesh(4)]
29    pub memory_size_bytes: u64,
30    /// Number of virtual processors.
31    #[mesh(5)]
32    pub vp_count: u32,
33    /// Page size in bytes.
34    #[mesh(6)]
35    pub page_size: u32,
36    /// Architecture string ("x86_64" or "aarch64").
37    #[mesh(7)]
38    pub architecture: String,
39}
40
41/// Write a snapshot to the given directory.
42///
43/// The directory is created if it does not exist. The snapshot consists of:
44/// - `manifest.bin` — protobuf-encoded [`SnapshotManifest`]
45/// - `state.bin` — raw device saved-state bytes
46/// - `memory.bin` — hard link to the memory backing file
47pub fn write_snapshot(
48    dir: &Path,
49    manifest: &SnapshotManifest,
50    saved_state_bytes: &[u8],
51    memory_file_path: &Path,
52) -> anyhow::Result<()> {
53    fs_err::create_dir_all(dir)?;
54
55    // Write manifest.
56    let manifest_bytes = mesh::payload::encode(manifest.clone());
57    fs_err::write(dir.join("manifest.bin"), &manifest_bytes)?;
58
59    // Write device state.
60    fs_err::write(dir.join("state.bin"), saved_state_bytes)?;
61
62    // Handle memory.bin: hard-link from the backing file.
63    let memory_bin_path = dir.join("memory.bin");
64    let canonical_source = fs_err::canonicalize(memory_file_path)?;
65
66    // Check whether source and target are already the same file (e.g.,
67    // the user pointed --memory-backing-file at <dir>/memory.bin directly).
68    let needs_link = if memory_bin_path.exists() {
69        let canonical_target = fs_err::canonicalize(&memory_bin_path)?;
70        if canonical_source == canonical_target {
71            false
72        } else {
73            // Different file at the target path — remove it so the hard
74            // link can be created.
75            fs_err::remove_file(&memory_bin_path)?;
76            true
77        }
78    } else {
79        true
80    };
81
82    if needs_link {
83        if let Err(err) = std::fs::hard_link(&canonical_source, &memory_bin_path) {
84            if err.kind() == std::io::ErrorKind::CrossesDevices {
85                anyhow::bail!(
86                    "memory backing file ({}) must be on the same filesystem as the snapshot \
87                     directory ({}); consider placing the backing file inside the snapshot \
88                     directory",
89                    memory_file_path.display(),
90                    dir.display(),
91                );
92            }
93            return Err(err).with_context(|| {
94                format!(
95                    "failed to hard-link {} -> {}",
96                    canonical_source.display(),
97                    memory_bin_path.display()
98                )
99            });
100        }
101    }
102
103    Ok(())
104}
105
106/// Read a snapshot from the given directory.
107///
108/// Returns the decoded manifest and the raw saved-state bytes.
109/// The caller is responsible for opening `memory.bin` separately.
110pub fn read_snapshot(dir: &Path) -> anyhow::Result<(SnapshotManifest, Vec<u8>)> {
111    let manifest_bytes =
112        fs_err::read(dir.join("manifest.bin")).context("failed to read manifest.bin")?;
113    let manifest: SnapshotManifest =
114        mesh::payload::decode(&manifest_bytes).context("failed to decode snapshot manifest")?;
115
116    let state_bytes = fs_err::read(dir.join("state.bin")).context("failed to read state.bin")?;
117
118    Ok((manifest, state_bytes))
119}
120
121/// Validate that a snapshot manifest is compatible with the running VM config.
122///
123/// Checks version, architecture, memory size, VP count, and page size.
124/// Returns `Ok(())` if the manifest matches, or an error describing the
125/// first mismatch found.
126pub fn validate_manifest(
127    manifest: &SnapshotManifest,
128    expected_arch: &str,
129    expected_memory_size: u64,
130    expected_vp_count: u32,
131    expected_page_size: u32,
132) -> anyhow::Result<()> {
133    if manifest.version != MANIFEST_VERSION {
134        anyhow::bail!(
135            "snapshot manifest version {} is not supported (expected {})",
136            manifest.version,
137            MANIFEST_VERSION,
138        );
139    }
140
141    if manifest.architecture != expected_arch {
142        anyhow::bail!(
143            "snapshot architecture '{}' doesn't match expected '{}'",
144            manifest.architecture,
145            expected_arch,
146        );
147    }
148
149    if manifest.memory_size_bytes != expected_memory_size {
150        anyhow::bail!(
151            "snapshot memory size ({} bytes) doesn't match expected ({} bytes)",
152            manifest.memory_size_bytes,
153            expected_memory_size,
154        );
155    }
156
157    if manifest.vp_count != expected_vp_count {
158        anyhow::bail!(
159            "snapshot VP count ({}) doesn't match expected ({})",
160            manifest.vp_count,
161            expected_vp_count,
162        );
163    }
164
165    if manifest.page_size != expected_page_size {
166        anyhow::bail!(
167            "snapshot page size ({}) doesn't match expected ({})",
168            manifest.page_size,
169            expected_page_size,
170        );
171    }
172
173    Ok(())
174}
175
176#[cfg(test)]
177mod tests {
178    use super::*;
179
180    /// Helper: build a test manifest with sensible defaults.
181    fn test_manifest() -> SnapshotManifest {
182        SnapshotManifest {
183            version: MANIFEST_VERSION,
184            created_at: Timestamp {
185                seconds: 1234567890,
186                nanos: 0,
187            },
188            openvmm_version: "test-0.1.0".to_string(),
189            memory_size_bytes: 1024,
190            vp_count: 2,
191            page_size: 4096,
192            architecture: "x86_64".to_string(),
193        }
194    }
195
196    #[test]
197    fn write_read_roundtrip() {
198        let dir = tempfile::tempdir().unwrap();
199        let snap_dir = dir.path().join("snap");
200
201        // Create a fake memory backing file in the same directory (same fs).
202        let mem_path = dir.path().join("memory.bin");
203        std::fs::write(&mem_path, b"FAKEMEM").unwrap();
204
205        let manifest = test_manifest();
206        let state = b"saved-state-data";
207
208        write_snapshot(&snap_dir, &manifest, state, &mem_path).unwrap();
209
210        let (read_manifest, read_state) = read_snapshot(&snap_dir).unwrap();
211        assert_eq!(read_manifest.version, manifest.version);
212        assert_eq!(read_manifest.memory_size_bytes, manifest.memory_size_bytes);
213        assert_eq!(read_manifest.vp_count, manifest.vp_count);
214        assert_eq!(read_manifest.architecture, manifest.architecture);
215        assert_eq!(read_state, state);
216
217        // memory.bin should exist in the snapshot directory.
218        assert!(snap_dir.join("memory.bin").exists());
219    }
220
221    #[test]
222    fn write_snapshot_creates_dir() {
223        let dir = tempfile::tempdir().unwrap();
224        let snap_dir = dir.path().join("a").join("b").join("c");
225
226        let mem_path = dir.path().join("memory.bin");
227        std::fs::write(&mem_path, b"MEM").unwrap();
228
229        write_snapshot(&snap_dir, &test_manifest(), b"state", &mem_path).unwrap();
230
231        assert!(snap_dir.join("manifest.bin").exists());
232        assert!(snap_dir.join("state.bin").exists());
233        assert!(snap_dir.join("memory.bin").exists());
234    }
235
236    #[test]
237    fn write_snapshot_same_memory_path() {
238        // When the memory backing file IS <snap_dir>/memory.bin, the function
239        // should detect the collision and skip the hard-link.
240        let dir = tempfile::tempdir().unwrap();
241        let snap_dir = dir.path().join("snap");
242        std::fs::create_dir_all(&snap_dir).unwrap();
243
244        let mem_path = snap_dir.join("memory.bin");
245        std::fs::write(&mem_path, b"SAMEFILE").unwrap();
246
247        // Should succeed without error.
248        write_snapshot(&snap_dir, &test_manifest(), b"state", &mem_path).unwrap();
249
250        // The file content should be unchanged.
251        assert_eq!(std::fs::read(&mem_path).unwrap(), b"SAMEFILE");
252    }
253
254    #[test]
255    fn read_snapshot_missing_file() {
256        let dir = tempfile::tempdir().unwrap();
257        // No files written — read should fail.
258        let result = read_snapshot(dir.path());
259        assert!(result.is_err());
260    }
261
262    #[test]
263    fn validate_manifest_ok() {
264        let manifest = test_manifest();
265        validate_manifest(&manifest, "x86_64", 1024, 2, 4096).unwrap();
266    }
267
268    #[test]
269    fn validate_manifest_wrong_arch() {
270        let manifest = test_manifest();
271        let err = validate_manifest(&manifest, "aarch64", 1024, 2, 4096).unwrap_err();
272        assert!(
273            err.to_string().contains("architecture"),
274            "unexpected error: {err}"
275        );
276    }
277
278    #[test]
279    fn validate_manifest_wrong_memory_size() {
280        let manifest = test_manifest();
281        let err = validate_manifest(&manifest, "x86_64", 9999, 2, 4096).unwrap_err();
282        assert!(
283            err.to_string().contains("memory size"),
284            "unexpected error: {err}"
285        );
286    }
287
288    #[test]
289    fn validate_manifest_wrong_vp_count() {
290        let manifest = test_manifest();
291        let err = validate_manifest(&manifest, "x86_64", 1024, 99, 4096).unwrap_err();
292        assert!(
293            err.to_string().contains("VP count"),
294            "unexpected error: {err}"
295        );
296    }
297
298    #[test]
299    fn validate_manifest_wrong_page_size() {
300        let manifest = test_manifest();
301        let err = validate_manifest(&manifest, "x86_64", 1024, 2, 65536).unwrap_err();
302        assert!(
303            err.to_string().contains("page size"),
304            "unexpected error: {err}"
305        );
306    }
307
308    #[test]
309    fn validate_manifest_wrong_version() {
310        let mut manifest = test_manifest();
311        manifest.version = 999;
312        let err = validate_manifest(&manifest, "x86_64", 1024, 2, 4096).unwrap_err();
313        assert!(
314            err.to_string().contains("version"),
315            "unexpected error: {err}"
316        );
317    }
318}