Coverage Report

Created: 2026-07-06 06:09

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/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
}