Skip to content

Commit 35b8326

Browse files
composefs: Handle backwads compatibility with older versions
While finishing up GC, we had come up with the idea of prepending our boot binaries (UKI PEs, BLS directories) with a certain prefix and we ended up hard requiring these prefixes. If someone has an older version of bootc which they used to install their system with, then upgrade to a new version, many if not all of the important operations would cease to work. This basically handles the backwards compatibility of new binaries on older systems by prepending our custom prefix to all existing boot binaries Signed-off-by: Pragyan Poudyal <pragyanpoudyal41999@gmail.com>
1 parent 2db3be9 commit 35b8326

File tree

6 files changed

+421
-15
lines changed

6 files changed

+421
-15
lines changed
Lines changed: 389 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,389 @@
1+
use std::io::{Read, Write};
2+
3+
use crate::{
4+
bootc_composefs::{
5+
boot::{
6+
BOOTC_UKI_DIR, BootType, FILENAME_PRIORITY_PRIMARY, FILENAME_PRIORITY_SECONDARY,
7+
get_efi_uuid_source, get_uki_name, parse_os_release, type1_entry_conf_file_name,
8+
},
9+
rollback::{rename_exchange_bls_entries, rename_exchange_user_cfg},
10+
status::{get_bootloader, get_sorted_grub_uki_boot_entries, get_sorted_type1_boot_entries},
11+
},
12+
composefs_consts::{
13+
ORIGIN_KEY_BOOT, ORIGIN_KEY_BOOT_TYPE, STATE_DIR_RELATIVE, TYPE1_BOOT_DIR_PREFIX,
14+
TYPE1_ENT_PATH_STAGED, UKI_NAME_PREFIX, USER_CFG_STAGED,
15+
},
16+
parsers::bls_config::{BLSConfig, BLSConfigType},
17+
spec::Bootloader,
18+
store::{BootedComposefs, Storage},
19+
};
20+
use anyhow::{Context, Result};
21+
use camino::Utf8PathBuf;
22+
use cap_std_ext::{cap_std::fs::Dir, dirext::CapStdExtDirExt};
23+
use cfsctl::composefs_boot::bootloader::{EFI_ADDON_DIR_EXT, EFI_EXT};
24+
use fn_error_context::context;
25+
use ocidir::cap_std::ambient_authority;
26+
use rustix::fs::{RenameFlags, fsync, renameat_with};
27+
28+
/// Represents a pending rename operation to be executed atomically
29+
#[derive(Debug)]
30+
struct PendingRename {
31+
old_name: String,
32+
new_name: String,
33+
}
34+
35+
/// Transaction context for managing atomic renames (both files and directories)
36+
#[derive(Debug)]
37+
struct RenameTransaction {
38+
operations: Vec<PendingRename>,
39+
}
40+
41+
impl RenameTransaction {
42+
fn new() -> Self {
43+
Self {
44+
operations: Vec::new(),
45+
}
46+
}
47+
48+
fn add_operation(&mut self, old_name: String, new_name: String) {
49+
self.operations.push(PendingRename { old_name, new_name });
50+
}
51+
52+
/// Execute all renames atomically in the provided directory
53+
/// If any operation fails, attempt to rollback all completed operations
54+
///
55+
/// We currently only have two entries at max, so this is quite unlikely to fail...
56+
#[context("Executing rename transactions")]
57+
fn execute_transaction(&self, target_dir: &Dir) -> Result<()> {
58+
let mut completed_operations = Vec::new();
59+
60+
for op in &self.operations {
61+
match renameat_with(
62+
target_dir,
63+
&op.old_name,
64+
target_dir,
65+
&op.new_name,
66+
RenameFlags::empty(),
67+
) {
68+
Ok(()) => {
69+
completed_operations.push(op);
70+
tracing::debug!("Renamed {} -> {}", op.old_name, op.new_name);
71+
}
72+
Err(e) => {
73+
// Attempt rollback of completed operations
74+
for completed_op in completed_operations.iter().rev() {
75+
if let Err(rollback_err) = renameat_with(
76+
target_dir,
77+
&completed_op.new_name,
78+
target_dir,
79+
&completed_op.old_name,
80+
RenameFlags::EXCHANGE,
81+
) {
82+
tracing::error!(
83+
"Rollback failed for {} -> {}: {}",
84+
completed_op.new_name,
85+
completed_op.old_name,
86+
rollback_err
87+
);
88+
}
89+
}
90+
91+
return Err(e).context(format!("Failed to rename {}", op.old_name));
92+
}
93+
}
94+
}
95+
96+
Ok(())
97+
}
98+
}
99+
100+
/// Plan EFI binary renames and populate the transaction
101+
/// The actual renames are deferred to the transaction
102+
#[context("Planning EFI renames")]
103+
fn plan_efi_binary_renames(
104+
esp: &Dir,
105+
digest: &str,
106+
rename_transaction: &mut RenameTransaction,
107+
) -> Result<()> {
108+
let bootc_uki_dir = esp.open_dir(BOOTC_UKI_DIR)?;
109+
110+
for entry in bootc_uki_dir.entries_utf8()? {
111+
let entry = entry?;
112+
let filename = entry.file_name()?;
113+
114+
if filename.starts_with(UKI_NAME_PREFIX) {
115+
continue;
116+
}
117+
118+
if !filename.ends_with(EFI_EXT) && !filename.ends_with(EFI_ADDON_DIR_EXT) {
119+
continue;
120+
}
121+
122+
if !filename.contains(digest) {
123+
continue;
124+
}
125+
126+
let new_name = format!("{UKI_NAME_PREFIX}{filename}");
127+
rename_transaction.add_operation(filename.to_string(), new_name);
128+
}
129+
130+
Ok(())
131+
}
132+
133+
/// Plan BLS directory renames and populate the transaction
134+
/// The actual renames are deferred to the transaction
135+
#[context("Planning BLS directory renames")]
136+
fn plan_bls_entry_rename(binaries_dir: &Dir, entry_to_fix: &str) -> Result<Option<String>> {
137+
for entry in binaries_dir.entries_utf8()? {
138+
let entry = entry?;
139+
let filename = entry.file_name()?;
140+
141+
// We don't really put any files here, but just in case
142+
if !entry.file_type()?.is_dir() {
143+
continue;
144+
}
145+
146+
if filename != entry_to_fix {
147+
continue;
148+
}
149+
150+
let new_name = format!("{TYPE1_BOOT_DIR_PREFIX}{filename}");
151+
return Ok(Some(new_name));
152+
}
153+
154+
Ok(None)
155+
}
156+
157+
#[context("Staging BLS entry changes")]
158+
fn stage_bls_entry_changes(
159+
storage: &Storage,
160+
boot_dir: &Dir,
161+
entries: &Vec<BLSConfig>,
162+
booted_cfs: &BootedComposefs,
163+
) -> Result<(RenameTransaction, Vec<(String, BLSConfig)>)> {
164+
let mut rename_transaction = RenameTransaction::new();
165+
166+
let root = Dir::open_ambient_dir("/", ambient_authority())?;
167+
let osrel = parse_os_release(&root)?;
168+
169+
let os_id = osrel
170+
.as_ref()
171+
.map(|(s, _, _)| s.as_str())
172+
.unwrap_or("bootc");
173+
174+
// to not add duplicate transactions since we share BLS entries
175+
// across deployements
176+
let mut fixed = vec![];
177+
let mut new_bls_entries = vec![];
178+
179+
for entry in entries {
180+
let (digest, has_prefix) = entry.boot_artifact_info()?;
181+
let digest = digest.to_string();
182+
183+
if has_prefix || fixed.contains(&digest) {
184+
continue;
185+
}
186+
187+
let mut new_entry = entry.clone();
188+
189+
let conf_filename = if *booted_cfs.cmdline.digest == digest {
190+
type1_entry_conf_file_name(os_id, new_entry.version(), FILENAME_PRIORITY_PRIMARY)
191+
} else {
192+
type1_entry_conf_file_name(os_id, new_entry.version(), FILENAME_PRIORITY_SECONDARY)
193+
};
194+
195+
match &mut new_entry.cfg_type {
196+
BLSConfigType::NonEFI { linux, initrd, .. } => {
197+
let new_name =
198+
plan_bls_entry_rename(&storage.bls_boot_binaries_dir()?, &digest)?
199+
.ok_or_else(|| anyhow::anyhow!("Directory for entry {digest} not found"))?;
200+
201+
rename_transaction.add_operation(digest.clone(), new_name.clone());
202+
203+
*linux = linux.as_str().replace(&digest, &new_name).into();
204+
*initrd = initrd
205+
.iter_mut()
206+
.map(|path| path.as_str().replace(&digest, &new_name).into())
207+
.collect();
208+
}
209+
210+
BLSConfigType::EFI { efi, .. } => {
211+
// boot_dir in case of UKI is the ESP
212+
plan_efi_binary_renames(&boot_dir, &digest, &mut rename_transaction)?;
213+
*efi = Utf8PathBuf::from("/")
214+
.join(BOOTC_UKI_DIR)
215+
.join(get_uki_name(&digest));
216+
}
217+
218+
_ => anyhow::bail!("Expected NonEFI config"),
219+
}
220+
221+
new_bls_entries.push((conf_filename, new_entry));
222+
fixed.push(digest.into());
223+
}
224+
225+
Ok((rename_transaction, new_bls_entries))
226+
}
227+
228+
fn create_staged_bls_entries(boot_dir: &Dir, entries: &Vec<(String, BLSConfig)>) -> Result<()> {
229+
boot_dir.create_dir_all(TYPE1_ENT_PATH_STAGED)?;
230+
let staged_entries = boot_dir.open_dir(TYPE1_ENT_PATH_STAGED)?;
231+
232+
for (filename, new_entry) in entries {
233+
staged_entries.atomic_write(filename, new_entry.to_string().as_bytes())?;
234+
}
235+
236+
fsync(staged_entries.reopen_as_ownedfd()?).context("fsync")
237+
}
238+
239+
fn get_boot_type(storage: &Storage, booted_cfs: &BootedComposefs) -> Result<BootType> {
240+
let mut config = String::new();
241+
242+
let origin_path = Utf8PathBuf::from(STATE_DIR_RELATIVE)
243+
.join(&*booted_cfs.cmdline.digest)
244+
.join(format!("{}.origin", booted_cfs.cmdline.digest));
245+
246+
storage
247+
.physical_root
248+
.open(origin_path)
249+
.context("Opening origin file")?
250+
.read_to_string(&mut config)
251+
.context("Reading origin file")?;
252+
253+
let origin = tini::Ini::from_string(&config)
254+
.with_context(|| format!("Failed to parse origin as ini"))?;
255+
256+
let boot_type = match origin.get::<String>(ORIGIN_KEY_BOOT, ORIGIN_KEY_BOOT_TYPE) {
257+
Some(s) => BootType::try_from(s.as_str())?,
258+
None => anyhow::bail!("{ORIGIN_KEY_BOOT} not found"),
259+
};
260+
261+
Ok(boot_type)
262+
}
263+
264+
fn handle_bls_conf(
265+
storage: &Storage,
266+
booted_cfs: &BootedComposefs,
267+
boot_dir: &Dir,
268+
is_uki: bool,
269+
) -> Result<()> {
270+
let entries = get_sorted_type1_boot_entries(boot_dir, true)?;
271+
let (rename_transaction, new_bls_entries) =
272+
stage_bls_entry_changes(storage, boot_dir, &entries, booted_cfs)?;
273+
274+
if rename_transaction.operations.is_empty() {
275+
tracing::debug!("Nothing to do");
276+
return Ok(());
277+
}
278+
279+
create_staged_bls_entries(boot_dir, &new_bls_entries)?;
280+
281+
let binaries_dir = if is_uki {
282+
let esp = storage.require_esp()?;
283+
let uki_dir = esp.fd.open_dir(BOOTC_UKI_DIR).context("Opening UKI dir")?;
284+
285+
uki_dir
286+
} else {
287+
storage.bls_boot_binaries_dir()?
288+
};
289+
290+
// execute all EFI PE renames atomically before the final exchange
291+
rename_transaction
292+
.execute_transaction(&binaries_dir)
293+
.context("Failed to execute EFI binary rename transaction")?;
294+
295+
fsync(binaries_dir.reopen_as_ownedfd()?)?;
296+
297+
let loader_dir = boot_dir.open_dir("loader").context("Opening loader dir")?;
298+
rename_exchange_bls_entries(&loader_dir)?;
299+
300+
Ok(())
301+
}
302+
303+
/// Goes through the ESP and prepends every UKI/Addon with our custom prefix
304+
/// Goes through the BLS entries and prepends our custom prefix
305+
#[context("Prepending custom prefix to EFI and BLS entries")]
306+
pub(crate) async fn prepend_custom_prefix(
307+
storage: &Storage,
308+
booted_cfs: &BootedComposefs,
309+
) -> Result<()> {
310+
let boot_dir = storage.require_boot_dir()?;
311+
312+
let bootloader = get_bootloader()?;
313+
314+
match get_boot_type(storage, booted_cfs)? {
315+
BootType::Bls => {
316+
handle_bls_conf(storage, booted_cfs, boot_dir, false)?;
317+
}
318+
319+
BootType::Uki => match bootloader {
320+
Bootloader::Grub => {
321+
let esp = storage.require_esp()?;
322+
323+
let mut buf = String::new();
324+
let menuentries = get_sorted_grub_uki_boot_entries(boot_dir, &mut buf)?;
325+
326+
let mut new_menuentries = vec![];
327+
let mut rename_transaction = RenameTransaction::new();
328+
329+
for entry in menuentries {
330+
let (digest, has_prefix) = entry.boot_artifact_info()?;
331+
let digest = digest.to_string();
332+
333+
if has_prefix {
334+
continue;
335+
}
336+
337+
plan_efi_binary_renames(&esp.fd, &digest, &mut rename_transaction)?;
338+
339+
let new_path = Utf8PathBuf::from("/")
340+
.join(BOOTC_UKI_DIR)
341+
.join(get_uki_name(&digest));
342+
343+
let mut new_entry = entry.clone();
344+
new_entry.body.chainloader = new_path.into();
345+
346+
new_menuentries.push(new_entry);
347+
}
348+
349+
if rename_transaction.operations.is_empty() {
350+
tracing::debug!("Nothing to do");
351+
return Ok(());
352+
}
353+
354+
let grub_dir = boot_dir.open_dir("grub2").context("opening boot/grub2")?;
355+
356+
grub_dir
357+
.atomic_replace_with(USER_CFG_STAGED, |f| -> std::io::Result<_> {
358+
f.write_all(get_efi_uuid_source().as_bytes())?;
359+
360+
for entry in new_menuentries {
361+
f.write_all(entry.to_string().as_bytes())?;
362+
}
363+
364+
Ok(())
365+
})
366+
.with_context(|| format!("Writing to {USER_CFG_STAGED}"))?;
367+
368+
let esp = storage.require_esp()?;
369+
let uki_dir = esp.fd.open_dir(BOOTC_UKI_DIR).context("Opening UKI dir")?;
370+
371+
// execute all EFI PE renames atomically before the final exchange
372+
rename_transaction
373+
.execute_transaction(&uki_dir)
374+
.context("Failed to execute EFI binary rename transaction")?;
375+
376+
fsync(uki_dir.reopen_as_ownedfd()?)?;
377+
rename_exchange_user_cfg(&grub_dir)?;
378+
}
379+
380+
Bootloader::Systemd => {
381+
handle_bls_conf(storage, booted_cfs, boot_dir, true)?;
382+
}
383+
384+
Bootloader::None => unreachable!("Checked at install time"),
385+
},
386+
};
387+
388+
Ok(())
389+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
pub(crate) mod bcompat_boot;

crates/lib/src/bootc_composefs/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
pub(crate) mod backwards_compat;
12
pub(crate) mod boot;
23
pub(crate) mod delete;
34
pub(crate) mod digest;

0 commit comments

Comments
 (0)