/src/crosvm/snapshot/src/lib.rs
Line | Count | Source |
1 | | // Copyright 2025 The ChromiumOS Authors |
2 | | // Use of this source code is governed by a BSD-style license that can be |
3 | | // found in the LICENSE file. |
4 | | |
5 | | use std::fmt::Debug; |
6 | | use std::fmt::Formatter; |
7 | | use std::fs::File; |
8 | | use std::io::BufReader; |
9 | | use std::io::Read; |
10 | | use std::io::Write; |
11 | | use std::path::Path; |
12 | | use std::path::PathBuf; |
13 | | |
14 | | use anyhow::Context; |
15 | | use anyhow::Result; |
16 | | use crypto::CryptKey; |
17 | | |
18 | | mod any_snapshot; |
19 | | |
20 | | pub use any_snapshot::AnySnapshot; |
21 | | |
22 | | // Use 4kB encrypted chunks by default (if encryption is used). |
23 | | const DEFAULT_ENCRYPTED_CHUNK_SIZE_BYTES: usize = 1024 * 4; |
24 | | |
25 | | /// Writer of serialized VM snapshots. |
26 | | /// |
27 | | /// Each fragment is an opaque byte blob. Namespaces can be used to avoid fragment naming |
28 | | /// collisions between devices. |
29 | | /// |
30 | | /// In the current implementation, fragments are files and namespaces are directories, but the API |
31 | | /// is kept abstract so that we can potentially support something like a single file archive |
32 | | /// output. |
33 | | #[derive(Clone, serde::Serialize, serde::Deserialize)] |
34 | | pub struct SnapshotWriter { |
35 | | dir: PathBuf, |
36 | | /// If encryption is used, the plaintext key will be stored here. |
37 | | key: Option<CryptKey>, |
38 | | } |
39 | | |
40 | | impl Debug for SnapshotWriter { |
41 | 0 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { |
42 | 0 | f.debug_struct("SnapshotWriter") |
43 | 0 | .field("dir", &format!("{:?}", self.dir)) |
44 | 0 | .field("key", if self.key.is_some() { &"Some" } else { &"None" }) |
45 | 0 | .finish() |
46 | 0 | } |
47 | | } |
48 | | |
49 | | impl SnapshotWriter { |
50 | | /// Creates a new `SnapshotWriter` that will writes its data to a dir at `root`. The path must |
51 | | /// not exist yet. If encryption is desired, set encrypt (Note: only supported downstream on |
52 | | /// Windows). |
53 | | // TODO(b/268094487): If the snapshot fails, we leave incomplete snapshot files at the |
54 | | // requested path. Consider building up the snapshot dir somewhere else and moving it into |
55 | | // place at the end. |
56 | 0 | pub fn new(root: PathBuf, encrypt: bool) -> Result<Self> { |
57 | 0 | std::fs::create_dir(&root) |
58 | 0 | .with_context(|| format!("failed to create snapshot root dir: {}", root.display()))?; |
59 | | |
60 | 0 | if encrypt { |
61 | 0 | let key = crypto::generate_random_key(); |
62 | | // Creating an empty CryptWriter will still write header information |
63 | | // to the file, and that header information is what we need. This |
64 | | // ensures we use a single key for *all* snapshot files. |
65 | 0 | let mut writer = crypto::CryptWriter::new_from_key( |
66 | 0 | File::create(root.join("enc_metadata")).context("failed to create enc_metadata")?, |
67 | | 1024, |
68 | 0 | &key, |
69 | | ) |
70 | 0 | .context("failed to create enc_metadata writer")?; |
71 | 0 | writer.flush().context("flush of enc_metadata failed")?; |
72 | 0 | return Ok(Self { |
73 | 0 | dir: root, |
74 | 0 | key: Some(key), |
75 | 0 | }); |
76 | 0 | } |
77 | | |
78 | 0 | Ok(Self { |
79 | 0 | dir: root, |
80 | 0 | key: None, |
81 | 0 | }) |
82 | 0 | } |
83 | | |
84 | | /// Creates a snapshot fragment and get access to the `Write` impl representing it. |
85 | 0 | pub fn raw_fragment(&self, name: &str) -> Result<Box<dyn Write>> { |
86 | 0 | self.raw_fragment_with_chunk_size(name, DEFAULT_ENCRYPTED_CHUNK_SIZE_BYTES) |
87 | 0 | } |
88 | | |
89 | | /// When encryption is used, allows direct control of the encrypted chunk size. |
90 | 0 | pub fn raw_fragment_with_chunk_size( |
91 | 0 | &self, |
92 | 0 | name: &str, |
93 | 0 | chunk_size_bytes: usize, |
94 | 0 | ) -> Result<Box<dyn Write>> { |
95 | 0 | let path = self.dir.join(name); |
96 | 0 | let file = File::options() |
97 | 0 | .write(true) |
98 | 0 | .create_new(true) |
99 | 0 | .open(&path) |
100 | 0 | .with_context(|| { |
101 | 0 | format!( |
102 | 0 | "failed to create snapshot fragment {name:?} at {}", |
103 | 0 | path.display() |
104 | | ) |
105 | 0 | })?; |
106 | | |
107 | 0 | if let Some(key) = self.key.as_ref() { |
108 | 0 | return Ok(Box::new(crypto::CryptWriter::new_from_key( |
109 | 0 | file, |
110 | 0 | chunk_size_bytes, |
111 | 0 | key, |
112 | 0 | )?)); |
113 | 0 | } |
114 | | |
115 | 0 | Ok(Box::new(file)) |
116 | 0 | } |
117 | | |
118 | | /// Creates a snapshot fragment from a serialized representation of `v`. |
119 | 0 | pub fn write_fragment<T: serde::Serialize>(&self, name: &str, v: &T) -> Result<()> { |
120 | 0 | let mut w = std::io::BufWriter::new(self.raw_fragment(name)?); |
121 | 0 | ciborium::into_writer(v, &mut w)?; |
122 | 0 | w.flush()?; |
123 | 0 | Ok(()) |
124 | 0 | } |
125 | | |
126 | | /// Creates new namespace and returns a `SnapshotWriter` that writes to it. Namespaces can be |
127 | | /// nested. |
128 | 0 | pub fn add_namespace(&self, name: &str) -> Result<Self> { |
129 | 0 | let dir = self.dir.join(name); |
130 | 0 | std::fs::create_dir(&dir).with_context(|| { |
131 | 0 | format!( |
132 | 0 | "failed to create nested snapshot writer {name:?} at {}", |
133 | 0 | dir.display() |
134 | | ) |
135 | 0 | })?; |
136 | 0 | Ok(Self { |
137 | 0 | dir, |
138 | 0 | key: self.key.clone(), |
139 | 0 | }) |
140 | 0 | } |
141 | | } |
142 | | |
143 | | /// Reads snapshots created by `SnapshotWriter`. |
144 | | #[derive(Clone, serde::Serialize, serde::Deserialize)] |
145 | | pub struct SnapshotReader { |
146 | | dir: PathBuf, |
147 | | /// If encryption is used, the plaintext key will be stored here. |
148 | | key: Option<CryptKey>, |
149 | | } |
150 | | |
151 | | impl Debug for SnapshotReader { |
152 | 0 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { |
153 | 0 | f.debug_struct("SnapshotReader") |
154 | 0 | .field("dir", &format!("{:?}", self.dir)) |
155 | 0 | .field("key", if self.key.is_some() { &"Some" } else { &"None" }) |
156 | 0 | .finish() |
157 | 0 | } |
158 | | } |
159 | | |
160 | | impl SnapshotReader { |
161 | | /// Reads a snapshot at `root`. Set require_encrypted to require an encrypted snapshot. |
162 | 0 | pub fn new(root: &Path, require_encrypted: bool) -> Result<Self> { |
163 | 0 | let enc_metadata_path = root.join("enc_metadata"); |
164 | 0 | if Path::exists(&enc_metadata_path) { |
165 | 0 | let key = Some( |
166 | 0 | crypto::CryptReader::extract_key( |
167 | 0 | File::open(&enc_metadata_path).context("failed to open encryption metadata")?, |
168 | | ) |
169 | 0 | .context("failed to load snapshot key")?, |
170 | | ); |
171 | 0 | return Ok(Self { |
172 | 0 | dir: root.to_path_buf(), |
173 | 0 | key, |
174 | 0 | }); |
175 | 0 | } else if require_encrypted { |
176 | 0 | return Err(anyhow::anyhow!("snapshot was not encrypted")); |
177 | 0 | } |
178 | | |
179 | 0 | Ok(Self { |
180 | 0 | dir: root.to_path_buf(), |
181 | 0 | key: None, |
182 | 0 | }) |
183 | 0 | } |
184 | | |
185 | | /// Gets access to a `Read` impl that represents a fragment. |
186 | 0 | pub fn raw_fragment(&self, name: &str) -> Result<Box<dyn Read>> { |
187 | 0 | let path = self.dir.join(name); |
188 | 0 | let file = File::open(&path).with_context(|| { |
189 | 0 | format!( |
190 | 0 | "failed to open snapshot fragment {name:?} at {}", |
191 | 0 | path.display() |
192 | | ) |
193 | 0 | })?; |
194 | 0 | if let Some(key) = self.key.as_ref() { |
195 | 0 | return Ok(Box::new(crypto::CryptReader::from_file_and_key(file, key)?)); |
196 | 0 | } |
197 | | |
198 | 0 | Ok(Box::new(file)) |
199 | 0 | } |
200 | | |
201 | | /// Reads a fragment. |
202 | 0 | pub fn read_fragment<T: serde::de::DeserializeOwned>(&self, name: &str) -> Result<T> { |
203 | 0 | Ok(ciborium::from_reader(BufReader::new( |
204 | 0 | self.raw_fragment(name)?, |
205 | 0 | ))?) |
206 | 0 | } |
207 | | |
208 | | /// Reads the names of all fragments in this namespace. |
209 | 0 | pub fn list_fragments(&self) -> Result<Vec<String>> { |
210 | 0 | let mut result = Vec::new(); |
211 | 0 | for entry in std::fs::read_dir(&self.dir)? { |
212 | 0 | let entry = entry?; |
213 | 0 | if entry.file_type()?.is_file() { |
214 | 0 | result.push(entry.file_name().to_string_lossy().into_owned()); |
215 | 0 | } |
216 | | } |
217 | 0 | Ok(result) |
218 | 0 | } |
219 | | |
220 | | /// Open a namespace. |
221 | 0 | pub fn namespace(&self, name: &str) -> Result<Self> { |
222 | 0 | let dir = self.dir.join(name); |
223 | 0 | Ok(Self { |
224 | 0 | dir, |
225 | 0 | key: self.key.clone(), |
226 | 0 | }) |
227 | 0 | } |
228 | | |
229 | | /// Reads the names of all child namespaces |
230 | 0 | pub fn list_namespaces(&self) -> Result<Vec<String>> { |
231 | 0 | let mut result = Vec::new(); |
232 | 0 | for entry in std::fs::read_dir(&self.dir)? { |
233 | 0 | let entry = entry?; |
234 | 0 | if entry.path().is_dir() { |
235 | 0 | if let Some(file_name) = entry.path().file_name() { |
236 | 0 | result.push(file_name.to_string_lossy().into_owned()); |
237 | 0 | } |
238 | 0 | } |
239 | | } |
240 | 0 | Ok(result) |
241 | 0 | } |
242 | | } |