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