bootc_lib/bootc_composefs/
gc.rs1use anyhow::{Context, Result};
8use cap_std_ext::{cap_std::fs::Dir, dirext::CapStdExtDirExt};
9use composefs::fsverity::{FsVerityHashValue, Sha512HashValue};
10
11use crate::{
12 bootc_composefs::{
13 delete::{delete_image, delete_staged, delete_state_dir, get_image_objects},
14 status::{
15 get_bootloader, get_composefs_status, get_sorted_grub_uki_boot_entries,
16 get_sorted_type1_boot_entries,
17 },
18 },
19 composefs_consts::{STATE_DIR_RELATIVE, USER_CFG},
20 spec::Bootloader,
21 store::{BootedComposefs, Storage},
22};
23
24#[fn_error_context::context("Listing EROFS images")]
25fn list_erofs_images(sysroot: &Dir) -> Result<Vec<String>> {
26 let images_dir = sysroot
27 .open_dir("composefs/images")
28 .context("Opening images dir")?;
29
30 let mut images = vec![];
31
32 for entry in images_dir.entries_utf8()? {
33 let entry = entry?;
34 let name = entry.file_name()?;
35 images.push(name);
36 }
37
38 Ok(images)
39}
40
41#[fn_error_context::context("Listing bootloader entries")]
46fn list_bootloader_entries(storage: &Storage) -> Result<Vec<String>> {
47 let bootloader = get_bootloader()?;
48 let boot_dir = storage.require_boot_dir()?;
49
50 let entries = match bootloader {
51 Bootloader::Grub => {
52 let grub_dir = boot_dir.open_dir("grub2").context("Opening grub dir")?;
54
55 if grub_dir.exists(USER_CFG) {
56 let mut s = String::new();
58 let boot_entries = get_sorted_grub_uki_boot_entries(boot_dir, &mut s)?;
59
60 boot_entries
61 .into_iter()
62 .map(|entry| entry.get_verity())
63 .collect::<Result<Vec<_>, _>>()?
64 } else {
65 let boot_entries = get_sorted_type1_boot_entries(boot_dir, true)?;
67
68 boot_entries
69 .into_iter()
70 .map(|entry| entry.get_verity())
71 .collect::<Result<Vec<_>, _>>()?
72 }
73 }
74
75 Bootloader::Systemd => {
76 let boot_entries = get_sorted_type1_boot_entries(boot_dir, true)?;
77
78 boot_entries
79 .into_iter()
80 .map(|entry| entry.get_verity())
81 .collect::<Result<Vec<_>, _>>()?
82 }
83 };
84
85 Ok(entries)
86}
87
88#[fn_error_context::context("Listing state directories")]
89fn list_state_dirs(sysroot: &Dir) -> Result<Vec<String>> {
90 let state = sysroot
91 .open_dir(STATE_DIR_RELATIVE)
92 .context("Opening state dir")?;
93
94 let mut dirs = vec![];
95
96 for dir in state.entries_utf8()? {
97 let dir = dir?;
98
99 if dir.file_type()?.is_file() {
100 continue;
101 }
102
103 dirs.push(dir.file_name()?);
104 }
105
106 Ok(dirs)
107}
108
109#[fn_error_context::context("Garbage collecting objects")]
114pub(crate) fn gc_objects(sysroot: &Dir) -> Result<()> {
116 tracing::debug!("Running garbage collection on unreferenced objects");
117
118 let obj_refs = get_image_objects(sysroot)?;
120
121 let objects_dir = sysroot
123 .open_dir("composefs/objects")
124 .context("Opening objects dir")?;
125
126 for dir_name in 0x0..=0xff {
127 let dir = objects_dir
128 .open_dir_optional(dir_name.to_string())
129 .with_context(|| format!("Opening {dir_name}"))?;
130
131 let Some(dir) = dir else {
132 continue;
133 };
134
135 for entry in dir.entries_utf8()? {
136 let entry = entry?;
137 let filename = entry.file_name()?;
138
139 let id = Sha512HashValue::from_object_dir_and_basename(dir_name, filename.as_bytes())?;
140
141 if !obj_refs.contains(&id) {
143 tracing::trace!("Deleting unreferenced object: {filename}");
144
145 entry
146 .remove_file()
147 .with_context(|| format!("Removing object {filename}"))?;
148 }
149 }
150 }
151
152 Ok(())
153}
154
155#[fn_error_context::context("Running composefs garbage collection")]
166pub(crate) async fn composefs_gc(storage: &Storage, booted_cfs: &BootedComposefs) -> Result<()> {
167 let host = get_composefs_status(storage, booted_cfs).await?;
168 let booted_cfs_status = host.require_composefs_booted()?;
169
170 let sysroot = &storage.physical_root;
171
172 let bootloader_entries = list_bootloader_entries(&storage)?;
173 let images = list_erofs_images(&sysroot)?;
174
175 let img_bootloader_diff = images
177 .iter()
178 .filter(|i| !bootloader_entries.contains(i))
179 .collect::<Vec<_>>();
180
181 let staged = &host.status.staged;
182
183 if img_bootloader_diff.contains(&&booted_cfs_status.verity) {
184 anyhow::bail!(
185 "Inconsistent state. Booted entry '{}' found for cleanup",
186 booted_cfs_status.verity
187 )
188 }
189
190 for verity in &img_bootloader_diff {
191 tracing::debug!("Cleaning up orphaned image: {verity}");
192
193 delete_staged(staged)?;
194 delete_image(&sysroot, verity)?;
195 delete_state_dir(&sysroot, verity)?;
196 }
197
198 let state_dirs = list_state_dirs(&sysroot)?;
199
200 let state_img_diff = state_dirs
203 .iter()
204 .filter(|s| !images.contains(s))
205 .collect::<Vec<_>>();
206
207 for verity in &state_img_diff {
208 delete_staged(staged)?;
209 delete_state_dir(&sysroot, verity)?;
210 }
211
212 gc_objects(&sysroot)?;
214
215 Ok(())
216}