1use anyhow::{Context, Result};
8use cap_std_ext::{cap_std::fs::Dir, dirext::CapStdExtDirExt};
9use composefs::fsverity::FsVerityHashValue;
10use composefs::repository::GcResult;
11use composefs_boot::bootloader::EFI_EXT;
12use composefs_ctl::composefs;
13use composefs_ctl::composefs_boot;
14use composefs_ctl::composefs_oci;
15
16use crate::{
17 bootc_composefs::{
18 boot::{BOOTC_UKI_DIR, BootType, get_type1_dir_name, get_uki_addon_dir_name, get_uki_name},
19 delete::{delete_staged, delete_state_dir},
20 repo::bootc_tag_for_manifest,
21 state::read_origin,
22 status::{BootloaderEntry, get_composefs_status, list_bootloader_entries},
23 },
24 composefs_consts::{
25 BOOTC_TAG_PREFIX, ORIGIN_KEY_IMAGE, ORIGIN_KEY_MANIFEST_DIGEST, STATE_DIR_RELATIVE,
26 TYPE1_BOOT_DIR_PREFIX, UKI_NAME_PREFIX,
27 },
28 store::{BootedComposefs, Storage},
29};
30
31#[fn_error_context::context("Listing state directories")]
32fn list_state_dirs(sysroot: &Dir) -> Result<Vec<String>> {
33 let state = sysroot
34 .open_dir(STATE_DIR_RELATIVE)
35 .context("Opening state dir")?;
36
37 let mut dirs = vec![];
38
39 for dir in state.entries_utf8()? {
40 let dir = dir?;
41
42 if dir.file_type()?.is_file() {
43 continue;
44 }
45
46 dirs.push(dir.file_name()?);
47 }
48
49 Ok(dirs)
50}
51
52type BootBinary = (BootType, String);
53
54#[fn_error_context::context("Collecting boot binaries")]
58fn collect_boot_binaries(storage: &Storage) -> Result<Vec<BootBinary>> {
59 let mut boot_binaries = Vec::new();
60 let boot_dir = storage.bls_boot_binaries_dir()?;
61 let esp = storage.require_esp()?;
62
63 collect_uki_binaries(&esp.fd, &mut boot_binaries)?;
65
66 collect_type1_boot_binaries(&boot_dir, &mut boot_binaries)?;
69
70 Ok(boot_binaries)
71}
72
73#[fn_error_context::context("Collecting UKI binaries")]
75fn collect_uki_binaries(boot_dir: &Dir, boot_binaries: &mut Vec<BootBinary>) -> Result<()> {
76 let Ok(Some(efi_dir)) = boot_dir.open_dir_optional(BOOTC_UKI_DIR) else {
77 return Ok(());
78 };
79
80 for entry in efi_dir.entries_utf8()? {
81 let entry = entry?;
82 let name = entry.file_name()?;
83
84 let Some(efi_name_no_prefix) = name.strip_prefix(UKI_NAME_PREFIX) else {
85 continue;
86 };
87
88 if let Some(verity) = efi_name_no_prefix.strip_suffix(EFI_EXT) {
89 boot_binaries.push((BootType::Uki, verity.into()));
90 }
91 }
92
93 Ok(())
94}
95
96#[fn_error_context::context("Collecting Type1 boot binaries")]
101fn collect_type1_boot_binaries(boot_dir: &Dir, boot_binaries: &mut Vec<BootBinary>) -> Result<()> {
102 for entry in boot_dir.entries_utf8()? {
103 let entry = entry?;
104 let dir_name = entry.file_name()?;
105
106 if !entry.file_type()?.is_dir() {
107 continue;
108 }
109
110 let Some(verity) = dir_name.strip_prefix(TYPE1_BOOT_DIR_PREFIX) else {
111 continue;
112 };
113
114 boot_binaries.push((BootType::Bls, verity.to_string()));
116 }
117
118 Ok(())
119}
120
121#[fn_error_context::context("Deleting kernel and initrd")]
122fn delete_kernel_initrd(storage: &Storage, dir_to_delete: &str, dry_run: bool) -> Result<()> {
123 tracing::debug!("Deleting Type1 entry {dir_to_delete}");
124
125 if dry_run {
126 return Ok(());
127 }
128
129 let boot_dir = storage.bls_boot_binaries_dir()?;
130
131 boot_dir
132 .remove_dir_all(dir_to_delete)
133 .with_context(|| anyhow::anyhow!("Deleting {dir_to_delete}"))
134}
135
136#[fn_error_context::context("Deleting UKI and UKI addons {uki_id}")]
138fn delete_uki(storage: &Storage, uki_id: &str, dry_run: bool) -> Result<()> {
139 let esp_mnt = storage.require_esp()?;
140
141 let uki_dir = esp_mnt.fd.open_dir(BOOTC_UKI_DIR)?;
144
145 for entry in uki_dir.entries_utf8()? {
146 let entry = entry?;
147 let entry_name = entry.file_name()?;
148
149 if entry_name == get_uki_name(uki_id) {
151 tracing::debug!("Deleting UKI: {}", entry_name);
152
153 if dry_run {
154 continue;
155 }
156
157 entry.remove_file().context("Deleting UKI")?;
158 } else if entry_name == get_uki_addon_dir_name(uki_id) {
159 tracing::debug!("Deleting UKI addons directory: {}", entry_name);
161
162 if dry_run {
163 continue;
164 }
165
166 uki_dir
167 .remove_dir_all(entry_name)
168 .context("Deleting UKI addons dir")?;
169 }
170 }
171
172 Ok(())
173}
174
175fn unreferenced_boot_binaries<'a>(
182 boot_binaries: &'a [BootBinary],
183 bootloader_entries: &[BootloaderEntry],
184) -> Vec<&'a BootBinary> {
185 boot_binaries
186 .iter()
187 .filter(|bin| {
188 !bootloader_entries
189 .iter()
190 .any(|entry| entry.boot_artifact_name == bin.1)
191 })
192 .collect()
193}
194
195#[fn_error_context::context("Running composefs garbage collection")]
212pub(crate) async fn composefs_gc(
213 storage: &Storage,
214 booted_cfs: &BootedComposefs,
215 dry_run: bool,
216 prune_repo: bool,
217) -> Result<GcResult> {
218 const COMPOSEFS_GC_JOURNAL_ID: &str = "3b2a1f0e9d8c7b6a5f4e3d2c1b0a9f8e7";
219
220 tracing::info!(
221 message_id = COMPOSEFS_GC_JOURNAL_ID,
222 bootc.operation = "gc",
223 bootc.current_deployment = booted_cfs.cmdline.digest,
224 "Starting composefs garbage collection"
225 );
226
227 let upgrade_result = composefs_oci::upgrade_repo(&booted_cfs.repo)
250 .context("Upgrading old-format OCI images before GC")?;
251 if upgrade_result.upgraded > 0 {
252 tracing::info!(
253 "Upgraded {} old-format OCI image(s) to current format before GC",
254 upgrade_result.upgraded
255 );
256 }
257
258 let host = get_composefs_status(storage, booted_cfs).await?;
259 let booted_cfs_status = host.require_composefs_booted()?;
260
261 let sysroot = &storage.physical_root;
262
263 let bootloader_entries = list_bootloader_entries(storage)?;
264 let boot_binaries = collect_boot_binaries(storage)?;
265
266 tracing::debug!("bootloader_entries: {bootloader_entries:?}");
267 tracing::debug!("boot_binaries: {boot_binaries:?}");
268
269 let unreferenced_boot_binaries =
270 unreferenced_boot_binaries(&boot_binaries, &bootloader_entries);
271
272 tracing::debug!("unreferenced_boot_binaries: {unreferenced_boot_binaries:?}");
273
274 if unreferenced_boot_binaries
275 .iter()
276 .find(|be| be.1 == booted_cfs_status.verity)
277 .is_some()
278 {
279 anyhow::bail!(
280 "Inconsistent state. Booted binaries '{}' found for cleanup",
281 booted_cfs_status.verity
282 )
283 }
284
285 for (ty, verity) in unreferenced_boot_binaries {
286 match ty {
287 BootType::Bls => delete_kernel_initrd(storage, &get_type1_dir_name(verity), dry_run)?,
288 BootType::Uki => delete_uki(storage, verity, dry_run)?,
289 }
290 }
291
292 if !prune_repo {
293 return Ok(GcResult::default());
294 }
295
296 let state_dirs = list_state_dirs(&sysroot)?;
301
302 let staged = &host.status.staged;
303
304 let orphaned_state_dirs: Vec<_> = state_dirs
306 .iter()
307 .filter(|s| !bootloader_entries.iter().any(|entry| &entry.fsverity == *s))
308 .collect();
309
310 let orphaned_boot_entries: Vec<_> = bootloader_entries
312 .iter()
313 .map(|entry| &entry.fsverity)
314 .filter(|verity| !state_dirs.contains(verity))
315 .collect();
316
317 let all_orphans: Vec<_> = orphaned_state_dirs
318 .iter()
319 .chain(orphaned_boot_entries.iter())
320 .copied()
321 .collect();
322
323 if all_orphans.contains(&&booted_cfs_status.verity) {
324 anyhow::bail!(
325 "Inconsistent state. Booted entry '{}' found for cleanup",
326 booted_cfs_status.verity
327 )
328 }
329
330 for verity in &orphaned_state_dirs {
331 tracing::debug!("Cleaning up orphaned state dir: {verity}");
332 delete_staged(staged, &all_orphans, dry_run)?;
333 delete_state_dir(&sysroot, verity, dry_run)?;
334 }
335
336 for verity in &orphaned_boot_entries {
337 tracing::debug!("Cleaning up orphaned bootloader entry: {verity}");
338 delete_staged(staged, &all_orphans, dry_run)?;
339 }
340
341 let mut live_manifest_digests: Vec<composefs_oci::OciDigest> = Vec::new();
345 let mut additional_roots = Vec::new();
346 let mut live_container_images: std::collections::HashSet<String> = Default::default();
348
349 let existing_tags = composefs_oci::list_refs(&*booted_cfs.repo)
352 .context("Listing OCI tags in composefs repo")?;
353
354 for deployment in host.list_deployments() {
355 let verity = &deployment.require_composefs()?.verity;
356
357 if all_orphans.contains(&verity) {
359 continue;
360 }
361
362 additional_roots.push(verity.clone());
367
368 if let Some(ini) = read_origin(sysroot, verity)? {
369 if let Some(container_ref) =
371 ini.get::<String>("origin", ostree_ext::container::deploy::ORIGIN_CONTAINER)
372 {
373 let image_name = container_ref
376 .parse::<ostree_ext::container::OstreeImageReference>()
377 .map(|r| r.imgref.name)
378 .unwrap_or_else(|_| container_ref.clone());
379 live_container_images.insert(image_name);
380 }
381
382 if let Some(manifest_digest_str) =
383 ini.get::<String>(ORIGIN_KEY_IMAGE, ORIGIN_KEY_MANIFEST_DIGEST)
384 {
385 let digest: composefs_oci::OciDigest = manifest_digest_str
386 .parse()
387 .with_context(|| format!("Parsing manifest digest {manifest_digest_str}"))?;
388 live_manifest_digests.push(digest);
389 } else {
390 let mut found_manifest = false;
393 for (_, ref_digest) in &existing_tags {
394 if let Ok(img) = composefs_oci::oci_image::OciImage::open(
395 &*booted_cfs.repo,
396 ref_digest,
397 None,
398 ) {
399 if let Some(img_ref) = img.image_ref() {
400 if img_ref.to_hex() == *verity {
401 tracing::info!(
402 "Deployment {verity} has no manifest_digest in origin; \
403 found matching manifest {ref_digest} via image_ref"
404 );
405 live_manifest_digests.push(ref_digest.clone());
406 found_manifest = true;
407 break;
408 }
409 }
410 }
411 }
412 if !found_manifest {
413 tracing::warn!(
414 "Deployment {verity} has no manifest_digest in origin \
415 and no tagged manifest references it; \
416 EROFS image is protected but OCI metadata may be collected"
417 );
418 }
419 }
420 }
421 }
422
423 for manifest_digest in &live_manifest_digests {
428 let expected_tag = bootc_tag_for_manifest(&manifest_digest.to_string());
429 let has_tag = existing_tags
430 .iter()
431 .any(|(tag_name, _)| tag_name == &expected_tag);
432 if !has_tag {
433 tracing::info!("Creating missing bootc tag for live deployment: {expected_tag}");
434 if !dry_run {
435 composefs_oci::tag_image(&*booted_cfs.repo, manifest_digest, &expected_tag)
436 .with_context(|| format!("Creating migration tag {expected_tag}"))?;
437 }
438 }
439 }
440
441 let all_tags = composefs_oci::list_refs(&*booted_cfs.repo)
443 .context("Listing OCI tags in composefs repo")?;
444
445 for (tag_name, manifest_digest) in &all_tags {
446 if !tag_name.starts_with(BOOTC_TAG_PREFIX) {
447 continue;
449 }
450
451 if !live_manifest_digests.iter().any(|d| d == manifest_digest) {
452 tracing::debug!("Removing unreferenced bootc tag: {tag_name}");
453 if !dry_run {
454 composefs_oci::untag_image(&*booted_cfs.repo, tag_name)
455 .with_context(|| format!("Removing tag {tag_name}"))?;
456 }
457 }
458 }
459
460 let additional_roots = additional_roots
461 .iter()
462 .map(|x| x.as_str())
463 .collect::<Vec<_>>();
464
465 if !dry_run && !live_container_images.is_empty() {
467 let subpath = crate::podstorage::CStorage::subpath();
468 if sysroot.try_exists(&subpath).unwrap_or(false) {
469 let run = Dir::open_ambient_dir("/run", cap_std_ext::cap_std::ambient_authority())?;
470 let imgstore = crate::podstorage::CStorage::create(&sysroot, &run, None)?;
471 let roots: std::collections::HashSet<&str> =
472 live_container_images.iter().map(|s| s.as_str()).collect();
473 let pruned = imgstore.prune_except_roots(&roots).await?;
474 if !pruned.is_empty() {
475 tracing::info!("Pruned {} images from containers-storage", pruned.len());
476 }
477 }
478 }
479
480 let gc_result = if dry_run {
486 booted_cfs.repo.gc_dry_run(&additional_roots)?
487 } else {
488 booted_cfs.repo.gc(&additional_roots)?
489 };
490
491 Ok(gc_result)
492}
493
494#[cfg(test)]
495mod tests {
496 use super::*;
497 use crate::bootc_composefs::status::list_type1_entries;
498 use crate::testutils::{ChangeType, TestRoot};
499
500 #[test]
518 fn test_gc_shared_boot_binaries_not_deleted() -> anyhow::Result<()> {
519 let mut root = TestRoot::new()?;
520 let digest_a = root.current().verity.clone();
521
522 root.upgrade(1, ChangeType::Userspace)?;
524
525 root.upgrade(2, ChangeType::Kernel)?;
527 let digest_c = root.current().verity.clone();
528
529 root.upgrade(3, ChangeType::Userspace)?;
531 let digest_d = root.current().verity.clone();
532
533 root.gc_deployment(&digest_a)?;
535
536 let boot_dir = root.boot_dir()?;
541
542 let mut on_disk = Vec::new();
544 collect_type1_boot_binaries(&boot_dir, &mut on_disk)?;
545 assert_eq!(
546 on_disk.len(),
547 2,
548 "should have A's and C's boot dirs on disk"
549 );
550
551 let bls_entries = list_type1_entries(&boot_dir)?;
553 assert_eq!(bls_entries.len(), 2, "D (primary) + C (secondary)");
554
555 let unreferenced = unreferenced_boot_binaries(&on_disk, &bls_entries);
561
562 assert_eq!(unreferenced.len(), 1);
564 assert_eq!(unreferenced[0].1, digest_a);
565
566 assert!(
568 !unreferenced.iter().any(|b| b.1 == digest_c),
569 "C's boot dir must not be unreferenced"
570 );
571
572 root.gc_deployment(&digest_c)?;
576
577 let mut on_disk_2 = Vec::new();
578 collect_type1_boot_binaries(&root.boot_dir()?, &mut on_disk_2)?;
579 assert_eq!(on_disk_2.len(), 2);
581
582 let bls_entries_2 = list_type1_entries(&root.boot_dir()?)?;
583 assert_eq!(bls_entries_2.len(), 2);
585
586 let entry_d = bls_entries_2
587 .iter()
588 .find(|e| e.fsverity == digest_d)
589 .unwrap();
590 assert_eq!(
591 entry_d.boot_artifact_name, digest_c,
592 "D shares C's boot dir"
593 );
594
595 let unreferenced_2 = unreferenced_boot_binaries(&on_disk_2, &bls_entries_2);
596
597 assert!(
601 unreferenced_2.is_empty(),
602 "no boot dirs should be unreferenced when both are shared"
603 );
604
605 let buggy_unreferenced: Vec<_> = on_disk_2
611 .iter()
612 .filter(|bin| !bls_entries_2.iter().any(|e| e.fsverity == bin.1))
613 .collect();
614 assert_eq!(
615 buggy_unreferenced.len(),
616 2,
617 "old fsverity-based logic would incorrectly GC both boot dirs"
618 );
619
620 Ok(())
621 }
622
623 #[test]
627 fn test_list_type1_entries_handles_legacy_bls() -> anyhow::Result<()> {
628 let mut root = TestRoot::new_legacy()?;
629 let digest_a = root.current().verity.clone();
630
631 root.upgrade(1, ChangeType::Userspace)?;
632 let digest_b = root.current().verity.clone();
633
634 let boot_dir = root.boot_dir()?;
635 let bls_entries = list_type1_entries(&boot_dir)?;
636
637 assert_eq!(bls_entries.len(), 2, "Should find both BLS entries");
638
639 for entry in &bls_entries {
642 assert_eq!(
643 entry.boot_artifact_name, digest_a,
644 "Both entries should reference A's boot dir (shared kernel)"
645 );
646 }
647
648 let verity_set: std::collections::HashSet<&str> =
650 bls_entries.iter().map(|e| e.fsverity.as_str()).collect();
651 assert!(verity_set.contains(digest_a.as_str()));
652 assert!(verity_set.contains(digest_b.as_str()));
653
654 Ok(())
655 }
656
657 #[test]
664 fn test_legacy_boot_dirs_invisible_to_gc_scanner() -> anyhow::Result<()> {
665 let root = TestRoot::new_legacy()?;
666
667 let boot_dir = root.boot_dir()?;
669 let mut on_disk = Vec::new();
670 collect_type1_boot_binaries(&boot_dir, &mut on_disk)?;
671
672 assert!(
675 on_disk.is_empty(),
676 "Legacy (unprefixed) boot dirs should not be found by collect_type1_boot_binaries"
677 );
678
679 Ok(())
680 }
681
682 #[test]
686 fn test_gc_works_after_legacy_migration() -> anyhow::Result<()> {
687 let mut root = TestRoot::new_legacy()?;
688 let digest_a = root.current().verity.clone();
689
690 root.upgrade(1, ChangeType::Userspace)?;
692
693 root.upgrade(2, ChangeType::Kernel)?;
695
696 root.migrate_to_prefixed()?;
698
699 let boot_dir = root.boot_dir()?;
701 let mut on_disk = Vec::new();
702 collect_type1_boot_binaries(&boot_dir, &mut on_disk)?;
703 assert_eq!(on_disk.len(), 2, "Should see A's and C's boot dirs");
704
705 let bls_entries = list_type1_entries(&boot_dir)?;
707 assert_eq!(bls_entries.len(), 2);
708
709 let unreferenced = unreferenced_boot_binaries(&on_disk, &bls_entries);
711 assert!(
712 unreferenced.is_empty(),
713 "All boot dirs should be referenced after migration"
714 );
715
716 root.gc_deployment(&digest_a)?;
718
719 let boot_dir = root.boot_dir()?;
720 let bls_entries = list_type1_entries(&boot_dir)?;
721 assert_eq!(bls_entries.len(), 2, "B (secondary) + C (primary)");
722
723 let mut on_disk = Vec::new();
724 collect_type1_boot_binaries(&boot_dir, &mut on_disk)?;
725 assert_eq!(on_disk.len(), 2, "Both boot dirs still on disk");
726
727 let unreferenced = unreferenced_boot_binaries(&on_disk, &bls_entries);
728 assert!(
730 unreferenced.is_empty(),
731 "A's boot dir should still be referenced by B after migration"
732 );
733
734 Ok(())
735 }
736
737 #[test]
743 fn test_gc_post_migration_upgrade_cycle() -> anyhow::Result<()> {
744 let mut root = TestRoot::new_legacy()?;
745 let digest_a = root.current().verity.clone();
746
747 root.upgrade(1, ChangeType::Userspace)?;
749
750 root.migrate_to_prefixed()?;
752
753 root.upgrade(2, ChangeType::Kernel)?;
755 let digest_c = root.current().verity.clone();
756
757 root.upgrade(3, ChangeType::Userspace)?;
759 let digest_d = root.current().verity.clone();
760
761 root.gc_deployment(&digest_a)?;
763
764 let boot_dir = root.boot_dir()?;
765 let mut on_disk = Vec::new();
766 collect_type1_boot_binaries(&boot_dir, &mut on_disk)?;
767
768 let bls_entries = list_type1_entries(&boot_dir)?;
769 assert_eq!(bls_entries.len(), 2, "D (primary) + C (secondary)");
770
771 let unreferenced = unreferenced_boot_binaries(&on_disk, &bls_entries);
772 assert_eq!(
774 unreferenced.len(),
775 1,
776 "A's boot dir should be unreferenced after GC of A and B is evicted"
777 );
778 assert_eq!(unreferenced[0].1, digest_a);
779
780 assert!(
782 !unreferenced.iter().any(|b| b.1 == digest_c),
783 "C's boot dir must still be referenced by D"
784 );
785
786 let entry_d = bls_entries
788 .iter()
789 .find(|e| e.fsverity == digest_d)
790 .expect("D should have a BLS entry");
791 assert_eq!(
792 entry_d.boot_artifact_name, digest_c,
793 "D should share C's boot dir"
794 );
795
796 Ok(())
797 }
798
799 #[test]
809 fn test_gc_deep_transitive_sharing_chain() -> anyhow::Result<()> {
810 let mut root = TestRoot::new()?;
811 let digest_a = root.current().verity.clone();
812
813 root.upgrade(1, ChangeType::Userspace)?;
815 root.upgrade(2, ChangeType::Userspace)?;
816 root.upgrade(3, ChangeType::Userspace)?;
817 let digest_d = root.current().verity.clone();
818
819 let boot_dir = root.boot_dir()?;
821 let mut on_disk = Vec::new();
822 collect_type1_boot_binaries(&boot_dir, &mut on_disk)?;
823 assert_eq!(on_disk.len(), 1, "All deployments share one boot dir");
824 assert_eq!(on_disk[0].1, digest_a, "The boot dir belongs to A");
825
826 let bls_entries = list_type1_entries(&boot_dir)?;
828 assert_eq!(bls_entries.len(), 2);
829 for entry in &bls_entries {
830 assert_eq!(
831 entry.boot_artifact_name, digest_a,
832 "All entries reference A's boot dir"
833 );
834 }
835
836 root.gc_deployment(&digest_a)?;
838
839 let boot_dir = root.boot_dir()?;
840 let bls_entries = list_type1_entries(&boot_dir)?;
841 assert_eq!(bls_entries.len(), 2);
843
844 let mut on_disk = Vec::new();
845 collect_type1_boot_binaries(&boot_dir, &mut on_disk)?;
846
847 let unreferenced = unreferenced_boot_binaries(&on_disk, &bls_entries);
848 assert!(
849 unreferenced.is_empty(),
850 "A's boot dir must stay — C and D still reference it"
851 );
852
853 let digest_b = crate::testutils::fake_digest_version(1);
855 let digest_c = crate::testutils::fake_digest_version(2);
856 root.gc_deployment(&digest_b)?;
857 root.gc_deployment(&digest_c)?;
858
859 let boot_dir = root.boot_dir()?;
861 let bls_entries = list_type1_entries(&boot_dir)?;
862 assert_eq!(bls_entries.len(), 1, "Only D remains");
863 assert_eq!(bls_entries[0].fsverity, digest_d);
864 assert_eq!(
865 bls_entries[0].boot_artifact_name, digest_a,
866 "D still references A's boot dir"
867 );
868
869 let mut on_disk = Vec::new();
870 collect_type1_boot_binaries(&boot_dir, &mut on_disk)?;
871 let unreferenced = unreferenced_boot_binaries(&on_disk, &bls_entries);
872 assert!(
873 unreferenced.is_empty(),
874 "A's boot dir must survive — D is the last deployment and still uses it"
875 );
876
877 Ok(())
878 }
879
880 #[test]
887 fn test_boot_artifact_info_drives_migration_decisions() -> anyhow::Result<()> {
888 use crate::bootc_composefs::status::get_sorted_type1_boot_entries;
889
890 let mut root = TestRoot::new_legacy()?;
891 let digest_a = root.current().verity.clone();
892
893 root.upgrade(1, ChangeType::Userspace)?;
894 root.upgrade(2, ChangeType::Kernel)?;
895
896 let boot_dir = root.boot_dir()?;
898 let raw_entries = get_sorted_type1_boot_entries(&boot_dir, true)?;
899 assert_eq!(raw_entries.len(), 2);
900
901 let needs_migration: Vec<_> = raw_entries
902 .iter()
903 .filter(|e| !e.boot_artifact_info().unwrap().1)
904 .collect();
905 assert_eq!(
906 needs_migration.len(),
907 2,
908 "All legacy entries should need migration (has_prefix=false)"
909 );
910
911 let mut on_disk = Vec::new();
913 collect_type1_boot_binaries(&boot_dir, &mut on_disk)?;
914 assert!(on_disk.is_empty(), "Legacy dirs invisible before migration");
915
916 root.migrate_to_prefixed()?;
918
919 let boot_dir = root.boot_dir()?;
921 let raw_entries = get_sorted_type1_boot_entries(&boot_dir, true)?;
922 assert_eq!(raw_entries.len(), 2);
923
924 let needs_migration: Vec<_> = raw_entries
925 .iter()
926 .filter(|e| !e.boot_artifact_info().unwrap().1)
927 .collect();
928 assert!(
929 needs_migration.is_empty(),
930 "No entries should need migration after migrate_to_prefixed()"
931 );
932
933 let mut on_disk = Vec::new();
935 collect_type1_boot_binaries(&boot_dir, &mut on_disk)?;
936 assert_eq!(on_disk.len(), 2, "Both dirs visible after migration");
937
938 let bls_entries = list_type1_entries(&boot_dir)?;
940 let unreferenced = unreferenced_boot_binaries(&on_disk, &bls_entries);
941 assert!(
942 unreferenced.is_empty(),
943 "All dirs referenced after migration"
944 );
945
946 root.upgrade(3, ChangeType::Kernel)?;
948
949 let boot_dir = root.boot_dir()?;
950 let raw_entries = get_sorted_type1_boot_entries(&boot_dir, true)?;
951 for entry in &raw_entries {
953 let (_, has_prefix) = entry.boot_artifact_info()?;
954 assert!(
955 has_prefix,
956 "All entries should have prefix after migration + upgrade"
957 );
958 }
959
960 let mut on_disk = Vec::new();
963 collect_type1_boot_binaries(&boot_dir, &mut on_disk)?;
964 assert_eq!(on_disk.len(), 3, "Three boot dirs on disk");
965
966 let bls_entries = list_type1_entries(&boot_dir)?;
968 assert_eq!(bls_entries.len(), 2);
969 let unreferenced = unreferenced_boot_binaries(&on_disk, &bls_entries);
970 assert_eq!(
971 unreferenced.len(),
972 1,
973 "A's boot dir should be unreferenced (B evicted from BLS)"
974 );
975 assert_eq!(unreferenced[0].1, digest_a);
976
977 Ok(())
978 }
979}