bootc_lib/bootc_composefs/
finalize.rs1use std::{path::Path, process::Command};
2
3use crate::bootc_composefs::boot::{GLOBAL_UKI_ADDONS_DIR, STAGED_GLOBAL_UKI_ADDONS_DIR};
4use crate::bootc_composefs::gc::{GCOpts, composefs_gc};
5use crate::bootc_composefs::rollback::{rename_exchange_bls_entries, rename_exchange_user_cfg};
6use crate::bootc_composefs::status::get_composefs_status;
7use crate::bootc_composefs::{aboot, boot::BootType};
8use crate::composefs_consts::STATE_DIR_ABS;
9use crate::install::BOOT;
10use crate::spec::BootloaderKind;
11use crate::store::{BootedComposefs, Storage};
12use anyhow::{Context, Result};
13use bootc_initramfs_setup::mount_composefs_image;
14use bootc_mount::tempmount::TempMount;
15use cap_std_ext::cap_std::{ambient_authority, fs::Dir};
16use cap_std_ext::dirext::CapStdExtDirExt;
17use composefs::generic_tree::{FileSystem, Stat};
18use composefs_ctl::composefs;
19use etc_merge::{Diff, compute_diff, merge, traverse_etc};
20use rustix::fs::{Mode, OFlags, RenameFlags, fsync, renameat, renameat_with};
21
22use fn_error_context::context;
23
24pub(crate) async fn get_etc_diff(
25 storage: &Storage,
26 booted_cfs: &BootedComposefs,
27 new_etc: Option<&Dir>,
28) -> Result<Diff> {
29 let host = get_composefs_status(storage, booted_cfs).await?;
30 let booted_composefs = host.require_composefs_booted()?;
31
32 let sysroot_fd = storage.physical_root.reopen_as_ownedfd()?;
34 let composefs_fd = mount_composefs_image(
35 &sysroot_fd,
36 &booted_composefs.verity,
37 booted_cfs.cmdline.allow_missing_fsverity,
38 )?;
39
40 let erofs_tmp_mnt = TempMount::mount_fd(&composefs_fd)?;
41
42 let pristine_etc =
43 Dir::open_ambient_dir(erofs_tmp_mnt.dir.path().join("etc"), ambient_authority())?;
44 let current_etc = Dir::open_ambient_dir("/etc", ambient_authority())?;
45
46 let (pristine_files, current_files, new_etc_files) =
47 traverse_etc(&pristine_etc, ¤t_etc, new_etc)?;
48
49 let diff = compute_diff(
50 &pristine_files,
51 ¤t_files,
52 &new_etc_files
53 .as_ref()
54 .unwrap_or(&FileSystem::new(Stat::uninitialized())),
55 )?;
56
57 Ok(diff)
58}
59
60pub(crate) async fn composefs_backend_finalize(
61 storage: &Storage,
62 booted_cfs: &BootedComposefs,
63) -> Result<()> {
64 const COMPOSEFS_FINALIZE_JOURNAL_ID: &str = "0e9d8c7b6a5f4e3d2c1b0a9f8e7d6c5b4";
65
66 tracing::info!(
67 message_id = COMPOSEFS_FINALIZE_JOURNAL_ID,
68 bootc.operation = "finalize",
69 bootc.current_deployment = booted_cfs.cmdline.digest,
70 "Starting composefs staged deployment finalization"
71 );
72
73 let host = get_composefs_status(storage, booted_cfs).await?;
74
75 let booted_composefs = host.require_composefs_booted()?;
76
77 let Some(staged_depl) = host.status.staged.as_ref() else {
78 tracing::info!(
79 message_id = COMPOSEFS_FINALIZE_JOURNAL_ID,
80 bootc.operation = "finalize",
81 "No staged deployment found"
82 );
83 return Ok(());
84 };
85
86 if staged_depl.download_only {
87 tracing::info!(
88 message_id = COMPOSEFS_FINALIZE_JOURNAL_ID,
89 bootc.operation = "finalize",
90 bootc.download_only = "true",
91 "Staged deployment is marked download only. Won't finalize"
92 );
93 return Ok(());
94 }
95
96 let staged_composefs = staged_depl.composefs.as_ref().ok_or(anyhow::anyhow!(
97 "Staged deployment is not a composefs deployment"
98 ))?;
99
100 let mut aboot_state = (staged_composefs.boot_type == BootType::Aboot)
101 .then(|| aboot::AbootState::open(&storage.physical_root))
102 .transpose()?;
103 if let Some(state) = aboot_state.as_ref()
104 && state.read_attempted()?.is_some()
105 {
106 tracing::warn!("Aboot deployment was already attempted; not flashing it again");
107 return Ok(());
108 }
109
110 let sysroot_fd = storage.physical_root.reopen_as_ownedfd()?;
112 let composefs_fd = mount_composefs_image(
113 &sysroot_fd,
114 &booted_composefs.verity,
115 booted_cfs.cmdline.allow_missing_fsverity,
116 )?;
117
118 let erofs_tmp_mnt = TempMount::mount_fd(&composefs_fd)?;
119
120 let pristine_etc =
122 Dir::open_ambient_dir(erofs_tmp_mnt.dir.path().join("etc"), ambient_authority())?;
123 let current_etc = Dir::open_ambient_dir("/etc", ambient_authority())?;
124
125 let new_etc_path = Path::new(STATE_DIR_ABS)
126 .join(&staged_composefs.verity)
127 .join("etc");
128
129 let new_etc = Dir::open_ambient_dir(new_etc_path, ambient_authority())?;
130
131 let (pristine_files, current_files, new_files) =
132 traverse_etc(&pristine_etc, ¤t_etc, Some(&new_etc))?;
133
134 let new_files =
135 new_files.ok_or_else(|| anyhow::anyhow!("Failed to get dirtree for new etc"))?;
136
137 let diff = compute_diff(&pristine_files, ¤t_files, &new_files)?;
138 merge(¤t_etc, ¤t_files, &new_etc, &new_files, &diff)?;
139
140 new_etc
144 .remove_file_optional(".updated")
145 .context("Removing /etc/.updated from staged deployment")?;
146
147 drop(erofs_tmp_mnt);
149
150 if staged_composefs.boot_type == BootType::Aboot {
151 return finalize_staged_aboot(
152 storage,
153 aboot_state.as_mut().unwrap(),
154 &staged_composefs.verity,
155 );
156 }
157
158 let boot_mount = storage.require_boot_mount()?;
159
160 if boot_mount.fd.exists(STAGED_GLOBAL_UKI_ADDONS_DIR) {
162 if boot_mount.fd.exists(GLOBAL_UKI_ADDONS_DIR) {
163 tracing::debug!(
164 "Atomically exchanging {STAGED_GLOBAL_UKI_ADDONS_DIR} and {GLOBAL_UKI_ADDONS_DIR}"
165 );
166
167 renameat_with(
168 &boot_mount.fd,
169 STAGED_GLOBAL_UKI_ADDONS_DIR,
170 &boot_mount.fd,
171 GLOBAL_UKI_ADDONS_DIR,
172 RenameFlags::EXCHANGE,
173 )
174 .context("Atomically exchanging staged Global UKI Addons")?;
175
176 boot_mount
177 .fd
178 .remove_dir_all(STAGED_GLOBAL_UKI_ADDONS_DIR)
179 .context("Removing staged Global UKI Addons dir")?;
180 } else {
181 tracing::debug!(
182 "No global addons in the current deployment. Finalizing global UKI Addons"
183 );
184
185 renameat(
186 &boot_mount.fd,
187 STAGED_GLOBAL_UKI_ADDONS_DIR,
188 &boot_mount.fd,
189 GLOBAL_UKI_ADDONS_DIR,
190 )
191 .context("Finalizing global UKI Addons")?;
192 }
193 }
194
195 let boot_dir = storage.require_boot_dir()?;
196
197 match booted_composefs.bootloader.kind()? {
198 BootloaderKind::GRUBClassic => match staged_composefs.boot_type {
199 BootType::Bls => {
200 let entries_dir = boot_dir.open_dir("loader")?;
201 rename_exchange_bls_entries(&entries_dir)?;
202 }
203 BootType::Uki => finalize_staged_grub_uki(boot_dir)?,
204 BootType::Aboot => unreachable!(),
205 },
206
207 BootloaderKind::BLSCompatible => {
208 let entries_dir = boot_dir.open_dir("loader")?;
209 rename_exchange_bls_entries(&entries_dir)?;
210 }
211 };
212
213 composefs_gc(
216 storage,
217 booted_cfs,
218 GCOpts {
219 dry_run: false,
220 prune_repo: false,
221 },
222 )
223 .await?;
224
225 Ok(())
226}
227
228pub(crate) fn hold_boot() -> Result<()> {
242 let path = format!("/{BOOT}");
243 let _fd = rustix::fs::open(
244 path.as_str(),
245 OFlags::RDONLY | OFlags::DIRECTORY | OFlags::CLOEXEC,
246 Mode::empty(),
247 )
248 .with_context(|| format!("Opening {path}"))?;
249 tracing::debug!("Holding {path} open until terminated");
250 loop {
251 std::thread::park();
252 }
253}
254
255#[context("Finalizing staged aboot deployment {deployment}")]
256fn finalize_staged_aboot(
257 storage: &Storage,
258 state: &mut aboot::AbootState<'_>,
259 deployment: &str,
260) -> Result<()> {
261 let pending = state
262 .read_pending()?
263 .ok_or_else(|| anyhow::anyhow!("No pending aboot deployment"))?;
264 anyhow::ensure!(
265 pending.depl_id == deployment,
266 "Pending aboot deployment changed"
267 );
268 anyhow::ensure!(
269 !pending.finalization_locked,
270 "Pending aboot deployment is download-only"
271 );
272 let artifacts = state.verify_artifacts(&storage.physical_root_path, &pending)?;
273 let kernel_cmdline =
274 std::fs::read_to_string("/proc/cmdline").context("Reading kernel command line")?;
275 let booted_slot = aboot::Slot::from_cmdline(&linux_kernel_cmdline::utf8::Cmdline::from(
276 kernel_cmdline.as_str(),
277 ));
278
279 state.record_attempt(deployment)?;
280 if let Some(slot) = booted_slot {
281 state.invalidate_other(slot)?;
282 }
283
284 let mut command = Command::new("aboot-deploy");
285 command.args(["--local", "--boot-image"]);
286 command.arg(artifacts.boot);
287 if let Some(vbmeta) = artifacts.vbmeta {
288 command.arg("--vbmeta-image").arg(vbmeta);
289 }
290 let status = command.status().context("Running aboot-deploy")?;
291 anyhow::ensure!(status.success(), "aboot-deploy exited with status {status}");
292 Ok(())
293}
294
295#[context("Grub: Finalizing staged UKI")]
296fn finalize_staged_grub_uki(boot_fd: &Dir) -> Result<()> {
297 let entries_dir = boot_fd.open_dir("grub2")?;
298 rename_exchange_user_cfg(&entries_dir)?;
299
300 let entries_dir = entries_dir.reopen_as_ownedfd()?;
301 fsync(entries_dir).context("fsync")?;
302
303 Ok(())
304}