Skip to main content

squib_snapshot/
error.rs

1//! `SnapshotError` — wire-stable error variants for the snapshot subsystem.
2//!
3//! Per [11-runtime-core.md § 6](../../../specs/11-runtime-core.md#6-error-types) the
4//! variants map 1:1 to the `fault_message` strings the API server emits on
5//! `PUT /snapshot/create` / `PUT /snapshot/load` failures. Renaming a variant is a
6//! compat-suite golden change (I-RC-8 in 11 § 7), so the variants and their `Display`
7//! shapes are the public contract.
8//!
9//! The variant set in this enum is **richer** than the spec's exemplar — it also
10//! carries the per-step failure modes the implementation needs (`InvalidPath`,
11//! `MemoryWrite`, `Bitcode`). Each is mapped through to a single wire-shape category
12//! by [`SnapshotError::wire_message`] so the public surface stays stable.
13
14use std::path::PathBuf;
15
16use semver::Version;
17use thiserror::Error;
18
19/// Errors produced by the snapshot subsystem.
20///
21/// Variants and their `Display` shapes are wire-stable per I-RC-8: the API layer
22/// surfaces `to_string()` verbatim into the `fault_message` body. Renaming a variant
23/// or its message is a compat-suite golden change.
24#[derive(Debug, Error)]
25#[non_exhaustive]
26pub enum SnapshotError {
27    /// A vCPU did not acknowledge the quiesce request within the timeout.
28    ///
29    /// Surfaces as `503 Service Unavailable` on `PUT /snapshot/create`. The save
30    /// is aborted; the previous on-disk pair (if any) is untouched.
31    #[error("a vCPU did not ack quiesce within the timeout")]
32    QuiesceTimeout,
33
34    /// Snapshot magic mismatch — the file is not a squib-/Firecracker-compatible
35    /// state file.
36    #[error("snapshot magic mismatch (file: {found:#x}, expected: {expected:#x})")]
37    MagicMismatch {
38        /// Magic value read from the file header.
39        found: u64,
40        /// Magic value squib expected (architecture-specific).
41        expected: u64,
42    },
43
44    /// Snapshot version is not loadable by this squib build.
45    ///
46    /// The compat rule mirrors upstream: `major` must match exactly; `minor` must
47    /// be ≤ ours; `patch` is unrestricted.
48    #[error("snapshot version {found} is incompatible with squib's {expected}")]
49    VersionMismatch {
50        /// Version embedded in the file.
51        found: Version,
52        /// Version this squib build emits.
53        expected: Version,
54    },
55
56    /// CRC64 of the file body does not match its trailing checksum.
57    #[error("snapshot CRC64 mismatch")]
58    CrcMismatch,
59
60    /// The file is shorter than the trailing 8-byte CRC.
61    #[error("snapshot file is too short to contain a CRC trailer")]
62    TooShort,
63
64    /// Snapshot deserializes to a structurally compatible state, but the contents
65    /// (sysreg subset, GIC blob shape) are from a different VMM.
66    #[error("snapshot is from a different VMM (sysreg or GIC blob shape mismatch)")]
67    Incompatible,
68
69    /// Atomic-commit failed: the temp file wrote successfully but `rename(2)` did
70    /// not complete. The previous destination pair (if any) is left untouched.
71    #[error("atomic commit (rename) failed: {0}")]
72    AtomicCommitFailed(#[source] std::io::Error),
73
74    /// The user-supplied destination path and the temp-file directory live on
75    /// different filesystems, so `rename(2)` could not be atomic.
76    ///
77    /// Pre-flight check; surfaces *before* any data is written. The remediation is
78    /// for the operator to point the snapshot at a path on the same filesystem as
79    /// the temp directory (or vice-versa).
80    #[error(
81        "snapshot temp-file path is on a different filesystem from the destination \
82         (dest={dest:?}, temp_dir={temp_dir:?}); rename(2) cannot be atomic across mounts"
83    )]
84    AtomicCommitCrossFs {
85        /// User-supplied destination path.
86        dest: PathBuf,
87        /// Directory in which the temp file would have been created.
88        temp_dir: PathBuf,
89    },
90
91    /// Operator handed the API a path that did not pass boundary validation
92    /// (NUL byte, oversized, traversal).
93    #[error("invalid snapshot path: {0}")]
94    InvalidPath(String),
95
96    /// `bitcode` failed to encode or decode the snapshot envelope.
97    ///
98    /// `Display` matches [`Self::wire_message`] so the API server's
99    /// `fault_message` body is byte-equal to the rendered `to_string()`.
100    #[error("snapshot encoding error: {0}")]
101    Bitcode(String),
102
103    /// The file is larger than the squib deserialization size limit.
104    #[error("snapshot exceeds {limit} byte deserialization limit")]
105    SizeLimitExceeded {
106        /// The configured limit.
107        limit: usize,
108    },
109
110    /// Memory file write failed (sparse pwrite, full dump, or fsync).
111    #[error("memory file I/O error: {0}")]
112    MemoryIo(#[source] std::io::Error),
113
114    /// Generic I/O error during state file read/write or fsync.
115    #[error("snapshot I/O error: {0}")]
116    Io(#[source] std::io::Error),
117
118    /// A host-side capture or restore step failed (HVF call returned an error,
119    /// MMDS handle was poisoned, etc.). The string is the underlying cause for
120    /// the `fault_message`; the variant exists to keep host-FFI failures out
121    /// of `Bitcode` and `Io` (both of which connote different remediations).
122    #[error("snapshot capture/restore failure: {0}")]
123    Capture(String),
124}
125
126impl SnapshotError {
127    /// The exact `fault_message` body string this error surfaces to the API.
128    ///
129    /// Stable per I-RC-8 — renaming a variant or its `Display` shape is a
130    /// compat-suite golden change. Single-source-of-truth: this delegates to
131    /// `Display`, so the two cannot drift.
132    #[must_use]
133    pub fn wire_message(&self) -> String {
134        self.to_string()
135    }
136}
137
138impl From<bitcode::Error> for SnapshotError {
139    fn from(err: bitcode::Error) -> Self {
140        Self::Bitcode(err.to_string())
141    }
142}
143
144impl From<std::io::Error> for SnapshotError {
145    fn from(err: std::io::Error) -> Self {
146        Self::Io(err)
147    }
148}
149
150/// Result alias used throughout `squib-snapshot`.
151pub type Result<T, E = SnapshotError> = core::result::Result<T, E>;
152
153#[cfg(test)]
154mod tests {
155    use super::*;
156
157    #[test]
158    fn test_should_render_quiesce_timeout_with_stable_message() {
159        let err = SnapshotError::QuiesceTimeout;
160        assert_eq!(err.to_string(), err.wire_message());
161        assert_eq!(
162            err.wire_message(),
163            "a vCPU did not ack quiesce within the timeout"
164        );
165    }
166
167    #[test]
168    fn test_should_format_magic_mismatch_with_hex() {
169        let err = SnapshotError::MagicMismatch {
170            found: 0xDEAD_BEEF,
171            expected: 0x0710_1984_AAAA_0000,
172        };
173        let s = err.wire_message();
174        assert!(s.contains("0xdeadbeef"), "msg = {s}");
175        assert!(s.contains("0x7101984aaaa0000"), "msg = {s}");
176    }
177
178    #[test]
179    fn test_should_classify_io_errors_under_io_variant() {
180        let io = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "no");
181        let err: SnapshotError = io.into();
182        assert!(matches!(err, SnapshotError::Io(_)));
183    }
184
185    #[test]
186    fn test_should_route_atomic_commit_failure_into_dedicated_variant() {
187        let io = std::io::Error::other("rename failed");
188        let err = SnapshotError::AtomicCommitFailed(io);
189        assert!(
190            err.wire_message()
191                .starts_with("atomic commit (rename) failed:")
192        );
193    }
194
195    #[test]
196    fn test_should_describe_cross_fs_with_paths() {
197        let err = SnapshotError::AtomicCommitCrossFs {
198            dest: PathBuf::from("/dest/x.snap"),
199            temp_dir: PathBuf::from("/other-fs"),
200        };
201        let s = err.wire_message();
202        assert!(s.contains("/dest/x.snap"));
203        assert!(s.contains("/other-fs"));
204    }
205}