From 55f54c875b995b1cee89543ebe31d96cea27cad4 Mon Sep 17 00:00:00 2001 From: Johan-Liebert1 Date: Sun, 24 May 2026 20:35:47 +0530 Subject: [PATCH 1/5] bootloader: Add GrubCC bootloader Add GrubCC (Grub ConfidentialClusters) as a new bootloader option. This is a minimal version of grub that's supposed to work exactly like systemd-boot Fixes: #2212 Signed-off-by: Johan-Liebert1 --- Dockerfile | 15 +++++++ .../backwards_compat/bcompat_boot.rs | 2 +- crates/lib/src/bootc_composefs/boot.rs | 43 +++++++++++++++++-- crates/lib/src/bootc_composefs/delete.rs | 2 +- crates/lib/src/bootc_composefs/finalize.rs | 2 +- crates/lib/src/bootc_composefs/rollback.rs | 2 +- crates/lib/src/bootc_composefs/status.rs | 14 +++--- crates/lib/src/bootc_composefs/update.rs | 2 +- crates/lib/src/install.rs | 3 +- crates/lib/src/install/baseline.rs | 2 +- crates/lib/src/spec.rs | 4 ++ crates/lib/src/store/mod.rs | 7 ++- crates/xtask/src/xtask.rs | 3 ++ 13 files changed, 84 insertions(+), 17 deletions(-) diff --git a/Dockerfile b/Dockerfile index 31f515800..8c07bd102 100644 --- a/Dockerfile +++ b/Dockerfile @@ -59,6 +59,7 @@ COPY --from=target-base /target-rootfs/ / ARG SKIP_CONFIGS ARG boot_type ARG seal_state +ARG bootloader # All network-fetching operations: package installs from distro repos, Copr, Koji. # Separated so `just build-fetch --target=fetch` can be retried independently on # transient network failures without re-running the configuration phase. @@ -89,6 +90,20 @@ RUN --mount=type=tmpfs,target=/run --mount=type=tmpfs,target=/tmp \ if [[ ${#pkgs_to_install[@]} -gt 0 ]]; then dnf install -y "${pkgs_to_install[@]}" fi + + if [[ "$bootloader" == "grub-cc" ]]; then + # We have this until we get grub-cc support in bootupd + arch=$(uname -m) + curl -L -o /var/grub-cc.rpm "https://kojipkgs.fedoraproject.org/packages/grub2/2.12/59.eln156/x86_64/grub2-efi-x64-cc-2.12-59.eln156.${arch}.rpm" + mkdir /var/grub-cc + rpm2archive /var/grub-cc.rpm | tar -xvz -C /var/grub-cc + file=$(find /var/grub-cc -name '*.efi') + mkdir /usr/lib/grub-cc + cp $file /usr/lib/grub-cc + rm -rvf /var/grub-cc + rm -rvf /var/grub-cc.rpm + fi + EOF # Note we don't do any customization here yet diff --git a/crates/lib/src/bootc_composefs/backwards_compat/bcompat_boot.rs b/crates/lib/src/bootc_composefs/backwards_compat/bcompat_boot.rs index f624bbead..765972611 100644 --- a/crates/lib/src/bootc_composefs/backwards_compat/bcompat_boot.rs +++ b/crates/lib/src/bootc_composefs/backwards_compat/bcompat_boot.rs @@ -384,7 +384,7 @@ pub(crate) async fn prepend_custom_prefix( rename_exchange_user_cfg(&grub_dir)?; } - Bootloader::Systemd => { + Bootloader::Systemd | Bootloader::GrubCC => { handle_bls_conf(storage, cfs_cmdline, boot_dir, true)?; } diff --git a/crates/lib/src/bootc_composefs/boot.rs b/crates/lib/src/bootc_composefs/boot.rs index f4a020c5e..6c13581cc 100644 --- a/crates/lib/src/bootc_composefs/boot.rs +++ b/crates/lib/src/bootc_composefs/boot.rs @@ -607,7 +607,7 @@ pub(crate) fn setup_composefs_bls_boot( ) } - Bootloader::Systemd => { + Bootloader::Systemd | Bootloader::GrubCC => { let efi_mount = mount_esp(&esp_device).context("Mounting ESP")?; let mounted_efi = Utf8PathBuf::from(efi_mount.dir.path().as_str()?); @@ -1169,7 +1169,9 @@ pub(crate) fn setup_composefs_uki_boot( write_grub_uki_menuentry(root_path, &setup_type, uki_info.boot_label, id, &esp_device)? } - Bootloader::Systemd => write_systemd_uki_config(&esp_mount.fd, &setup_type, uki_info, id)?, + Bootloader::Systemd | Bootloader::GrubCC => { + write_systemd_uki_config(&esp_mount.fd, &setup_type, uki_info, id)? + } Bootloader::None => unreachable!("Checked at install time"), }; @@ -1369,13 +1371,48 @@ pub(crate) async fn setup_composefs_boot( &root_setup.device_info.require_single_root()?, boot_uuid, )?; - } else if postfetch.detected_bootloader == Bootloader::Grub { + } else if matches!( + postfetch.detected_bootloader, + Bootloader::Grub | Bootloader::GrubCC + ) { crate::bootloader::install_via_bootupd( &root_setup.device_info, &root_setup.physical_root_path, &state.config_opts, None, )?; + + // FIXME: Remove this hack once we have support in bootupd + if matches!(postfetch.detected_bootloader, Bootloader::GrubCC) { + root_setup + .physical_root + .remove_dir_all("boot/grub2") + .context("removing grub2")?; + + let (os_id, ..) = parse_os_release(mounted_root.dir())? + .ok_or_else(|| anyhow::anyhow!("Failed to parse os-release"))?; + + let dir = format!("EFI/{os_id}"); + + // Files are in EFI// + let efis_dir = mounted_root + .open_esp_dir() + .context("opening esp")? + .open_dir(&dir) + .with_context(|| format!("Opening {dir}"))?; + + efis_dir + .remove_file_optional("bootuuid.cfg") + .context("Removing bootuuid.cfg")?; + efis_dir + .remove_file_optional("grub.cfg") + .context("Removing grub.cfg")?; + + mounted_root + .dir() + .copy("usr/lib/grub-cc/grubx64-cc.efi", &efis_dir, "grubx64.efi") + .context("Copying grub-cc binary")?; + } } else { crate::bootloader::install_systemd_boot( &mounted_root, diff --git a/crates/lib/src/bootc_composefs/delete.rs b/crates/lib/src/bootc_composefs/delete.rs index da4b61ecb..17a8bafea 100644 --- a/crates/lib/src/bootc_composefs/delete.rs +++ b/crates/lib/src/bootc_composefs/delete.rs @@ -153,7 +153,7 @@ fn delete_depl_boot_entries( } }, - Bootloader::Systemd => { + Bootloader::Systemd | Bootloader::GrubCC => { // For Systemd UKI as well, we use .conf files delete_type1_conf_file(deployment, boot_dir, deleting_staged) } diff --git a/crates/lib/src/bootc_composefs/finalize.rs b/crates/lib/src/bootc_composefs/finalize.rs index e1d67d99f..935da2153 100644 --- a/crates/lib/src/bootc_composefs/finalize.rs +++ b/crates/lib/src/bootc_composefs/finalize.rs @@ -140,7 +140,7 @@ pub(crate) async fn composefs_backend_finalize( BootType::Uki => finalize_staged_grub_uki(boot_dir)?, }, - Bootloader::Systemd => { + Bootloader::Systemd | Bootloader::GrubCC => { let entries_dir = boot_dir.open_dir("loader")?; rename_exchange_bls_entries(&entries_dir)?; } diff --git a/crates/lib/src/bootc_composefs/rollback.rs b/crates/lib/src/bootc_composefs/rollback.rs index 106919f04..5003a6b43 100644 --- a/crates/lib/src/bootc_composefs/rollback.rs +++ b/crates/lib/src/bootc_composefs/rollback.rs @@ -234,7 +234,7 @@ pub(crate) async fn composefs_rollback( } }, - Bootloader::Systemd => { + Bootloader::Systemd | Bootloader::GrubCC => { // We use BLS entries for systemd UKI as well rollback_composefs_entries(boot_dir, rollback_entry.bootloader.clone())?; } diff --git a/crates/lib/src/bootc_composefs/status.rs b/crates/lib/src/bootc_composefs/status.rs index ac1b261dd..52c7673e0 100644 --- a/crates/lib/src/bootc_composefs/status.rs +++ b/crates/lib/src/bootc_composefs/status.rs @@ -368,7 +368,7 @@ pub(crate) fn list_bootloader_entries(storage: &Storage) -> Result list_type1_entries(boot_dir)?, + Bootloader::Systemd | Bootloader::GrubCC => list_type1_entries(boot_dir)?, Bootloader::None => unreachable!("Checked at install time"), }; @@ -414,10 +414,14 @@ pub(crate) fn get_bootloader() -> Result { let bootloader = match read_uefi_var(EFI_LOADER_INFO) { Ok(loader) => { if loader.to_lowercase().contains("systemd-boot") { - Bootloader::Systemd - } else { - Bootloader::Grub + return Ok(Bootloader::Systemd); + } + + if loader.to_lowercase().contains("grub cc") { + return Ok(Bootloader::GrubCC); } + + return Ok(Bootloader::Grub); } Err(efi_error) => match efi_error { @@ -911,7 +915,7 @@ async fn composefs_deployment_status_from( }, // We will have BLS stuff and the UKI stuff in the same DIR - Bootloader::Systemd => { + Bootloader::Systemd | Bootloader::GrubCC => { let bls_configs = get_sorted_type1_boot_entries(boot_dir, true)?; let bls_config = bls_configs .first() diff --git a/crates/lib/src/bootc_composefs/update.rs b/crates/lib/src/bootc_composefs/update.rs index a1fb2722f..78c120c85 100644 --- a/crates/lib/src/bootc_composefs/update.rs +++ b/crates/lib/src/bootc_composefs/update.rs @@ -184,7 +184,7 @@ pub(crate) fn validate_update( } }, - Bootloader::Systemd => rm_staged_type1_ent(boot_dir)?, + Bootloader::Systemd | Bootloader::GrubCC => rm_staged_type1_ent(boot_dir)?, Bootloader::None => unreachable!("Checked at install time"), } diff --git a/crates/lib/src/install.rs b/crates/lib/src/install.rs index c9fcaf88c..9fdb8c37b 100644 --- a/crates/lib/src/install.rs +++ b/crates/lib/src/install.rs @@ -1291,6 +1291,7 @@ pub(crate) fn exec_in_host_mountns(args: &[std::ffi::OsString]) -> Result<()> { Err(Command::new(cmd).args(args).arg0(bootc_utils::NAME).exec()).context("exec")? } +#[derive(Debug)] pub(crate) struct RootSetup { #[cfg(feature = "install-to-disk")] luks_device: Option, @@ -1874,7 +1875,7 @@ async fn install_with_sysroot( Some(&deployment_path.as_str()), )?; } - Bootloader::Systemd => { + Bootloader::Systemd | Bootloader::GrubCC => { anyhow::bail!("bootupd is required for ostree-based installs"); } Bootloader::None => { diff --git a/crates/lib/src/install/baseline.rs b/crates/lib/src/install/baseline.rs index cfd8878e1..25e1b1a65 100644 --- a/crates/lib/src/install/baseline.rs +++ b/crates/lib/src/install/baseline.rs @@ -55,7 +55,7 @@ fn use_discoverable_partitions(state: &State) -> bool { // systemd-boot always supports BLI matches!( state.config_opts.bootloader, - Some(crate::spec::Bootloader::Systemd) + Some(crate::spec::Bootloader::Systemd) | Some(crate::spec::Bootloader::GrubCC) ) } diff --git a/crates/lib/src/spec.rs b/crates/lib/src/spec.rs index ea2c5a77c..45f60d66a 100644 --- a/crates/lib/src/spec.rs +++ b/crates/lib/src/spec.rs @@ -237,6 +237,8 @@ pub enum Bootloader { /// Use Grub as the bootloader #[default] Grub, + /// Use Grub for confidential clusters as the bootloader + GrubCC, /// Use SystemdBoot as the bootloader Systemd, /// Don't use a bootloader managed by bootc @@ -247,6 +249,7 @@ impl Display for Bootloader { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let string = match self { Bootloader::Grub => "grub", + Bootloader::GrubCC => "grub-cc", Bootloader::Systemd => "systemd", Bootloader::None => "none", }; @@ -261,6 +264,7 @@ impl FromStr for Bootloader { fn from_str(value: &str) -> Result { match value { "grub" => Ok(Self::Grub), + "grub-cc" => Ok(Self::GrubCC), "systemd" => Ok(Self::Systemd), "none" => Ok(Self::None), unrecognized => Err(anyhow::anyhow!("Unrecognized bootloader: '{unrecognized}'")), diff --git a/crates/lib/src/store/mod.rs b/crates/lib/src/store/mod.rs index 653fd2a25..8453fb86b 100644 --- a/crates/lib/src/store/mod.rs +++ b/crates/lib/src/store/mod.rs @@ -92,6 +92,7 @@ use std::cell::OnceCell; use std::ops::Deref; +use std::os::fd::{AsFd, AsRawFd}; use std::sync::Arc; use anyhow::{Context, Result}; @@ -335,7 +336,9 @@ impl BootedStorage { let boot_dir = match get_bootloader()? { Bootloader::Grub => physical_root.open_dir("boot").context("Opening boot")?, // NOTE: Handle XBOOTLDR partitions here if and when we use it - Bootloader::Systemd => esp_mount.fd.try_clone().context("Cloning fd")?, + Bootloader::Systemd | Bootloader::GrubCC => { + esp_mount.fd.try_clone().context("Cloning fd")? + } Bootloader::None => unreachable!("Checked at install time"), }; @@ -526,7 +529,7 @@ impl Storage { // the actual binaries inside ESP/EFI/Linux let boot_dir = match get_bootloader()? { Bootloader::Grub => boot_dir.try_clone()?, - Bootloader::Systemd => { + Bootloader::Systemd | Bootloader::GrubCC => { let boot_dir = boot_dir .open_dir(EFI_LINUX) .with_context(|| format!("Opening {EFI_LINUX}"))?; diff --git a/crates/xtask/src/xtask.rs b/crates/xtask/src/xtask.rs index 79f4ffff6..8e54b5772 100644 --- a/crates/xtask/src/xtask.rs +++ b/crates/xtask/src/xtask.rs @@ -151,6 +151,8 @@ pub(crate) struct LocalRustDepsArgs { pub enum Bootloader { /// grub as bootloader Grub, + /// grub cc as bootloader + GrubCC, /// systemd-boot as bootloader Systemd, } @@ -159,6 +161,7 @@ impl Display for Bootloader { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Bootloader::Grub => f.write_str("grub"), + Bootloader::GrubCC => f.write_str("grub-cc"), Bootloader::Systemd => f.write_str("systemd"), } } From 6b0e715372b628e5bfc5cc59d4da797c7cdf7685 Mon Sep 17 00:00:00 2001 From: Pragyan Poudyal Date: Tue, 2 Jun 2026 15:54:58 +0530 Subject: [PATCH 2/5] ci: Add grub-cc to matrix Only run grub-cc tests for f45 for now Signed-off-by: Pragyan Poudyal --- .github/workflows/ci.yml | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 127b67ca7..9f9e87ea5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -217,9 +217,11 @@ jobs: test_os: ${{ fromJson(needs.compute-ci-level.outputs.integration_os_matrix) }} variant: [ostree, composefs] filesystem: ["ext4", "xfs"] - bootloader: ["grub", "systemd"] + # TODO: Remove "grub" once "grub-cc" is stable + bootloader: ["grub", "grub-cc", "systemd"] boot_type: ["bls", "uki"] seal_state: ["sealed", "unsealed"] + exclude: # https://github.com/bootc-dev/bootc/issues/1812 - test_os: centos-9 @@ -245,6 +247,22 @@ jobs: - variant: ostree bootloader: systemd + # For now only have grub-cc tests in F44 + - test_os: fedora-45 + bootloader: grub-cc + - test_os: fedora-43 + bootloader: grub-cc + - test_os: centos-9 + bootloader: grub-cc + - test_os: centos-10 + bootloader: grub-cc + # Not in ostree + - variant: ostree + bootloader: grub-cc + # Not yet "sealed" + - bootloader: grub-cc + seal_state: sealed + runs-on: ubuntu-24.04 steps: From 751c2bb036f1d3d526ceaeb7abecc99d3bf94e04 Mon Sep 17 00:00:00 2001 From: Pragyan Poudyal Date: Wed, 3 Jun 2026 09:08:18 +0530 Subject: [PATCH 3/5] bootloader: Introduce BootloaderKind enum The two variants in this enum distinguish between Grub classic and BLS compatible bootloader (GrubCC and SystemdBoot). This cleans up the match statements where we perform the same op for GrubCC and SystemdBoot Signed-off-by: Pragyan Poudyal --- .../backwards_compat/bcompat_boot.rs | 10 ++++----- crates/lib/src/bootc_composefs/boot.rs | 17 +++++++-------- crates/lib/src/bootc_composefs/delete.rs | 10 ++++----- crates/lib/src/bootc_composefs/finalize.rs | 10 ++++----- crates/lib/src/bootc_composefs/rollback.rs | 10 ++++----- crates/lib/src/bootc_composefs/status.rs | 21 +++++++++---------- crates/lib/src/bootc_composefs/update.rs | 11 +++++----- crates/lib/src/spec.rs | 21 +++++++++++++++++++ crates/lib/src/store/mod.rs | 19 ++++++++--------- 9 files changed, 68 insertions(+), 61 deletions(-) diff --git a/crates/lib/src/bootc_composefs/backwards_compat/bcompat_boot.rs b/crates/lib/src/bootc_composefs/backwards_compat/bcompat_boot.rs index 765972611..9f3e9be36 100644 --- a/crates/lib/src/bootc_composefs/backwards_compat/bcompat_boot.rs +++ b/crates/lib/src/bootc_composefs/backwards_compat/bcompat_boot.rs @@ -17,7 +17,7 @@ use crate::{ TYPE1_ENT_PATH_STAGED, UKI_NAME_PREFIX, USER_CFG_STAGED, }, parsers::bls_config::{BLSConfig, BLSConfigType}, - spec::Bootloader, + spec::BootloaderKind, store::Storage, }; use anyhow::{Context, Result}; @@ -323,8 +323,8 @@ pub(crate) async fn prepend_custom_prefix( handle_bls_conf(storage, cfs_cmdline, boot_dir, false)?; } - BootType::Uki => match bootloader { - Bootloader::Grub => { + BootType::Uki => match bootloader.kind()? { + BootloaderKind::GRUBClassic => { let esp = storage.require_esp()?; let mut buf = String::new(); @@ -384,11 +384,9 @@ pub(crate) async fn prepend_custom_prefix( rename_exchange_user_cfg(&grub_dir)?; } - Bootloader::Systemd | Bootloader::GrubCC => { + BootloaderKind::BLSCompatible => { handle_bls_conf(storage, cfs_cmdline, boot_dir, true)?; } - - Bootloader::None => unreachable!("Checked at install time"), }, }; diff --git a/crates/lib/src/bootc_composefs/boot.rs b/crates/lib/src/bootc_composefs/boot.rs index 6c13581cc..72f00828d 100644 --- a/crates/lib/src/bootc_composefs/boot.rs +++ b/crates/lib/src/bootc_composefs/boot.rs @@ -96,6 +96,7 @@ use crate::bootc_composefs::status::ComposefsCmdline; use crate::bootc_kargs::compute_new_kargs; use crate::composefs_consts::{TYPE1_BOOT_DIR_PREFIX, TYPE1_ENT_PATH, TYPE1_ENT_PATH_STAGED}; use crate::parsers::bls_config::{BLSConfig, BLSConfigType}; +use crate::spec::BootloaderKind; use crate::task::Task; use crate::{bootc_composefs::repo::open_composefs_repo, store::Storage}; use crate::{bootc_composefs::status::get_sorted_grub_uki_boot_entries, install::PostFetchState}; @@ -582,8 +583,8 @@ pub(crate) fn setup_composefs_bls_boot( compute_new_kargs(mounted_erofs, current_root, &mut cmdline_refs)?; - let (entry_paths, _tmpdir_guard) = match bootloader { - Bootloader::Grub => { + let (entry_paths, _tmpdir_guard) = match bootloader.kind()? { + BootloaderKind::GRUBClassic => { let root = Dir::open_ambient_dir(&root_path, ambient_authority()) .context("Opening root path")?; @@ -607,7 +608,7 @@ pub(crate) fn setup_composefs_bls_boot( ) } - Bootloader::Systemd | Bootloader::GrubCC => { + BootloaderKind::BLSCompatible => { let efi_mount = mount_esp(&esp_device).context("Mounting ESP")?; let mounted_efi = Utf8PathBuf::from(efi_mount.dir.path().as_str()?); @@ -622,8 +623,6 @@ pub(crate) fn setup_composefs_bls_boot( Some(efi_mount), ) } - - Bootloader::None => unreachable!("Checked at install time"), }; let (bls_config, boot_digest, os_id) = match &entry { @@ -1164,16 +1163,14 @@ pub(crate) fn setup_composefs_uki_boot( let boot_digest = uki_info.boot_digest.clone(); - match bootloader { - Bootloader::Grub => { + match bootloader.kind()? { + BootloaderKind::GRUBClassic => { write_grub_uki_menuentry(root_path, &setup_type, uki_info.boot_label, id, &esp_device)? } - Bootloader::Systemd | Bootloader::GrubCC => { + BootloaderKind::BLSCompatible => { write_systemd_uki_config(&esp_mount.fd, &setup_type, uki_info, id)? } - - Bootloader::None => unreachable!("Checked at install time"), }; Ok(boot_digest) diff --git a/crates/lib/src/bootc_composefs/delete.rs b/crates/lib/src/bootc_composefs/delete.rs index 17a8bafea..999afaa7c 100644 --- a/crates/lib/src/bootc_composefs/delete.rs +++ b/crates/lib/src/bootc_composefs/delete.rs @@ -15,7 +15,7 @@ use crate::{ TYPE1_ENT_PATH, TYPE1_ENT_PATH_STAGED, USER_CFG_STAGED, }, parsers::bls_config::{BLSConfigType, parse_bls_config}, - spec::{BootEntry, Bootloader, DeploymentEntry}, + spec::{BootEntry, BootloaderKind, DeploymentEntry}, status::Slot, store::{BootedComposefs, Storage}, }; @@ -145,20 +145,18 @@ fn delete_depl_boot_entries( ) -> Result<()> { let boot_dir = storage.require_boot_dir()?; - match deployment.deployment.bootloader { - Bootloader::Grub => match deployment.deployment.boot_type { + match deployment.deployment.bootloader.kind()? { + BootloaderKind::GRUBClassic => match deployment.deployment.boot_type { BootType::Bls => delete_type1_conf_file(deployment, boot_dir, deleting_staged), BootType::Uki => { remove_grub_menucfg_entry(&deployment.deployment.verity, boot_dir, deleting_staged) } }, - Bootloader::Systemd | Bootloader::GrubCC => { + BootloaderKind::BLSCompatible => { // For Systemd UKI as well, we use .conf files delete_type1_conf_file(deployment, boot_dir, deleting_staged) } - - Bootloader::None => unreachable!("Checked at install time"), } } diff --git a/crates/lib/src/bootc_composefs/finalize.rs b/crates/lib/src/bootc_composefs/finalize.rs index 935da2153..9e0f5e3d4 100644 --- a/crates/lib/src/bootc_composefs/finalize.rs +++ b/crates/lib/src/bootc_composefs/finalize.rs @@ -5,7 +5,7 @@ use crate::bootc_composefs::gc::{GCOpts, composefs_gc}; use crate::bootc_composefs::rollback::{rename_exchange_bls_entries, rename_exchange_user_cfg}; use crate::bootc_composefs::status::get_composefs_status; use crate::composefs_consts::STATE_DIR_ABS; -use crate::spec::Bootloader; +use crate::spec::BootloaderKind; use crate::store::{BootedComposefs, Storage}; use anyhow::{Context, Result}; use bootc_initramfs_setup::mount_composefs_image; @@ -131,8 +131,8 @@ pub(crate) async fn composefs_backend_finalize( let boot_dir = storage.require_boot_dir()?; - match booted_composefs.bootloader { - Bootloader::Grub => match staged_composefs.boot_type { + match booted_composefs.bootloader.kind()? { + BootloaderKind::GRUBClassic => match staged_composefs.boot_type { BootType::Bls => { let entries_dir = boot_dir.open_dir("loader")?; rename_exchange_bls_entries(&entries_dir)?; @@ -140,12 +140,10 @@ pub(crate) async fn composefs_backend_finalize( BootType::Uki => finalize_staged_grub_uki(boot_dir)?, }, - Bootloader::Systemd | Bootloader::GrubCC => { + BootloaderKind::BLSCompatible => { let entries_dir = boot_dir.open_dir("loader")?; rename_exchange_bls_entries(&entries_dir)?; } - - Bootloader::None => unreachable!("Checked at install time"), }; // Now that we have successfully updated bootloader entires, we can GC the unreferenced ones diff --git a/crates/lib/src/bootc_composefs/rollback.rs b/crates/lib/src/bootc_composefs/rollback.rs index 5003a6b43..b61d6ac15 100644 --- a/crates/lib/src/bootc_composefs/rollback.rs +++ b/crates/lib/src/bootc_composefs/rollback.rs @@ -12,7 +12,7 @@ use crate::bootc_composefs::boot::{ }; use crate::bootc_composefs::status::{get_composefs_status, get_sorted_type1_boot_entries}; use crate::composefs_consts::TYPE1_ENT_PATH_STAGED; -use crate::spec::Bootloader; +use crate::spec::{Bootloader, BootloaderKind}; use crate::store::{BootedComposefs, Storage}; use crate::{ bootc_composefs::{boot::get_efi_uuid_source, status::get_sorted_grub_uki_boot_entries}, @@ -224,8 +224,8 @@ pub(crate) async fn composefs_rollback( let boot_dir = storage.require_boot_dir()?; - match &rollback_entry.bootloader { - Bootloader::Grub => match rollback_entry.boot_type { + match &rollback_entry.bootloader.kind()? { + BootloaderKind::GRUBClassic => match rollback_entry.boot_type { BootType::Bls => { rollback_composefs_entries(boot_dir, rollback_entry.bootloader.clone())?; } @@ -234,12 +234,10 @@ pub(crate) async fn composefs_rollback( } }, - Bootloader::Systemd | Bootloader::GrubCC => { + BootloaderKind::BLSCompatible => { // We use BLS entries for systemd UKI as well rollback_composefs_entries(boot_dir, rollback_entry.bootloader.clone())?; } - - Bootloader::None => unreachable!("Checked at install time"), } if reverting { diff --git a/crates/lib/src/bootc_composefs/status.rs b/crates/lib/src/bootc_composefs/status.rs index 52c7673e0..f1fca53f6 100644 --- a/crates/lib/src/bootc_composefs/status.rs +++ b/crates/lib/src/bootc_composefs/status.rs @@ -25,7 +25,7 @@ use crate::{ bls_config::{BLSConfig, BLSConfigType, parse_bls_config}, grub_menuconfig::{MenuEntry, parse_grub_menuentry_file}, }, - spec::{BootEntry, BootOrder, Host, HostSpec, ImageStatus}, + spec::{BootEntry, BootOrder, BootloaderKind, Host, HostSpec, ImageStatus}, store::Storage, utils::{EfiError, read_uefi_var}, }; @@ -339,8 +339,8 @@ pub(crate) fn list_bootloader_entries(storage: &Storage) -> Result { + let entries = match bootloader.kind()? { + BootloaderKind::GRUBClassic => { // Grub entries are always in boot let grub_dir = boot_dir.open_dir("grub2").context("Opening grub dir")?; @@ -368,9 +368,7 @@ pub(crate) fn list_bootloader_entries(storage: &Storage) -> Result list_type1_entries(boot_dir)?, - - Bootloader::None => unreachable!("Checked at install time"), + BootloaderKind::BLSCompatible => list_type1_entries(boot_dir)?, }; Ok(entries) @@ -873,8 +871,11 @@ async fn composefs_deployment_status_from( let booted_cfs = host.require_composefs_booted()?; let mut grub_menu_string = String::new(); - let (is_rollback_queued, sorted_bls_config, grub_menu_entries) = match booted_cfs.bootloader { - Bootloader::Grub => match boot_type { + let (is_rollback_queued, sorted_bls_config, grub_menu_entries) = match booted_cfs + .bootloader + .kind()? + { + BootloaderKind::GRUBClassic => match boot_type { BootType::Bls => { let bls_configs = get_sorted_type1_boot_entries(boot_dir, false)?; let bls_config = bls_configs @@ -915,7 +916,7 @@ async fn composefs_deployment_status_from( }, // We will have BLS stuff and the UKI stuff in the same DIR - Bootloader::Systemd | Bootloader::GrubCC => { + BootloaderKind::BLSCompatible => { let bls_configs = get_sorted_type1_boot_entries(boot_dir, true)?; let bls_config = bls_configs .first() @@ -938,8 +939,6 @@ async fn composefs_deployment_status_from( (is_rollback_queued, Some(bls_configs), None) } - - Bootloader::None => unreachable!("Checked at install time"), }; // Determine rollback deployment by matching extra deployment boot entries against entires read from /boot diff --git a/crates/lib/src/bootc_composefs/update.rs b/crates/lib/src/bootc_composefs/update.rs index 78c120c85..13cad88b6 100644 --- a/crates/lib/src/bootc_composefs/update.rs +++ b/crates/lib/src/bootc_composefs/update.rs @@ -12,6 +12,7 @@ use ocidir::cap_std::ambient_authority; use ostree_ext::container::ManifestDiff; use crate::bootc_composefs::gc::GCOpts; +use crate::spec::BootloaderKind; use crate::{ bootc_composefs::{ boot::{BootSetupType, BootType, setup_composefs_bls_boot, setup_composefs_uki_boot}, @@ -30,7 +31,7 @@ use crate::{ COMPOSEFS_STAGED_DEPLOYMENT_FNAME, COMPOSEFS_TRANSIENT_STATE_DIR, STATE_DIR_RELATIVE, TYPE1_ENT_PATH_STAGED, USER_CFG_STAGED, }, - spec::{Bootloader, Host, ImageReference}, + spec::{Host, ImageReference}, store::{BootedComposefs, ComposefsRepository, Storage}, }; @@ -170,8 +171,8 @@ pub(crate) fn validate_update( // Remove staged bootloader entries, if any // GC should take care of the UKI PEs and other binaries - match get_bootloader()? { - Bootloader::Grub => match booted.boot_type { + match get_bootloader()?.kind()? { + BootloaderKind::GRUBClassic => match booted.boot_type { BootType::Bls => rm_staged_type1_ent(boot_dir)?, BootType::Uki => { @@ -184,9 +185,7 @@ pub(crate) fn validate_update( } }, - Bootloader::Systemd | Bootloader::GrubCC => rm_staged_type1_ent(boot_dir)?, - - Bootloader::None => unreachable!("Checked at install time"), + BootloaderKind::BLSCompatible => rm_staged_type1_ent(boot_dir)?, } // Remove state directory diff --git a/crates/lib/src/spec.rs b/crates/lib/src/spec.rs index 45f60d66a..678830e91 100644 --- a/crates/lib/src/spec.rs +++ b/crates/lib/src/spec.rs @@ -245,6 +245,15 @@ pub enum Bootloader { None, } +#[derive(Debug)] +pub enum BootloaderKind { + /// Bootloader that support Bootloader Specification + /// GrubCC and SystemdBoot + BLSCompatible, + /// Classic Grub + GRUBClassic, +} + impl Display for Bootloader { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let string = match self { @@ -272,6 +281,18 @@ impl FromStr for Bootloader { } } +impl Bootloader { + /// Returns whether the Bootloader is BLSCompatible + /// Throws and error if Bootloader is None + pub(crate) fn kind(&self) -> Result { + match self { + Bootloader::Grub => Ok(BootloaderKind::GRUBClassic), + Bootloader::Systemd | Bootloader::GrubCC => Ok(BootloaderKind::BLSCompatible), + Bootloader::None => anyhow::bail!("Bootloader was None"), + } + } +} + /// A bootable entry #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] #[serde(rename_all = "camelCase")] diff --git a/crates/lib/src/store/mod.rs b/crates/lib/src/store/mod.rs index 8453fb86b..563594f43 100644 --- a/crates/lib/src/store/mod.rs +++ b/crates/lib/src/store/mod.rs @@ -92,7 +92,6 @@ use std::cell::OnceCell; use std::ops::Deref; -use std::os::fd::{AsFd, AsRawFd}; use std::sync::Arc; use anyhow::{Context, Result}; @@ -119,7 +118,7 @@ use crate::bootc_composefs::boot::{EFI_LINUX, mount_esp}; use crate::bootc_composefs::status::{ComposefsCmdline, composefs_booted, get_bootloader}; use crate::lsm; use crate::podstorage::CStorage; -use crate::spec::{Bootloader, ImageStatus}; +use crate::spec::{BootloaderKind, ImageStatus}; use crate::utils::{deployment_fd, open_dir_remount_rw}; /// See @@ -333,13 +332,14 @@ impl BootedStorage { let esp_dev = root_dev.find_first_colocated_esp()?; let esp_mount = mount_esp(&esp_dev.path())?; - let boot_dir = match get_bootloader()? { - Bootloader::Grub => physical_root.open_dir("boot").context("Opening boot")?, + let boot_dir = match get_bootloader()?.kind()? { + BootloaderKind::GRUBClassic => { + physical_root.open_dir("boot").context("Opening boot")? + } // NOTE: Handle XBOOTLDR partitions here if and when we use it - Bootloader::Systemd | Bootloader::GrubCC => { + BootloaderKind::BLSCompatible => { esp_mount.fd.try_clone().context("Cloning fd")? } - Bootloader::None => unreachable!("Checked at install time"), }; let storage = Storage { @@ -527,16 +527,15 @@ impl Storage { // boot dir in case of systemd-boot points to the ESP, but we store // the actual binaries inside ESP/EFI/Linux - let boot_dir = match get_bootloader()? { - Bootloader::Grub => boot_dir.try_clone()?, - Bootloader::Systemd | Bootloader::GrubCC => { + let boot_dir = match get_bootloader()?.kind()? { + BootloaderKind::GRUBClassic => boot_dir.try_clone()?, + BootloaderKind::BLSCompatible => { let boot_dir = boot_dir .open_dir(EFI_LINUX) .with_context(|| format!("Opening {EFI_LINUX}"))?; boot_dir } - Bootloader::None => anyhow::bail!("Unknown bootloader"), }; Ok(boot_dir) From f415e69e5a3f2c12349f6dfd8cac0895a98b0fd7 Mon Sep 17 00:00:00 2001 From: Pragyan Poudyal Date: Wed, 3 Jun 2026 11:19:46 +0530 Subject: [PATCH 4/5] tests: Update tests for GrubCC Signed-off-by: Pragyan Poudyal No secureboot for grub-cc Signed-off-by: Pragyan Poudyal --- crates/lib/src/spec.rs | 3 ++- crates/xtask/src/bcvk.rs | 5 ++++- tmt/tests/booted/readonly/030-test-composefs.nu | 15 ++++++++++++--- tmt/tests/booted/test-composefs-gc.nu | 8 ++++---- .../test-install-to-filesystem-var-mount.sh | 14 +++++++------- tmt/tests/booted/test-multi-device-esp.nu | 5 +++-- 6 files changed, 32 insertions(+), 18 deletions(-) diff --git a/crates/lib/src/spec.rs b/crates/lib/src/spec.rs index 678830e91..9fa8d7a25 100644 --- a/crates/lib/src/spec.rs +++ b/crates/lib/src/spec.rs @@ -238,6 +238,7 @@ pub enum Bootloader { #[default] Grub, /// Use Grub for confidential clusters as the bootloader + #[serde(rename = "grub-cc")] GrubCC, /// Use SystemdBoot as the bootloader Systemd, @@ -245,7 +246,7 @@ pub enum Bootloader { None, } -#[derive(Debug)] +#[derive(Debug, PartialEq, Eq)] pub enum BootloaderKind { /// Bootloader that support Bootloader Specification /// GrubCC and SystemdBoot diff --git a/crates/xtask/src/bcvk.rs b/crates/xtask/src/bcvk.rs index 7d883d205..a7508f6cf 100644 --- a/crates/xtask/src/bcvk.rs +++ b/crates/xtask/src/bcvk.rs @@ -110,7 +110,10 @@ impl BcvkInstallOpts { Run 'just generate-secureboot-keys' to generate them." ); } - } else if matches!(self.bootloader, Some(Bootloader::Systemd)) { + } else if matches!( + self.bootloader, + Some(Bootloader::Systemd) | Some(Bootloader::GrubCC) + ) { Ok(vec!["--firmware=uefi-insecure".into()]) } else { Ok(Vec::new()) diff --git a/tmt/tests/booted/readonly/030-test-composefs.nu b/tmt/tests/booted/readonly/030-test-composefs.nu index 58bbd94c5..801c5a7ee 100644 --- a/tmt/tests/booted/readonly/030-test-composefs.nu +++ b/tmt/tests/booted/readonly/030-test-composefs.nu @@ -7,6 +7,12 @@ def parse_cmdline [] { open /proc/cmdline | str trim | split row " " } +def find_root_eq_in_cmdline [bootloader: string] { + let cmdline = parse_cmdline + let has_root_param = ($cmdline | any { |param| $param | str starts-with 'root=' }) + assert (not $has_root_param) $"($bootloader) image should not have root= in kernel cmdline; systemd-gpt-auto-generator should discover the root partition via DPS" +} + # Detect composefs by checking if composefs field is present let st = bootc status --json | from json let is_composefs = (tap is_composefs) @@ -24,11 +30,14 @@ if $expecting_composefs { let bootctl_output = (bootctl) if ($bootctl_output | str contains 'Product: systemd-boot') { - let cmdline = parse_cmdline - let has_root_param = ($cmdline | any { |param| $param | str starts-with 'root=' }) - assert (not $has_root_param) "systemd-boot image should not have root= in kernel cmdline; systemd-gpt-auto-generator should discover the root partition via DPS" + find_root_eq_in_cmdline "systemd-boot" } } + + # GrubCC also supports BLS and shouldn't need root= + if $bootloader == "grub-cc" { + find_root_eq_in_cmdline "grub-cc" + } } if $is_composefs { diff --git a/tmt/tests/booted/test-composefs-gc.nu b/tmt/tests/booted/test-composefs-gc.nu index d19a01efe..4262b583b 100644 --- a/tmt/tests/booted/test-composefs-gc.nu +++ b/tmt/tests/booted/test-composefs-gc.nu @@ -88,9 +88,9 @@ def third_boot [] { # Also assert we have two different kernel + initrd pairs let booted_verity = (bootc status --json | from json).status.booted.composefs.verity - let bootloader = (bootc status --json | from json).status.booted.composefs.bootloader + let bootloader = ((bootc status --json | from json).status.booted.composefs.bootloader | str downcase) - let boot_dir = if ($bootloader | str downcase) == "systemd" { + let boot_dir = if $bootloader == "systemd" or $bootloader == "grub-cc" { # TODO: Some concrete API for this would be great mkdir /var/tmp/efi mount /dev/disk/by-partlabel/EFI-SYSTEM /var/tmp/efi @@ -123,9 +123,9 @@ def third_boot [] { } def fourth_boot [] { - let bootloader = (bootc status --json | from json).status.booted.composefs.bootloader + let bootloader = ((bootc status --json | from json).status.booted.composefs.bootloader | str downcase) - if ($bootloader | str downcase) == "systemd" { + if $bootloader == "systemd" or $bootloader == "grub-cc" { # TODO: Some concrete API for this would be great mkdir /var/tmp/efi mount /dev/disk/by-partlabel/EFI-SYSTEM /var/tmp/efi diff --git a/tmt/tests/booted/test-install-to-filesystem-var-mount.sh b/tmt/tests/booted/test-install-to-filesystem-var-mount.sh index 5025b0a76..8d588dd48 100644 --- a/tmt/tests/booted/test-install-to-filesystem-var-mount.sh +++ b/tmt/tests/booted/test-install-to-filesystem-var-mount.sh @@ -141,11 +141,15 @@ echo "Filesystem layout:" mount | grep /var/mnt/target || true df -h /var/mnt/target /var/mnt/target/boot /var/mnt/target/boot/efi /var/mnt/target/var -COMPOSEFS_BACKEND=() +bootloader=$(bootc status --json | jq -r '.status.booted.composefs.bootloader' | tr '[:upper:]' '[:lower:]') + +COMPOSEFS_BACKEND_PARAMS=() KARGS=("--karg=root=UUID=$ROOT_UUID") if [[ $is_composefs != "null" ]]; then - COMPOSEFS_BACKEND+=("--composefs-backend") + COMPOSEFS_BACKEND_PARAMS+=("--composefs-backend") + COMPOSEFS_BACKEND_PARAMS+=("--bootloader" "${bootloader}") + tune2fs -O verity /dev/BL/var02 tune2fs -O verity /dev/BL/root02 @@ -164,7 +168,7 @@ podman run \ "$TARGET_IMAGE" \ bootc install to-filesystem \ --disable-selinux \ - "${COMPOSEFS_BACKEND[@]}" \ + "${COMPOSEFS_BACKEND_PARAMS[@]}" \ "${KARGS[@]}" \ --root-mount-spec=UUID="$ROOT_UUID" \ --boot-mount-spec=UUID="$BOOT_UUID" \ @@ -186,16 +190,12 @@ else # It works for now as the CI runs separately for each bootloader, but we need to get the # bootloader from the installed systemd if we wish to run the tests locally without rebuilding the images # This probably also happens in other tests, one instance is install-outside-container - bootloader=$(bootc status --json | jq '.status.booted.composefs.bootloader' | tr '[:upper:]' '[:lower:]') - bootloader=${bootloader//\"/} - if [[ $bootloader == "grub" ]]; then test -d /var/mnt/target/boot/grub2 || test -d /var/mnt/target/boot/loader else test -d /var/mnt/target/boot/efi/EFI test -d /var/mnt/target/boot/efi/loader/entries fi - fi diff --git a/tmt/tests/booted/test-multi-device-esp.nu b/tmt/tests/booted/test-multi-device-esp.nu index b3f69fcf3..40fd4acb9 100644 --- a/tmt/tests/booted/test-multi-device-esp.nu +++ b/tmt/tests/booted/test-multi-device-esp.nu @@ -437,8 +437,9 @@ def main [] { # supports GRUB today. Skip when the image uses systemd-boot. if (tap is_composefs) { let st = bootc status --json | from json - if ($st.status.booted.composefs.bootloader | str downcase) == "systemd" { - print "SKIP: multi-device ESP test not supported with systemd-boot" + let bootloader = $st.status.booted.composefs.bootloader | str downcase + if $bootloader == "systemd" or $bootloader == "grub-cc" { + print $"SKIP: multi-device ESP test not supported with ($bootloader)" tap ok return } From b2f1576e5940e7583a62ddffd0cbb6b5a18dd977 Mon Sep 17 00:00:00 2001 From: Pragyan Poudyal Date: Wed, 3 Jun 2026 16:40:58 +0530 Subject: [PATCH 5/5] Run `just update-generated` Signed-off-by: Pragyan Poudyal --- docs/src/host-v1.schema.json | 5 +++++ docs/src/man/bootc-install-to-disk.8.md | 1 + docs/src/man/bootc-install-to-existing-root.8.md | 1 + docs/src/man/bootc-install-to-filesystem.8.md | 1 + 4 files changed, 8 insertions(+) diff --git a/docs/src/host-v1.schema.json b/docs/src/host-v1.schema.json index db716c3f1..0503068a7 100644 --- a/docs/src/host-v1.schema.json +++ b/docs/src/host-v1.schema.json @@ -214,6 +214,11 @@ "type": "string", "const": "grub" }, + { + "description": "Use Grub for confidential clusters as the bootloader", + "type": "string", + "const": "grub-cc" + }, { "description": "Use SystemdBoot as the bootloader", "type": "string", diff --git a/docs/src/man/bootc-install-to-disk.8.md b/docs/src/man/bootc-install-to-disk.8.md index 5b4a7bd39..6850d0ba9 100644 --- a/docs/src/man/bootc-install-to-disk.8.md +++ b/docs/src/man/bootc-install-to-disk.8.md @@ -166,6 +166,7 @@ set `discoverable-partitions = true` in their install configuration Possible values: - grub + - grub-cc - systemd - none diff --git a/docs/src/man/bootc-install-to-existing-root.8.md b/docs/src/man/bootc-install-to-existing-root.8.md index 4b4f8925d..9cfaec6ef 100644 --- a/docs/src/man/bootc-install-to-existing-root.8.md +++ b/docs/src/man/bootc-install-to-existing-root.8.md @@ -212,6 +212,7 @@ of migrating the fstab entries. See the "Injecting kernel arguments" section abo Possible values: - grub + - grub-cc - systemd - none diff --git a/docs/src/man/bootc-install-to-filesystem.8.md b/docs/src/man/bootc-install-to-filesystem.8.md index f46892de3..a48ecc50a 100644 --- a/docs/src/man/bootc-install-to-filesystem.8.md +++ b/docs/src/man/bootc-install-to-filesystem.8.md @@ -120,6 +120,7 @@ is currently expected to be empty by default. Possible values: - grub + - grub-cc - systemd - none