1use std::collections::BTreeSet;
4use std::fs::File;
5use std::io::Read;
6use std::path::{Path, PathBuf};
7use std::time::Instant;
8
9use sha2::{Digest as _, Sha256};
10
11use super::admitted_disk::AdmittedDiskLayers;
12use super::{
13 CheckpointManifest, DiskGenerationManifest, DiskLayerRef, MemoryExtentContent, MemoryManifest,
14 ObjectId,
15};
16use crate::error::{ImageError, ImageResult};
17
18const CHECKPOINT_ROOT_FILE: &str = "checkpoint.json";
23const MAX_MANIFEST_BYTES: u64 = 8 * 1024 * 1024;
24const MAX_EXECUTION_STATE_BYTES: u64 = 512 * 1024 * 1024;
25const MAX_DEVICE_STATE_BYTES: u64 = 1024 * 1024;
26
27#[derive(Clone, Debug)]
37pub struct CheckpointClosure {
38 root: PathBuf,
39 root_id: ObjectId,
40 checkpoint: CheckpointManifest,
41 memory: MemoryManifest,
42 disks: Vec<DiskGenerationManifest>,
43 admitted_disks: AdmittedDiskLayers,
44}
45
46#[derive(Clone, Copy, Debug, Default)]
48pub struct CheckpointObjectReadTiming {
49 pub read_us: u128,
51 pub hash_us: u128,
53}
54
55impl CheckpointClosure {
60 pub fn inspect_manifest(
63 root: &Path,
64 expected_root: Option<&ObjectId>,
65 ) -> ImageResult<CheckpointManifest> {
66 read_checkpoint_root(root, expected_root).map(|(_, manifest)| manifest)
67 }
68
69 pub fn open(root: impl Into<PathBuf>, expected_root: Option<&ObjectId>) -> ImageResult<Self> {
71 Self::open_inner(root.into(), expected_root, true)
72 }
73
74 pub fn open_portable(
79 root: impl Into<PathBuf>,
80 expected_root: Option<&ObjectId>,
81 ) -> ImageResult<Self> {
82 Self::open_inner(root.into(), expected_root, false)
83 }
84
85 fn open_inner(
86 root: PathBuf,
87 expected_root: Option<&ObjectId>,
88 require_host_architecture: bool,
89 ) -> ImageResult<Self> {
90 let (root_id, checkpoint) = read_checkpoint_root(&root, expected_root)?;
91 if require_host_architecture && checkpoint.architecture != std::env::consts::ARCH {
92 return checkpoint_error(format!(
93 "checkpoint architecture {} cannot restore on {}",
94 checkpoint.architecture,
95 std::env::consts::ARCH
96 ));
97 }
98
99 let memory_bytes = read_object_verified(&root, &checkpoint.memory, MAX_MANIFEST_BYTES)?;
100 crate::snapshot::verify_owned_directory_payloads(&root, &checkpoint.owned_volumes)?;
101 let memory = MemoryManifest::from_bytes(&memory_bytes)?;
102 if memory.architecture != checkpoint.architecture
103 || memory.pause_generation != checkpoint.pause_generation
104 {
105 return checkpoint_error("memory state does not belong to the checkpoint epoch");
106 }
107 validate_memory_objects(&root, &memory)?;
108
109 read_object_verified(
112 &root,
113 &checkpoint.execution_state,
114 MAX_EXECUTION_STATE_BYTES,
115 )?;
116 for device in &checkpoint.devices {
117 read_object_verified(&root, &device.state, MAX_DEVICE_STATE_BYTES)?;
118 }
119
120 let mut disks = Vec::with_capacity(checkpoint.disks.len());
121 let mut admitted_disks = AdmittedDiskLayers::default();
122 let mut volumes = BTreeSet::new();
123 for disk_id in &checkpoint.disks {
124 let bytes = read_object_verified(&root, disk_id, MAX_MANIFEST_BYTES)?;
125 let disk = DiskGenerationManifest::from_bytes(&bytes)?;
126 if disk.pause_generation != checkpoint.pause_generation {
127 return checkpoint_error("disk generation does not belong to the checkpoint epoch");
128 }
129 if !volumes.insert(disk.volume_id.clone()) {
130 return checkpoint_error("checkpoint repeats a logical disk volume");
131 }
132 for layer in &disk.layers {
133 let path = disk_layer_path(&root, layer);
134 if open_regular(&path)?.metadata()?.len() != layer.file_size {
135 return checkpoint_error("disk layer length differs from captured file size");
136 }
137 if let Some(expected) = &layer.integrity_root {
138 admitted_disks.admit(&path, expected)?;
139 }
140 }
141 disks.push(disk);
142 }
143 for volume in &checkpoint.owned_volumes {
144 if let crate::snapshot::OwnedVolumeData::Disk { generation } = &volume.data
145 && !disks.contains(generation)
146 {
147 return checkpoint_error("owned disk is absent from the checkpoint closure");
148 }
149 }
150
151 Ok(Self {
152 root,
153 root_id,
154 checkpoint,
155 memory,
156 disks,
157 admitted_disks,
158 })
159 }
160
161 pub fn root_id(&self) -> &ObjectId {
163 &self.root_id
164 }
165
166 pub fn root(&self) -> &Path {
168 &self.root
169 }
170
171 pub fn checkpoint(&self) -> &CheckpointManifest {
173 &self.checkpoint
174 }
175
176 pub fn memory(&self) -> &MemoryManifest {
178 &self.memory
179 }
180
181 pub fn disks(&self) -> &[DiskGenerationManifest] {
183 &self.disks
184 }
185
186 pub fn read_object(&self, id: &ObjectId, max_len: u64) -> ImageResult<Vec<u8>> {
188 read_object_verified(&self.root, id, max_len)
189 }
190
191 pub fn read_object_into(
194 &self,
195 id: &ObjectId,
196 max_len: u64,
197 bytes: &mut Vec<u8>,
198 ) -> ImageResult<CheckpointObjectReadTiming> {
199 read_object_verified_into(&self.root, id, max_len, bytes)
200 }
201
202 pub fn disk_layer_path(&self, layer: &DiskLayerRef) -> PathBuf {
204 disk_layer_path(&self.root, layer)
205 }
206
207 pub fn reused_disk_integrity(&self, path: &Path) -> ImageResult<Option<String>> {
211 self.admitted_disks
212 .reuse_for(path)
213 .map(|root| root.map(str::to_owned))
214 }
215
216 pub fn verify_memory_objects(&self) -> ImageResult<()> {
221 let mut verified = BTreeSet::new();
222 for extent in &self.memory.extents {
223 let MemoryExtentContent::Object(content) = &extent.content else {
224 continue;
225 };
226 if verified.insert(content.object.clone()) {
227 verify_object_streaming(&self.root, &content.object)?;
228 }
229 }
230 Ok(())
231 }
232}
233
234fn read_checkpoint_root(
239 root: &Path,
240 expected_root: Option<&ObjectId>,
241) -> ImageResult<(ObjectId, CheckpointManifest)> {
242 if !std::fs::symlink_metadata(root)?.file_type().is_dir() {
243 return checkpoint_error("checkpoint root is not a directory");
244 }
245 let bytes = read_regular_bounded(&root.join(CHECKPOINT_ROOT_FILE), MAX_MANIFEST_BYTES)?;
246 let id = ObjectId::from_bytes(&bytes)?;
247 if let Some(expected) = expected_root.filter(|expected| *expected != &id) {
248 return Err(ImageError::DigestMismatch {
249 digest: id.to_string(),
250 expected: expected.to_string(),
251 actual: id.to_string(),
252 });
253 }
254 Ok((id, CheckpointManifest::from_bytes(&bytes)?))
255}
256
257fn validate_memory_objects(root: &Path, memory: &MemoryManifest) -> ImageResult<()> {
258 let mut verified = BTreeSet::new();
259 for extent in &memory.extents {
260 let MemoryExtentContent::Object(content) = &extent.content else {
261 continue;
262 };
263 let path = object_path(root, &content.object);
264 if verified.insert(content.object.clone()) {
265 open_regular(&path)?;
268 }
269 let size = std::fs::metadata(&path)?.len();
270 let end = content
271 .object_offset
272 .checked_add(extent.length)
273 .ok_or_else(|| checkpoint_error_value("memory object slice overflows"))?;
274 if end > size {
275 return checkpoint_error("memory extent exceeds its immutable object");
276 }
277 }
278 Ok(())
279}
280
281fn disk_layer_path(root: &Path, layer: &DiskLayerRef) -> PathBuf {
282 root.join("layers")
283 .join(format!("{}.{}", layer.layer_id, layer.format))
284}
285
286fn read_object_verified(root: &Path, id: &ObjectId, max_len: u64) -> ImageResult<Vec<u8>> {
287 let mut bytes = Vec::new();
288 read_object_verified_into(root, id, max_len, &mut bytes)?;
289 Ok(bytes)
290}
291
292fn read_object_verified_into(
293 root: &Path,
294 id: &ObjectId,
295 max_len: u64,
296 bytes: &mut Vec<u8>,
297) -> ImageResult<CheckpointObjectReadTiming> {
298 let started = Instant::now();
299 let path = object_path(root, id);
300 let mut file = open_regular(&path)?;
301 let length = file.metadata()?.len();
302 if length > max_len {
303 return checkpoint_error(format!("checkpoint object exceeds {max_len} bytes"));
304 }
305 let length = usize::try_from(length)
306 .map_err(|_| checkpoint_error_value("checkpoint object exceeds host limits"))?;
307 bytes.resize(length, 0);
311 file.read_exact(bytes)?;
312 if file.read(&mut [0u8; 1])? != 0 {
313 return checkpoint_error("checkpoint object changed length during read");
314 }
315 let read_us = started.elapsed().as_micros();
316 let hash_started = Instant::now();
317 let actual = ObjectId::from_bytes(bytes)?;
318 if &actual != id {
319 return Err(ImageError::DigestMismatch {
320 digest: id.to_string(),
321 expected: id.to_string(),
322 actual: actual.to_string(),
323 });
324 }
325 Ok(CheckpointObjectReadTiming {
326 read_us,
327 hash_us: hash_started.elapsed().as_micros(),
328 })
329}
330
331fn verify_object_streaming(root: &Path, id: &ObjectId) -> ImageResult<()> {
332 let mut file = open_regular(&object_path(root, id))?;
333 let mut hasher = Sha256::new();
334 let mut buffer = vec![0u8; 1024 * 1024];
335 loop {
336 let read = file.read(&mut buffer)?;
337 if read == 0 {
338 break;
339 }
340 hasher.update(&buffer[..read]);
341 }
342 let actual = format!("sha256:{}", hex::encode(hasher.finalize()));
343 if actual != id.as_str() {
344 return Err(ImageError::DigestMismatch {
345 digest: id.to_string(),
346 expected: id.to_string(),
347 actual,
348 });
349 }
350 Ok(())
351}
352
353fn read_regular_bounded(path: &Path, max_len: u64) -> ImageResult<Vec<u8>> {
354 let mut file = open_regular(path)?;
355 let length = file.metadata()?.len();
356 if length > max_len {
357 return checkpoint_error(format!("checkpoint manifest exceeds {max_len} bytes"));
358 }
359 let mut bytes = Vec::with_capacity(length as usize);
360 file.read_to_end(&mut bytes)?;
361 Ok(bytes)
362}
363
364fn open_regular(path: &Path) -> ImageResult<File> {
365 let metadata = std::fs::symlink_metadata(path)?;
366 if !metadata.file_type().is_file() {
367 return checkpoint_error(format!(
368 "checkpoint member is not a regular file: {}",
369 path.display()
370 ));
371 }
372 Ok(File::open(path)?)
373}
374
375fn object_path(root: &Path, id: &ObjectId) -> PathBuf {
376 let encoded = id
377 .as_str()
378 .strip_prefix("sha256:")
379 .expect("ObjectId validates its algorithm");
380 root.join("objects")
381 .join("sha256")
382 .join(&encoded[..2])
383 .join(encoded)
384}
385
386fn checkpoint_error<T>(message: impl Into<String>) -> ImageResult<T> {
387 Err(checkpoint_error_value(message))
388}
389
390fn checkpoint_error_value(message: impl Into<String>) -> ImageError {
391 ImageError::ManifestParse(format!("checkpoint closure: {}", message.into()))
392}
393
394#[cfg(test)]
399mod tests {
400 use std::collections::BTreeMap;
401
402 use super::*;
403 use crate::checkpoint::{
404 CaptureIntent, ContentRef, DeviceStateRef, MemoryCaptureMode, MemoryExtent,
405 ResourceDescriptor, ResourceTreatment,
406 };
407
408 #[test]
409 fn reusable_object_reader_checks_each_identity_and_reuses_allocation() {
410 let directory = tempfile::tempdir().unwrap();
411 let store = super::super::LocalObjectStore::open(directory.path()).unwrap();
412 let first = store.put_bytes(b"first payload").unwrap();
413 let second = store.put_bytes(b"next payload!").unwrap();
414 let mut buffer = Vec::with_capacity(64);
415 let allocation = buffer.as_ptr();
416 read_object_verified_into(directory.path(), &first, 64, &mut buffer).unwrap();
417 assert_eq!(buffer, b"first payload");
418 read_object_verified_into(directory.path(), &second, 64, &mut buffer).unwrap();
419 assert_eq!(buffer, b"next payload!");
420 assert_eq!(buffer.as_ptr(), allocation);
421 assert!(read_object_verified_into(directory.path(), &first, 4, &mut buffer).is_err());
422 std::fs::write(store.object_path(&second), b"bad payload!!").unwrap();
423 assert!(matches!(
424 read_object_verified_into(directory.path(), &second, 64, &mut buffer),
425 Err(ImageError::DigestMismatch { .. })
426 ));
427 }
428
429 fn fixture() -> (tempfile::TempDir, ObjectId) {
430 let directory = tempfile::tempdir().unwrap();
431 let store = super::super::LocalObjectStore::open(directory.path()).unwrap();
432 let memory_bytes = b"memory";
433 let memory_object = store.put_bytes(memory_bytes).unwrap();
434 let memory = MemoryManifest {
435 schema: "microsandbox.memory/1".into(),
436 architecture: std::env::consts::ARCH.into(),
437 guest_page_size: 4096,
438 topology_generation: 1,
439 generation: 1,
440 capture_mode: MemoryCaptureMode::Full,
441 pause_generation: 7,
442 extents: vec![MemoryExtent {
443 start: 0,
444 length: memory_bytes.len() as u64,
445 content: MemoryExtentContent::Object(ContentRef {
446 object: memory_object,
447 object_offset: 0,
448 }),
449 }],
450 };
451 let memory_id = store
452 .put_bytes(&memory.to_canonical_bytes().unwrap())
453 .unwrap();
454 let execution_id = store.put_bytes(b"execution").unwrap();
455 let device_id = store.put_bytes(b"device").unwrap();
456 let checkpoint = CheckpointManifest {
457 schema: "microsandbox.checkpoint/1".into(),
458 checkpoint_id: "checkpoint".into(),
459 capture_intent: CaptureIntent::FullSnapshot,
460 geometry: crate::checkpoint::CheckpointGeometry {
461 vcpus: 1,
462 max_vcpus: 1,
463 memory_mib: 128,
464 max_memory_mib: 128,
465 },
466 architecture: std::env::consts::ARCH.into(),
467 pause_generation: 7,
468 execution_state: execution_id,
469 memory: memory_id,
470 disks: Vec::new(),
471 owned_volumes: Vec::new(),
472 devices: vec![DeviceStateRef {
473 device_type: 4,
474 device_id: "rng".into(),
475 state: device_id,
476 }],
477 resources: vec![ResourceDescriptor {
478 id: "virtio:4:rng".into(),
479 kind: "rng".into(),
480 treatment: ResourceTreatment::Reset,
481 binding: BTreeMap::new(),
482 }],
483 requires: Vec::new(),
484 };
485 let root_bytes = checkpoint.to_canonical_bytes().unwrap();
486 let root_id = ObjectId::from_bytes(&root_bytes).unwrap();
487 std::fs::write(directory.path().join(CHECKPOINT_ROOT_FILE), root_bytes).unwrap();
488 (directory, root_id)
489 }
490
491 #[test]
492 fn manifest_inspection_does_not_substitute_for_payload_admission() {
493 let (directory, root) = fixture();
494 let manifest = CheckpointClosure::inspect_manifest(directory.path(), Some(&root)).unwrap();
495 std::fs::remove_file(super::object_path(
496 directory.path(),
497 &manifest.execution_state,
498 ))
499 .unwrap();
500 assert!(CheckpointClosure::inspect_manifest(directory.path(), Some(&root)).is_ok());
501 assert!(CheckpointClosure::open(directory.path(), Some(&root)).is_err());
502 let wrong = ObjectId::from_bytes(b"wrong root").unwrap();
503 assert!(CheckpointClosure::inspect_manifest(directory.path(), Some(&wrong)).is_err());
504 }
505
506 #[test]
507 fn optional_disk_integrity_retains_length_checks_and_detects_opted_in_corruption() {
508 use crate::checkpoint::{
509 DiskGenerationManifest, DiskLayerRef, LocalObjectStore, sparse_file_integrity,
510 };
511 for record_integrity in [false, true] {
512 let (directory, root) = fixture();
513 let mut checkpoint =
514 CheckpointClosure::inspect_manifest(directory.path(), Some(&root)).unwrap();
515 let store = LocalObjectStore::open(directory.path()).unwrap();
516 std::fs::create_dir(directory.path().join("layers")).unwrap();
517 let path = directory.path().join("layers/base.raw");
518 std::fs::write(&path, [17; 4096]).unwrap();
519 let disk = DiskGenerationManifest {
520 schema: "microsandbox.disk-generation/1".into(),
521 volume_id: "volume".into(),
522 device_id: "vdb".into(),
523 generation: 1,
524 pause_generation: checkpoint.pause_generation,
525 head: "base".into(),
526 layers: vec![DiskLayerRef {
527 layer_id: "base".into(),
528 format: "raw".into(),
529 virtual_size: 4096,
530 file_size: 4096,
531 predecessor: None,
532 integrity_root: record_integrity
533 .then(|| sparse_file_integrity(&path).unwrap().root),
534 }],
535 };
536 checkpoint.disks = vec![
537 store
538 .put_bytes(&disk.to_canonical_bytes().unwrap())
539 .unwrap(),
540 ];
541 std::fs::write(
542 directory.path().join(CHECKPOINT_ROOT_FILE),
543 checkpoint.to_canonical_bytes().unwrap(),
544 )
545 .unwrap();
546 assert!(CheckpointClosure::open(directory.path(), None).is_ok());
547 std::fs::write(&path, [18; 4096]).unwrap();
549 assert_eq!(
550 CheckpointClosure::open(directory.path(), None).is_err(),
551 record_integrity
552 );
553 std::fs::write(&path, [17; 4095]).unwrap();
554 let error = CheckpointClosure::open(directory.path(), None)
555 .err()
556 .unwrap()
557 .to_string();
558 assert!(error.contains("length"), "{error}");
559 }
560 }
561
562 #[test]
563 fn opens_complete_valid_closure() {
564 let (directory, expected) = fixture();
565
566 let closure = CheckpointClosure::open(directory.path(), Some(&expected)).unwrap();
567
568 assert_eq!(closure.root_id(), &expected);
569 assert_eq!(closure.memory().pause_generation, 7);
570 }
571
572 #[test]
573 fn deep_closure_admission_keeps_file_handles_bounded() {
574 #[cfg(unix)]
575 if std::env::var_os("MSB_TEST_DEEP_ADMISSION_LOW_FD").is_none() {
576 use std::os::unix::process::CommandExt;
577
578 let mut child = std::process::Command::new(std::env::current_exe().unwrap());
581 child
582 .args([
583 "--exact",
584 "checkpoint::resolver::tests::deep_closure_admission_keeps_file_handles_bounded",
585 "--nocapture",
586 ])
587 .env("MSB_TEST_DEEP_ADMISSION_LOW_FD", "1");
588 unsafe {
591 child.pre_exec(|| {
592 let mut limit = std::mem::zeroed::<libc::rlimit>();
593 if libc::getrlimit(libc::RLIMIT_NOFILE, &mut limit) != 0 {
594 return Err(std::io::Error::last_os_error());
595 }
596 limit.rlim_cur = limit.rlim_max.min(64);
597 if libc::setrlimit(libc::RLIMIT_NOFILE, &limit) != 0 {
598 return Err(std::io::Error::last_os_error());
599 }
600 Ok(())
601 });
602 }
603 let output = child.output().unwrap();
604 assert!(
605 output.status.success(),
606 "low-FD admission failed: {}{}",
607 String::from_utf8_lossy(&output.stdout),
608 String::from_utf8_lossy(&output.stderr)
609 );
610 assert!(String::from_utf8_lossy(&output.stdout).contains("1 passed"));
611 return;
612 }
613
614 let (directory, _) = fixture();
615 let store = super::super::LocalObjectStore::open(directory.path()).unwrap();
616 let root_path = directory.path().join(CHECKPOINT_ROOT_FILE);
617 let mut checkpoint =
618 CheckpointManifest::from_bytes(&std::fs::read(&root_path).unwrap()).unwrap();
619 std::fs::create_dir_all(directory.path().join("layers")).unwrap();
620 let mut paths = Vec::new();
621 for volume in 0..2 {
624 let mut layers = Vec::new();
625 for index in 0..256 {
626 let layer_id = format!("volume_{volume}_layer_{index}");
627 let format = if index == 0 { "raw" } else { "qcow2" };
628 let path = directory
629 .path()
630 .join("layers")
631 .join(format!("{layer_id}.{format}"));
632 std::fs::write(&path, [0x55]).unwrap();
633 let integrity_root = super::super::sparse_file_integrity(&path).unwrap().root;
634 layers.push(DiskLayerRef {
635 file_size: 1,
636 layer_id,
637 format: format.into(),
638 virtual_size: 4096,
639 predecessor: (index > 0)
640 .then(|| format!("volume_{volume}_layer_{}", index - 1)),
641 integrity_root: Some(integrity_root),
642 });
643 paths.push(path);
644 }
645 let disk = DiskGenerationManifest {
646 schema: "microsandbox.disk-generation/1".into(),
647 volume_id: format!("volume_{volume}"),
648 device_id: format!("device_{volume}"),
649 generation: 1,
650 head: layers.last().unwrap().layer_id.clone(),
651 layers,
652 pause_generation: checkpoint.pause_generation,
653 };
654 checkpoint.disks.push(
655 store
656 .put_bytes(&disk.to_canonical_bytes().unwrap())
657 .unwrap(),
658 );
659 }
660 let bytes = checkpoint.to_canonical_bytes().unwrap();
661 let root = ObjectId::from_bytes(&bytes).unwrap();
662 std::fs::write(&root_path, bytes).unwrap();
663
664 let closure = CheckpointClosure::open(directory.path(), Some(&root)).unwrap();
665 assert_eq!(closure.disks().len(), 2);
666 assert!(closure.disks().iter().all(|disk| disk.layers.len() == 256));
667 let reusable = paths
668 .iter()
669 .filter(|path| closure.reused_disk_integrity(path).unwrap().is_some())
670 .count();
671 assert_eq!(reusable, 32);
672 drop(closure);
673
674 std::fs::write(paths.last().unwrap(), [0xAA]).unwrap();
676 assert!(matches!(
677 CheckpointClosure::open(directory.path(), Some(&root)),
678 Err(ImageError::DigestMismatch { .. })
679 ));
680 }
681
682 #[test]
683 fn portable_open_separates_integrity_from_restore_architecture() {
684 let (directory, _expected) = fixture();
685 let root_path = directory.path().join(CHECKPOINT_ROOT_FILE);
686 let mut checkpoint =
687 CheckpointManifest::from_bytes(&std::fs::read(&root_path).unwrap()).unwrap();
688 checkpoint.architecture = "another-architecture".into();
689 let memory_path = object_path(directory.path(), &checkpoint.memory);
690 let mut memory = MemoryManifest::from_bytes(&std::fs::read(&memory_path).unwrap()).unwrap();
691 memory.architecture = checkpoint.architecture.clone();
692 let store = super::super::LocalObjectStore::open(directory.path()).unwrap();
693 checkpoint.memory = store
694 .put_bytes(&memory.to_canonical_bytes().unwrap())
695 .unwrap();
696 let root_bytes = checkpoint.to_canonical_bytes().unwrap();
697 let expected = ObjectId::from_bytes(&root_bytes).unwrap();
698 std::fs::write(root_path, root_bytes).unwrap();
699
700 CheckpointClosure::open_portable(directory.path(), Some(&expected)).unwrap();
701 let error = CheckpointClosure::open(directory.path(), Some(&expected)).unwrap_err();
702 assert!(error.to_string().contains("cannot restore"));
703 }
704
705 #[test]
706 fn rejects_replaced_memory_object() {
707 let (directory, expected) = fixture();
708 let checkpoint = CheckpointManifest::from_bytes(
709 &std::fs::read(directory.path().join(CHECKPOINT_ROOT_FILE)).unwrap(),
710 )
711 .unwrap();
712 let memory = MemoryManifest::from_bytes(
713 &read_object_verified(directory.path(), &checkpoint.memory, MAX_MANIFEST_BYTES)
714 .unwrap(),
715 )
716 .unwrap();
717 let MemoryExtentContent::Object(content) = &memory.extents[0].content else {
718 panic!("fixture uses object memory");
719 };
720 std::fs::write(object_path(directory.path(), &content.object), b"changed").unwrap();
721
722 let closure = CheckpointClosure::open(directory.path(), Some(&expected)).unwrap();
723 let error = closure
724 .read_object(&content.object, MAX_MANIFEST_BYTES)
725 .unwrap_err();
726
727 assert!(matches!(error, ImageError::DigestMismatch { .. }));
728 }
729
730 #[test]
731 fn explicit_verification_detects_replaced_memory_object() {
732 let (directory, expected) = fixture();
733 let closure = CheckpointClosure::open(directory.path(), Some(&expected)).unwrap();
734 let MemoryExtentContent::Object(content) = &closure.memory().extents[0].content else {
735 panic!("fixture uses object memory");
736 };
737 std::fs::write(object_path(directory.path(), &content.object), b"changed").unwrap();
738
739 let error = closure.verify_memory_objects().unwrap_err();
740
741 assert!(matches!(error, ImageError::DigestMismatch { .. }));
742 }
743}