Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 25 additions & 26 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ clap_mangen = { version = "0.3.0" }
# pulls), which drags in rustls-webpki/untrusted/webpki-root-certs whose
# licenses aren't in our cargo-deny allow list. Re-enable once that's sorted.
# See: https://github.com/bootc-dev/bootc/pull/2295
composefs-ctl = { git = "https://github.com/composefs/composefs-rs", rev = "0fbc853325ff92dbb842e715611bed7ddf9fe169", default-features = false, features = [
composefs-ctl = { git = "https://github.com/composefs/composefs-rs", tag = "v0.9.0", default-features = false, features = [
"pre-6.15",
"pre-6.16",
"oci",
Expand Down
133 changes: 104 additions & 29 deletions crates/lib/src/bootc_composefs/boot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,23 @@ const AUTH_EXT: &str = "auth";
/// This is relative to the ESP
pub(crate) const BOOTC_UKI_DIR: &str = "EFI/Linux/bootc";

/// Directory (relative to the ESP) where systemd-stub looks for UKI addons that apply
/// to *every* UKI, as opposed to addons scoped to a single UKI (which live alongside
/// it under [`BOOTC_UKI_DIR`]). Unlike per-UKI addons, these aren't tied to a single
/// deployment, so they're neither namespaced by deployment verity nor cleaned up by GC.
///
/// TODO: This directory is shared, unscoped machine state (any systemd-stub UKI on the
/// ESP will load whatever's here), but we currently treat it like deployment-owned
/// content: we blindly overwrite same-named files with no ownership tracking, we only
/// (re)install addons on `install` (not on upgrade, see `uki_addons` being hardcoded to
/// `None` for `BootSetupType::Upgrade` below), and GC never removes stale entries here.
/// Before recommending this feature for real use we should track which files here are
/// bootc-owned, reconcile that set on every upgrade (installing newly-selected addons,
/// removing ones we own that are no longer selected/present), and decide/document how
/// this interacts with deployment rollback (a global addon update isn't reverted by
/// rolling back to an older deployment).
pub(crate) const GLOBAL_UKI_ADDONS_DIR: &str = "loader/addons";

#[derive(thiserror::Error, Debug)]
#[error("The UKI has the wrong composefs= parameter (is '{actual}', should be '{expected}')")]
pub(crate) struct UKIDigestMismatch {
Expand Down Expand Up @@ -852,6 +869,41 @@ struct UKIInfo {
boot_digest: String,
}

/// Determines the directory (under `mounted_efi`) that a PE binary should be written to.
///
/// - A UKI, or an addon scoped to a single UKI, is namespaced under [`BOOTC_UKI_DIR`] by
/// the deployment's verity digest, so it doesn't collide with other deployments.
/// - A global UKI addon applies to every UKI, so it's written to the shared
/// [`GLOBAL_UKI_ADDONS_DIR`] instead.
fn pe_output_dir(
pe_type: &PEType,
mounted_efi: &Path,
file_path: &Utf8Path,
uki_id: &Sha512HashValue,
) -> PathBuf {
if matches!(pe_type, PEType::GlobalUkiAddon) {
return mounted_efi.join(GLOBAL_UKI_ADDONS_DIR);
}

let efi_linux_path = mounted_efi.join(BOOTC_UKI_DIR);

match file_path.parent() {
Some(parent) if parent.as_str().ends_with(EFI_ADDON_DIR_EXT) => {
let dir_name = get_uki_addon_dir_name(&uki_id.to_hex());
let renamed_path = parent
.parent()
.map(|p| p.join(&dir_name))
.unwrap_or(dir_name.into());

efi_linux_path.join(renamed_path)
}

Some(parent) => efi_linux_path.join(parent),

None => efi_linux_path,
}
}

/// Writes a PortableExecutable to ESP along with any PE specific or Global addons
#[context("Writing {file_path} to ESP")]
fn write_pe_to_esp(
Expand Down Expand Up @@ -934,39 +986,15 @@ fn write_pe_to_esp(
});
}

let efi_linux_path = mounted_efi.as_ref().join(BOOTC_UKI_DIR);
create_dir_all(&efi_linux_path).context("Creating bootc UKI directory")?;

let final_pe_path = match file_path.parent() {
Some(parent) => {
let renamed_path = match parent.as_str().ends_with(EFI_ADDON_DIR_EXT) {
true => {
let dir_name = get_uki_addon_dir_name(&uki_id.to_hex());

parent
.parent()
.map(|p| p.join(&dir_name))
.unwrap_or(dir_name.into())
}

false => parent.to_path_buf(),
};

let full_path = efi_linux_path.join(renamed_path);
create_dir_all(&full_path)?;

full_path
}

None => efi_linux_path,
};
let final_pe_path = pe_output_dir(&pe_type, mounted_efi.as_ref(), file_path, uki_id);
create_dir_all(&final_pe_path).with_context(|| format!("Creating {final_pe_path:?}"))?;

let pe_dir = Dir::open_ambient_dir(&final_pe_path, ambient_authority())
.with_context(|| format!("Opening {final_pe_path:?}"))?;

let pe_name = match pe_type {
PEType::Uki => &get_uki_name(&uki_id.to_hex()),
PEType::UkiAddon => file_path
PEType::UkiAddon | PEType::GlobalUkiAddon => file_path
.components()
.last()
.ok_or_else(|| anyhow::anyhow!("Failed to get UKI Addon file name"))?
Expand Down Expand Up @@ -1182,6 +1210,10 @@ pub(crate) fn setup_composefs_uki_boot(
esp_dev.path(),
bootloader,
booted_cfs.cmdline.allow_missing_fsverity,
// TODO: We never (re)install UKI addons on upgrade, only on initial
// `install`. This is especially relevant for global addons (see the
// TODO on `GLOBAL_UKI_ADDONS_DIR`): if a newer image changes or drops
// one, the ESP copy is never reconciled.
None,
)
}
Expand All @@ -1199,8 +1231,9 @@ pub(crate) fn setup_composefs_uki_boot(
}

ComposefsBootEntry::Type2(entry) => {
// If --uki-addon is not passed, we don't install any addon
if matches!(entry.pe_type, PEType::UkiAddon) {
// If --uki-addon is not passed, we don't install any addon (whether
// it's scoped to this UKI or a global one)
if matches!(entry.pe_type, PEType::UkiAddon | PEType::GlobalUkiAddon) {
let Some(addons) = uki_addons else {
continue;
};
Expand Down Expand Up @@ -1703,6 +1736,48 @@ pub(crate) async fn setup_composefs_boot(
mod tests {
use super::*;

#[test]
fn test_pe_output_dir() {
let mounted_efi = Path::new("/esp");
let uki_id = Sha512HashValue::EMPTY;
let uki_hex = uki_id.to_hex();

// Boot entry paths are relative to the directory they were discovered in
// (e.g. "/boot/EFI/Linux" or "/boot/loader/addons"), not absolute filesystem paths.

// A UKI itself always lands directly in BOOTC_UKI_DIR.
assert_eq!(
pe_output_dir(&PEType::Uki, mounted_efi, Utf8Path::new("foo.efi"), &uki_id),
mounted_efi.join(BOOTC_UKI_DIR)
);

// A per-UKI addon (nested under a `<name>.efi.extra.d` directory) gets
// renamed into a directory namespaced by the UKI's verity digest.
assert_eq!(
pe_output_dir(
&PEType::UkiAddon,
mounted_efi,
Utf8Path::new("foo.efi.extra.d/bar.addon.efi"),
&uki_id
),
mounted_efi
.join(BOOTC_UKI_DIR)
.join(get_uki_addon_dir_name(&uki_hex))
);

// A global UKI addon is written to the shared addons directory, not
// namespaced by any particular UKI's verity digest.
assert_eq!(
pe_output_dir(
&PEType::GlobalUkiAddon,
mounted_efi,
Utf8Path::new("bar.addon.efi"),
&uki_id
),
mounted_efi.join(GLOBAL_UKI_ADDONS_DIR)
);
}

#[test]
fn test_type1_filename_generation() {
// Test basic os_id without hyphens
Expand Down
5 changes: 3 additions & 2 deletions crates/lib/src/bootc_composefs/gc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -138,8 +138,9 @@ fn delete_kernel_initrd(storage: &Storage, dir_to_delete: &str, dry_run: bool) -
fn delete_uki(storage: &Storage, uki_id: &str, dry_run: bool) -> Result<()> {
let esp_mnt = storage.require_esp()?;

// NOTE: We don't delete global addons here
// Which is fine as global addons don't belong to any single deployment
// NOTE: We don't delete global addons here (see `GLOBAL_UKI_ADDONS_DIR`)
// Which is fine as global addons don't belong to any single deployment, but it also
// means they're never cleaned up at all: see the TODO on `GLOBAL_UKI_ADDONS_DIR`.
let uki_dir = esp_mnt.fd.open_dir(BOOTC_UKI_DIR)?;

for entry in uki_dir.entries_utf8()? {
Expand Down
Loading