1use std::collections::BTreeMap;
4
5use serde::de::DeserializeOwned;
6use serde::{Deserialize, Serialize};
7
8use crate::error::{ImageError, ImageResult};
9
10use super::ObjectId;
11
12const MAX_MANIFEST_BYTES: usize = 8 * 1024 * 1024;
17const MAX_COMPONENTS: usize = 4096;
18const MAX_MEMORY_EXTENTS: usize = 4 * 1024 * 1024;
19
20#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
26#[serde(rename_all = "kebab-case")]
27pub enum CaptureIntent {
28 FullSnapshot,
30 Park,
32 TransparentTransfer,
34}
35
36#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
38#[serde(rename_all = "lowercase")]
39pub enum MemoryCaptureMode {
40 Full,
42 Incremental,
44}
45
46#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
48#[serde(deny_unknown_fields)]
49pub struct ContentRef {
50 pub object: ObjectId,
52 pub object_offset: u64,
54}
55
56#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
58#[serde(tag = "kind", rename_all = "lowercase")]
59pub enum MemoryExtentContent {
60 Object(ContentRef),
62 Zero,
64}
65
66#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
68#[serde(deny_unknown_fields)]
69pub struct MemoryExtent {
70 pub start: u64,
72 pub length: u64,
74 pub content: MemoryExtentContent,
76}
77
78#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
80#[serde(deny_unknown_fields)]
81pub struct MemoryManifest {
82 pub schema: String,
84 pub architecture: String,
86 pub guest_page_size: u64,
88 pub topology_generation: u64,
90 pub generation: u64,
92 pub capture_mode: MemoryCaptureMode,
94 pub pause_generation: u64,
96 pub extents: Vec<MemoryExtent>,
98}
99
100pub use microsandbox_types::snapshot::disk::{DiskGenerationManifest, DiskLayerRef};
101
102#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
104#[serde(rename_all = "lowercase")]
105pub enum ResourceTreatment {
106 Serialize,
108 Reconnect,
110 Reset,
112 Reject,
114}
115
116#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
118#[serde(deny_unknown_fields)]
119pub struct ResourceDescriptor {
120 pub id: String,
122 pub kind: String,
124 pub treatment: ResourceTreatment,
126 pub binding: BTreeMap<String, String>,
128}
129
130#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
132#[serde(deny_unknown_fields)]
133pub struct DeviceStateRef {
134 pub device_type: u32,
136 pub device_id: String,
138 pub state: ObjectId,
140}
141
142#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
144#[serde(deny_unknown_fields)]
145pub struct CheckpointGeometry {
146 pub vcpus: u8,
148 pub max_vcpus: u8,
150 pub memory_mib: u32,
152 pub max_memory_mib: u32,
154}
155
156#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
158#[serde(deny_unknown_fields)]
159pub struct CheckpointManifest {
160 pub schema: String,
162 pub checkpoint_id: String,
164 pub capture_intent: CaptureIntent,
166 pub architecture: String,
168 pub geometry: CheckpointGeometry,
170 pub pause_generation: u64,
172 pub execution_state: ObjectId,
174 pub memory: ObjectId,
176 pub disks: Vec<ObjectId>,
178 #[serde(default, skip_serializing_if = "Vec::is_empty")]
180 pub owned_volumes: Vec<crate::snapshot::OwnedVolumeCapture>,
181 pub devices: Vec<DeviceStateRef>,
183 pub resources: Vec<ResourceDescriptor>,
185 pub requires: Vec<String>,
187}
188
189impl MemoryManifest {
194 fn validate_body(&self) -> ImageResult<()> {
195 if self.architecture.is_empty() || self.guest_page_size == 0 || self.generation == 0 {
196 return manifest_error("memory manifest has empty architecture or zero geometry");
197 }
198 if self.extents.len() > MAX_MEMORY_EXTENTS {
199 return manifest_error("memory extent count exceeds the format bound");
200 }
201 validate_extents(&self.extents)
202 }
203}
204
205impl CheckpointManifest {
206 fn validate_body(&self) -> ImageResult<()> {
207 if self.checkpoint_id.is_empty() || self.architecture.is_empty() {
208 return manifest_error("checkpoint is missing identity or architecture");
209 }
210 if self.geometry.vcpus == 0
211 || self.geometry.vcpus > self.geometry.max_vcpus
212 || self.geometry.memory_mib == 0
213 || self.geometry.memory_mib > self.geometry.max_memory_mib
214 {
215 return manifest_error("checkpoint has invalid construction geometry");
216 }
217 if self.disks.len() > MAX_COMPONENTS
218 || self.devices.len() > MAX_COMPONENTS
219 || self.resources.len() > MAX_COMPONENTS
220 {
221 return manifest_error("checkpoint component count exceeds the format bound");
222 }
223 let mut devices = std::collections::BTreeSet::new();
224 for device in &self.devices {
225 if !devices.insert((device.device_type, device.device_id.as_str())) {
226 return manifest_error("checkpoint contains a duplicate logical device");
227 }
228 }
229 let mut resources = std::collections::BTreeSet::new();
230 for resource in &self.resources {
231 if resource.id.is_empty() || resource.kind.is_empty() || !resources.insert(&resource.id)
232 {
233 return manifest_error("checkpoint contains an invalid or duplicate resource");
234 }
235 if resource.treatment == ResourceTreatment::Reject {
236 return manifest_error("published checkpoint contains a rejected resource");
237 }
238 }
239 if self.requires.windows(2).any(|pair| pair[0] >= pair[1]) {
240 return manifest_error("checkpoint requires must be sorted and unique");
241 }
242 crate::snapshot::validate_owned_volumes(&self.owned_volumes)?;
243 crate::snapshot::validate_owned_resources(&self.owned_volumes, &self.resources)?;
244 for volume in &self.owned_volumes {
245 if let crate::snapshot::OwnedVolumeData::Disk { generation } = &volume.data
246 && generation.pause_generation != self.pause_generation
247 {
248 return manifest_error("owned disk belongs to another checkpoint epoch");
249 }
250 }
251 Ok(())
252 }
253}
254
255fn validate_extents(extents: &[MemoryExtent]) -> ImageResult<()> {
260 let mut previous_end = 0u64;
261 for (index, extent) in extents.iter().enumerate() {
262 if extent.length == 0 {
263 return manifest_error("memory extent has zero length");
264 }
265 let end = extent
266 .start
267 .checked_add(extent.length)
268 .ok_or_else(|| manifest_error_value("memory extent overflows the address space"))?;
269 if index != 0 && extent.start < previous_end {
270 return manifest_error("memory extents overlap or are unsorted");
271 }
272 if let MemoryExtentContent::Object(content) = &extent.content {
273 content
274 .object_offset
275 .checked_add(extent.length)
276 .ok_or_else(|| manifest_error_value("memory object slice overflows"))?;
277 }
278 previous_end = end;
279 }
280 Ok(())
281}
282
283fn canonical_bytes<T: Serialize>(manifest: &T) -> ImageResult<Vec<u8>> {
284 let value = serde_json::to_value(manifest)
285 .map_err(|error| manifest_error_value(format!("serialize failed: {error}")))?;
286 let mut output = Vec::new();
287 crate::snapshot::manifest::write_canonical_json(&value, &mut output)?;
288 if output.len() > MAX_MANIFEST_BYTES {
289 return manifest_error("manifest exceeds the encoded-size bound");
290 }
291 Ok(output)
292}
293
294fn parse_manifest<T>(bytes: &[u8]) -> ImageResult<T>
295where
296 T: DeserializeOwned + Serialize + Validate,
297{
298 if bytes.len() > MAX_MANIFEST_BYTES {
299 return manifest_error("manifest exceeds the encoded-size bound");
300 }
301 crate::snapshot::manifest::reject_duplicate_json_keys(bytes)?;
302 let manifest: T = serde_json::from_slice(bytes)
303 .map_err(|error| manifest_error_value(format!("parse failed: {error}")))?;
304 manifest.validate_manifest()?;
305 if canonical_bytes(&manifest)? != bytes {
306 return manifest_error("stored manifest bytes are not canonical");
307 }
308 Ok(manifest)
309}
310
311trait Validate {
312 fn validate_manifest(&self) -> ImageResult<()>;
313}
314
315impl Validate for MemoryManifest {
316 fn validate_manifest(&self) -> ImageResult<()> {
317 self.validate()
318 }
319}
320
321impl Validate for CheckpointManifest {
322 fn validate_manifest(&self) -> ImageResult<()> {
323 self.validate()
324 }
325}
326
327fn manifest_error<T>(message: impl Into<String>) -> ImageResult<T> {
328 Err(manifest_error_value(message))
329}
330
331fn manifest_error_value(message: impl Into<String>) -> ImageError {
332 ImageError::ManifestParse(format!("checkpoint manifest: {}", message.into()))
333}
334
335macro_rules! manifest_methods {
340 ($type:ty, $schema:literal) => {
341 impl $type {
342 pub fn validate(&self) -> ImageResult<()> {
344 if self.schema != $schema {
345 return manifest_error(format!(
346 "unsupported schema {} (expected {})",
347 self.schema, $schema
348 ));
349 }
350 self.validate_body()
351 }
352
353 pub fn to_canonical_bytes(&self) -> ImageResult<Vec<u8>> {
355 self.validate()?;
356 canonical_bytes(self)
357 }
358
359 pub fn from_bytes(bytes: &[u8]) -> ImageResult<Self> {
361 parse_manifest(bytes)
362 }
363
364 pub fn digest(&self) -> ImageResult<ObjectId> {
366 Ok(ObjectId::from_bytes(&self.to_canonical_bytes()?)?)
367 }
368 }
369 };
370}
371
372manifest_methods!(MemoryManifest, "microsandbox.memory/1");
373manifest_methods!(CheckpointManifest, "microsandbox.checkpoint/1");
374
375#[cfg(test)]
380mod tests {
381 use super::*;
382
383 #[test]
384 fn full_checkpoint_requires_valid_original_geometry() {
385 let object = ObjectId::from_bytes(b"state").unwrap();
386 let mut manifest = CheckpointManifest {
387 schema: "microsandbox.checkpoint/1".into(),
388 checkpoint_id: "checkpoint_geometry".into(),
389 capture_intent: CaptureIntent::FullSnapshot,
390 architecture: "aarch64".into(),
391 geometry: CheckpointGeometry {
392 vcpus: 2,
393 max_vcpus: 8,
394 memory_mib: 8192,
395 max_memory_mib: 32768,
396 },
397 pause_generation: 1,
398 execution_state: object.clone(),
399 memory: object,
400 disks: Vec::new(),
401 devices: Vec::new(),
402 resources: Vec::new(),
403 owned_volumes: Vec::new(),
404 requires: Vec::new(),
405 };
406 let bytes = manifest.to_canonical_bytes().unwrap();
407 assert_eq!(CheckpointManifest::from_bytes(&bytes).unwrap(), manifest);
408 let mut old = serde_json::to_value(&manifest).unwrap();
410 old.as_object_mut().unwrap().remove("geometry");
411 assert!(CheckpointManifest::from_bytes(&serde_json::to_vec(&old).unwrap()).is_err());
412 let original_geometry = manifest.geometry;
413 for geometry in [
414 CheckpointGeometry {
415 vcpus: 0,
416 ..original_geometry
417 },
418 CheckpointGeometry {
419 max_vcpus: 1,
420 ..original_geometry
421 },
422 CheckpointGeometry {
423 memory_mib: 0,
424 ..original_geometry
425 },
426 CheckpointGeometry {
427 max_memory_mib: 4096,
428 ..original_geometry
429 },
430 ] {
431 manifest.geometry = geometry;
432 assert!(manifest.to_canonical_bytes().is_err());
433 }
434 }
435
436 #[test]
437 fn incremental_memory_manifest_may_slice_reused_objects() {
438 let object = ObjectId::from_bytes(b"memory").unwrap();
439 let manifest = MemoryManifest {
440 schema: "microsandbox.memory/1".into(),
441 architecture: "aarch64".into(),
442 guest_page_size: 4096,
443 topology_generation: 1,
444 generation: 2,
445 capture_mode: MemoryCaptureMode::Incremental,
446 pause_generation: 42,
447 extents: vec![MemoryExtent {
448 start: 0,
449 length: 4096,
450 content: MemoryExtentContent::Object(ContentRef {
451 object,
452 object_offset: 4096,
453 }),
454 }],
455 };
456
457 let bytes = manifest.to_canonical_bytes().unwrap();
458 assert_eq!(MemoryManifest::from_bytes(&bytes).unwrap(), manifest);
459 }
460
461 #[test]
462 fn disk_layer_identity_cannot_escape_the_closure_directory() {
463 let manifest = DiskGenerationManifest {
464 schema: "microsandbox.disk-generation/1".into(),
465 volume_id: "vol_test".into(),
466 device_id: "vdb".into(),
467 generation: 1,
468 layers: vec![DiskLayerRef {
469 file_size: 4096,
470 layer_id: "../outside".into(),
471 format: "raw".into(),
472 virtual_size: 4096,
473 predecessor: None,
474 integrity_root: Some(format!("blake3:{}", "0".repeat(64))),
475 }],
476 head: "../outside".into(),
477 pause_generation: 1,
478 };
479
480 assert!(manifest.validate().is_err());
481 }
482
483 #[test]
484 fn earlier_disk_generation_defaults_to_managed_device() {
485 let manifest = DiskGenerationManifest {
486 schema: "microsandbox.disk-generation/1".into(),
487 volume_id: "vol_test".into(),
488 device_id: "vdb".into(),
489 generation: 1,
490 layers: vec![DiskLayerRef {
491 file_size: 4096,
492 layer_id: "layer_test".into(),
493 format: "raw".into(),
494 virtual_size: 4096,
495 predecessor: None,
496 integrity_root: Some(format!("blake3:{}", "0".repeat(64))),
497 }],
498 head: "layer_test".into(),
499 pause_generation: 1,
500 };
501 let mut value = serde_json::to_value(manifest).unwrap();
502 value.as_object_mut().unwrap().remove("device_id");
503 let parsed =
504 DiskGenerationManifest::from_bytes(&serde_json::to_vec(&value).unwrap()).unwrap();
505 assert_eq!(parsed.device_id, "vdb");
506 }
507}