diff --git a/Cargo.toml b/Cargo.toml index cd6fc416b..b65de253a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,13 +4,16 @@ resolver = "2" [profile.dev] opt-level = 1 # No optimizations are too slow for us. +# https://kobzol.github.io/rust/rustc/2025/05/20/disable-debuginfo-to-improve-rust-compile-times.html +debug = false [profile.release] lto = "thin" # We use FFI so this is safest panic = "abort" # We assume we're being delivered via e.g. RPM which supports split debuginfo -debug = true +# debug = true +debug = false [profile.thin] # drop bootc size when split debuginfo is not available and go a step diff --git a/crates/etc-merge/src/lib.rs b/crates/etc-merge/src/lib.rs index b6388e2a5..53cfb52fa 100644 --- a/crates/etc-merge/src/lib.rs +++ b/crates/etc-merge/src/lib.rs @@ -736,7 +736,10 @@ pub fn merge( Ok(..) => { /* no-op */ } // Removed file's not present in the new etc dir, nothing to do Err(e) if e.kind() == std::io::ErrorKind::NotFound => continue, - Err(e) => Err(e)?, + Err(e) if e.kind() == std::io::ErrorKind::IsADirectory => { + new_etc_fd.remove_dir_all(&removed).context(format!("Failed to remove dir {removed:?}"))? + } + Err(e) => Err(e).context(format!("Failed to remove file {removed:?}"))?, } println!("- Removed file {removed:?}"); diff --git a/crates/lib/Cargo.toml b/crates/lib/Cargo.toml index d7ecbf9e7..f71cccc66 100644 --- a/crates/lib/Cargo.toml +++ b/crates/lib/Cargo.toml @@ -73,7 +73,7 @@ similar-asserts = { workspace = true } static_assertions = { workspace = true } [features] -default = ["install-to-disk"] +default = ["install-to-disk", "composefs-backend"] # This feature enables `bootc install to-disk`, which is considered just a "demo" # or reference installer; we expect most nontrivial use cases to be using # `bootc install to-filesystem`. diff --git a/crates/lib/src/bootc_composefs/boot.rs b/crates/lib/src/bootc_composefs/boot.rs index 177d4ab00..7d761a298 100644 --- a/crates/lib/src/bootc_composefs/boot.rs +++ b/crates/lib/src/bootc_composefs/boot.rs @@ -35,7 +35,8 @@ use serde::{Deserialize, Serialize}; use crate::bootc_composefs::repo::open_composefs_repo; use crate::bootc_composefs::state::{get_booted_bls, write_composefs_state}; use crate::bootc_composefs::status::get_sorted_uki_boot_entries; -use crate::parsers::bls_config::BLSConfig; +use crate::composefs_consts::{TYPE1_ENT_PATH, TYPE1_ENT_PATH_STAGED}; +use crate::parsers::bls_config::{BLSConfig, BLSConfigType}; use crate::parsers::grub_menuconfig::MenuEntry; use crate::spec::ImageReference; use crate::task::Task; @@ -55,6 +56,16 @@ pub(crate) const EFI_UUID_FILE: &str = "efiuuid.cfg"; /// The EFI Linux directory const EFI_LINUX: &str = "EFI/Linux"; +/// Timeout for systemd-boot bootloader menu +const SYSTEMD_TIMEOUT: &str = "timeout 5"; +const SYSTEMD_LOADER_CONF_PATH: &str = "loader/loader.conf"; + +/// We want to be able to control the ordering of UKIs so we put them in a directory that's not the +/// directory specified by the BLS spec. We do this because we want systemd-boot to only look at +/// our config files and not show the actual UKIs in the bootloader menu +/// This is relative to the ESP +const SYSTEMD_UKI_DIR: &str = "EFI/Linux/uki"; + pub(crate) enum BootSetupType<'a> { /// For initial setup, i.e. install to-disk Setup((&'a RootSetup, &'a State, &'a FileSystem)), @@ -448,25 +459,31 @@ pub(crate) fn setup_composefs_bls_boot( .with_title(title) .with_sort_key(default_sort_key.into()) .with_version(version) - .with_linux(format!( - "/{}/{id_hex}/vmlinuz", - entry_paths.abs_entries_path - )) - .with_initrd(vec![format!( - "/{}/{id_hex}/initrd", - entry_paths.abs_entries_path - )]) - .with_options(cmdline_refs); + .with_cfg(BLSConfigType::NonEFI { + linux: format!("/{}/{id_hex}/vmlinuz", entry_paths.abs_entries_path), + initrd: vec![format!("/{}/{id_hex}/initrd", entry_paths.abs_entries_path)], + options: Some(cmdline_refs), + }); match find_vmlinuz_initrd_duplicates(&boot_digest)? { Some(symlink_to) => { - bls_config.linux = - format!("/{}/{symlink_to}/vmlinuz", entry_paths.abs_entries_path); - - bls_config.initrd = vec![format!( - "/{}/{symlink_to}/initrd", - entry_paths.abs_entries_path - )]; + match bls_config.cfg_type { + BLSConfigType::NonEFI { + ref mut linux, + ref mut initrd, + .. + } => { + *linux = + format!("/{}/{symlink_to}/vmlinuz", entry_paths.abs_entries_path); + + *initrd = vec![format!( + "/{}/{symlink_to}/initrd", + entry_paths.abs_entries_path + )]; + } + + _ => unreachable!(), + }; } None => { @@ -486,7 +503,9 @@ pub(crate) fn setup_composefs_bls_boot( let loader_path = entry_paths.config_path.join("loader"); let (config_path, booted_bls) = if is_upgrade { - let mut booted_bls = get_booted_bls()?; + let boot_dir = Dir::open_ambient_dir(&entry_paths.config_path, ambient_authority())?; + + let mut booted_bls = get_booted_bls(&boot_dir)?; booted_bls.sort_key = Some("0".into()); // entries are sorted by their filename in reverse order // This will be atomically renamed to 'loader/entries' on shutdown/reboot @@ -535,6 +554,7 @@ fn write_pe_to_esp( uki_id: &String, is_insecure_from_opts: bool, mounted_efi: impl AsRef, + bootloader: &Bootloader, ) -> Result> { let efi_bin = read_file(file, &repo).context("Reading .efi binary")?; @@ -571,7 +591,11 @@ fn write_pe_to_esp( } // Write the UKI to ESP - let efi_linux_path = mounted_efi.as_ref().join(EFI_LINUX); + let efi_linux_path = mounted_efi.as_ref().join(match bootloader { + Bootloader::Grub => EFI_LINUX, + Bootloader::Systemd => SYSTEMD_UKI_DIR, + }); + create_dir_all(&efi_linux_path).context("Creating EFI/Linux")?; let final_pe_path = match file_path.parent() { @@ -603,7 +627,12 @@ fn write_pe_to_esp( let pe_name = match pe_type { PEType::Uki => format!("{}{}", uki_id, EFI_EXT), - PEType::UkiAddon => format!("{}{}", uki_id, EFI_ADDON_FILE_EXT), + PEType::UkiAddon => file_path + .components() + .last() + .unwrap() + .to_string_lossy() + .to_string(), }; pe_dir @@ -624,7 +653,7 @@ fn write_pe_to_esp( fn write_grub_uki_menuentry( root_path: Utf8PathBuf, setup_type: &BootSetupType, - boot_label: &String, + boot_label: String, id: &Sha256HashValue, esp_device: &String, ) -> Result<()> { @@ -708,6 +737,76 @@ fn write_grub_uki_menuentry( Ok(()) } +#[context("Writing systemd UKI config")] +fn write_systemd_uki_config( + esp_dir: &Dir, + setup_type: &BootSetupType, + boot_label: String, + id: &Sha256HashValue, +) -> Result<()> { + let default_sort_key = "0"; + + let mut bls_conf = BLSConfig::default(); + bls_conf + .with_title(boot_label) + .with_cfg(BLSConfigType::EFI { + efi: format!("/{SYSTEMD_UKI_DIR}/{}{}", id.to_hex(), EFI_EXT), + }) + .with_sort_key(default_sort_key.into()) + // TODO (Johan-Liebert1): Get version from UKI like we get boot label + .with_version(default_sort_key.into()); + + let (entries_dir, booted_bls) = match setup_type { + BootSetupType::Setup(..) => { + esp_dir + .create_dir_all(TYPE1_ENT_PATH) + .with_context(|| format!("Creating {TYPE1_ENT_PATH}"))?; + + (esp_dir.open_dir(TYPE1_ENT_PATH)?, None) + } + + BootSetupType::Upgrade(_) => { + esp_dir + .create_dir_all(TYPE1_ENT_PATH_STAGED) + .with_context(|| format!("Creating {TYPE1_ENT_PATH_STAGED}"))?; + + let mut booted_bls = get_booted_bls(&esp_dir)?; + booted_bls.sort_key = Some("1".into()); + + (esp_dir.open_dir(TYPE1_ENT_PATH_STAGED)?, Some(booted_bls)) + } + }; + + entries_dir + .atomic_write( + type1_entry_conf_file_name(default_sort_key), + bls_conf.to_string().as_bytes(), + ) + .context("Writing conf file")?; + + if let Some(booted_bls) = booted_bls { + entries_dir.atomic_write( + // SAFETY: We set sort_key above + type1_entry_conf_file_name(booted_bls.sort_key.as_ref().unwrap()), + booted_bls.to_string().as_bytes(), + )?; + } + + // Write the timeout for bootloader menu if not exists + if !esp_dir.exists(SYSTEMD_LOADER_CONF_PATH) { + esp_dir + .atomic_write(SYSTEMD_LOADER_CONF_PATH, SYSTEMD_TIMEOUT) + .with_context(|| format!("Writing to {SYSTEMD_LOADER_CONF_PATH}"))?; + } + + let esp_dir = esp_dir + .reopen_as_ownedfd() + .context("Reopening as owned fd")?; + rustix::fs::fsync(esp_dir).context("fsync")?; + + Ok(()) +} + #[context("Setting up UKI boot")] pub(crate) fn setup_composefs_uki_boot( setup_type: BootSetupType, @@ -716,7 +815,7 @@ pub(crate) fn setup_composefs_uki_boot( id: &Sha256HashValue, entries: Vec>, ) -> Result<()> { - let (root_path, esp_device, bootloader, is_insecure_from_opts) = match setup_type { + let (root_path, esp_device, bootloader, is_insecure_from_opts, uki_addons) = match setup_type { BootSetupType::Setup((root_setup, state, ..)) => { if let Some(v) = &state.config_opts.karg { if v.len() > 0 { @@ -724,6 +823,10 @@ pub(crate) fn setup_composefs_uki_boot( } } + let Some(cfs_opts) = &state.composefs_options else { + anyhow::bail!("ComposeFS options not found"); + }; + let esp_part = root_setup .device_info .partitions @@ -731,23 +834,12 @@ pub(crate) fn setup_composefs_uki_boot( .find(|p| p.parttype.as_str() == ESP_GUID) .ok_or_else(|| anyhow!("ESP partition not found"))?; - let bootloader = state - .composefs_options - .as_ref() - .map(|opts| opts.bootloader.clone()) - .unwrap_or(Bootloader::default()); - - let is_insecure = state - .composefs_options - .as_ref() - .map(|x| x.insecure) - .unwrap_or(false); - ( root_setup.physical_root_path.clone(), esp_part.node.clone(), - bootloader, - is_insecure, + cfs_opts.bootloader.clone(), + cfs_opts.insecure, + cfs_opts.uki_addon.as_ref(), ) } @@ -761,6 +853,7 @@ pub(crate) fn setup_composefs_uki_boot( get_esp_partition(&sysroot_parent)?.0, bootloader, false, + None, ) } }; @@ -777,6 +870,32 @@ 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) { + let Some(addons) = uki_addons else { + continue; + }; + + let addon_name = entry + .file_path + .components() + .last() + .ok_or(anyhow::anyhow!("Could not get UKI addon name"))?; + + let addon_name = addon_name.as_str()?; + + let addon_name = + addon_name + .strip_suffix(EFI_ADDON_FILE_EXT) + .ok_or(anyhow::anyhow!( + "UKI addon doesn't end with {EFI_ADDON_DIR_EXT}" + ))?; + + if !addons.iter().any(|passed_addon| passed_addon == addon_name) { + continue; + } + } + let ret = write_pe_to_esp( &repo, &entry.file, @@ -785,6 +904,7 @@ pub(crate) fn setup_composefs_uki_boot( &id.to_hex(), is_insecure_from_opts, esp_mount.dir.path(), + &bootloader, )?; if let Some(label) = ret { @@ -796,12 +916,11 @@ pub(crate) fn setup_composefs_uki_boot( match bootloader { Bootloader::Grub => { - write_grub_uki_menuentry(root_path, &setup_type, &boot_label, id, &esp_device)? + write_grub_uki_menuentry(root_path, &setup_type, boot_label, id, &esp_device)? } Bootloader::Systemd => { - // No-op for now, but later we want to have .conf files so we can control the order of - // entries. + write_systemd_uki_config(&esp_mount.fd, &setup_type, boot_label, id)? } }; diff --git a/crates/lib/src/bootc_composefs/finalize.rs b/crates/lib/src/bootc_composefs/finalize.rs index 22ee0ea96..7f62b52ba 100644 --- a/crates/lib/src/bootc_composefs/finalize.rs +++ b/crates/lib/src/bootc_composefs/finalize.rs @@ -80,7 +80,12 @@ pub(crate) async fn composefs_native_finalize() -> Result<()> { let entries_dir = esp_mount.fd.open_dir("loader")?; rename_exchange_bls_entries(&entries_dir)?; } - BootType::Uki => rename_staged_uki_entries(&esp_mount.fd)?, + BootType::Uki => { + rename_staged_uki_entries(&esp_mount.fd)?; + + let entries_dir = esp_mount.fd.open_dir("loader")?; + rename_exchange_bls_entries(&entries_dir)?; + } }, }; diff --git a/crates/lib/src/bootc_composefs/repo.rs b/crates/lib/src/bootc_composefs/repo.rs index 1538b72bd..ff12427ec 100644 --- a/crates/lib/src/bootc_composefs/repo.rs +++ b/crates/lib/src/bootc_composefs/repo.rs @@ -49,11 +49,32 @@ pub(crate) async fn initialize_composefs_repository( &Arc::new(repo), &format!("{transport}{image_name}"), None, - None, + Some(ostree_ext::containers_image_proxy::ImageProxyConfig { + insecure_skip_tls_verification: Some(true), + ..Default::default() + }), ) .await } +/// skopeo (in composefs-rs) doesn't understand "registry:" +/// This function will convert it to "docker://" and return the image ref +/// +/// Ex +/// docker://quay.io/some-image +/// containers-storage:some-image +pub(crate) fn get_imgref(transport: &String, image: &String) -> String { + let img = image.strip_prefix(":").unwrap_or(&image); + + let final_imgref = if transport == "registry" { + format!("docker://{img}") + } else { + format!("{transport}:{img}") + }; + + final_imgref +} + /// Pulls the `image` from `transport` into a composefs repository at /sysroot /// Checks for boot entries in the image and returns them #[context("Pulling composefs repository")] @@ -70,10 +91,13 @@ pub(crate) async fn pull_composefs_repo( let repo = open_composefs_repo(&rootfs_dir).context("Opening compoesfs repo")?; - let (id, verity) = - composefs_oci_pull(&Arc::new(repo), &format!("{transport}:{image}"), None, None) - .await - .context("Pulling composefs repo")?; + let final_imgref = get_imgref(transport, image); + + tracing::debug!("Image to pull {final_imgref}"); + + let (id, verity) = composefs_oci_pull(&Arc::new(repo), &final_imgref, None, None) + .await + .context("Pulling composefs repo")?; tracing::info!("id: {}, verity: {}", hex::encode(id), verity.to_hex()); diff --git a/crates/lib/src/bootc_composefs/rollback.rs b/crates/lib/src/bootc_composefs/rollback.rs index 0ea23b7cd..b34a8b71e 100644 --- a/crates/lib/src/bootc_composefs/rollback.rs +++ b/crates/lib/src/bootc_composefs/rollback.rs @@ -8,7 +8,7 @@ use fn_error_context::context; use rustix::fs::{fsync, renameat_with, AtFlags, RenameFlags}; use crate::bootc_composefs::boot::BootType; -use crate::bootc_composefs::status::{composefs_deployment_status, get_sorted_bls_boot_entries}; +use crate::bootc_composefs::status::{composefs_deployment_status, get_sorted_type1_boot_entries}; use crate::{ bootc_composefs::{boot::get_efi_uuid_source, status::get_sorted_uki_boot_entries}, composefs_consts::{ @@ -53,8 +53,9 @@ pub(crate) fn rename_exchange_bls_entries(entries_dir: &Dir) -> Result<()> { .context("renameat")?; tracing::debug!("Removing {STAGED_BOOT_LOADER_ENTRIES}"); - rustix::fs::unlinkat(&entries_dir, STAGED_BOOT_LOADER_ENTRIES, AtFlags::REMOVEDIR) - .context("unlinkat")?; + entries_dir + .remove_dir_all(STAGED_BOOT_LOADER_ENTRIES) + .context("Removing staged dir")?; tracing::debug!("Syncing to disk"); let entries_dir = entries_dir @@ -110,7 +111,7 @@ pub(crate) fn rollback_composefs_bls() -> Result<()> { // After this: // all_configs[0] -> booted depl // all_configs[1] -> rollback depl - let mut all_configs = get_sorted_bls_boot_entries(&boot_dir, false)?; + let mut all_configs = get_sorted_type1_boot_entries(&boot_dir, false)?; // Update the indicies so that they're swapped for (idx, cfg) in all_configs.iter_mut().enumerate() { diff --git a/crates/lib/src/bootc_composefs/state.rs b/crates/lib/src/bootc_composefs/state.rs index ac12ed541..47b20cc4c 100644 --- a/crates/lib/src/bootc_composefs/state.rs +++ b/crates/lib/src/bootc_composefs/state.rs @@ -6,7 +6,8 @@ use bootc_kernel_cmdline::utf8::Cmdline; use bootc_mount::tempmount::TempMount; use bootc_utils::CommandRunExt; use camino::Utf8PathBuf; -use cap_std_ext::{cap_std, dirext::CapStdExtDirExt}; +use cap_std_ext::cap_std::ambient_authority; +use cap_std_ext::{cap_std::fs::Dir, dirext::CapStdExtDirExt}; use composefs::fsverity::{FsVerityHashValue, Sha256HashValue}; use fn_error_context::context; @@ -17,40 +18,54 @@ use rustix::{ }; use crate::bootc_composefs::boot::BootType; +use crate::bootc_composefs::repo::get_imgref; +use crate::bootc_composefs::status::get_sorted_type1_boot_entries; +use crate::parsers::bls_config::BLSConfigType; use crate::{ composefs_consts::{ COMPOSEFS_CMDLINE, COMPOSEFS_STAGED_DEPLOYMENT_FNAME, COMPOSEFS_TRANSIENT_STATE_DIR, ORIGIN_KEY_BOOT, ORIGIN_KEY_BOOT_DIGEST, ORIGIN_KEY_BOOT_TYPE, SHARED_VAR_PATH, STATE_DIR_RELATIVE, }, - parsers::bls_config::{parse_bls_config, BLSConfig}, + parsers::bls_config::BLSConfig, spec::ImageReference, utils::path_relative_to, }; -pub(crate) fn get_booted_bls() -> Result { +pub(crate) fn get_booted_bls(boot_dir: &Dir) -> Result { let cmdline = Cmdline::from_proc()?; let booted = cmdline .find(COMPOSEFS_CMDLINE) .ok_or_else(|| anyhow::anyhow!("Failed to find composefs parameter in kernel cmdline"))?; - for entry in std::fs::read_dir("/sysroot/boot/loader/entries")? { - let entry = entry?; + let sorted_entries = get_sorted_type1_boot_entries(boot_dir, true)?; - if !entry.file_name().as_str()?.ends_with(".conf") { - continue; - } + for entry in sorted_entries { + match &entry.cfg_type { + BLSConfigType::EFI { efi } => { + let composfs_param_value = booted.value().ok_or(anyhow::anyhow!( + "Failed to get composefs kernel cmdline value" + ))?; - let bls = parse_bls_config(&std::fs::read_to_string(&entry.path())?)?; + if efi.contains(composfs_param_value) { + return Ok(entry); + } + } - let Some(opts) = &bls.options else { - anyhow::bail!("options not found in bls config") - }; - let opts = Cmdline::from(opts); + BLSConfigType::NonEFI { options, .. } => { + let Some(opts) = options else { + anyhow::bail!("options not found in bls config") + }; + + let opts = Cmdline::from(opts); + + if opts.iter().any(|v| v == booted) { + return Ok(entry); + } + } - if opts.iter().any(|v| v == booted) { - return Ok(bls); - } + BLSConfigType::Unknown => anyhow::bail!("Unknown BLS Config type"), + }; } Err(anyhow::anyhow!("Booted BLS not found")) @@ -118,9 +133,12 @@ pub(crate) fn write_composefs_state( .. } = &imgref; + let imgref = get_imgref(&transport, &image_name); + let mut config = tini::Ini::new().section("origin").item( ORIGIN_CONTAINER, - format!("ostree-unverified-image:{transport}{image_name}"), + // TODO (Johan-Liebert1): The image won't always be unverified + format!("ostree-unverified-image:{imgref}"), ); config = config @@ -133,8 +151,8 @@ pub(crate) fn write_composefs_state( .item(ORIGIN_KEY_BOOT_DIGEST, boot_digest); } - let state_dir = cap_std::fs::Dir::open_ambient_dir(&state_path, cap_std::ambient_authority()) - .context("Opening state dir")?; + let state_dir = + Dir::open_ambient_dir(&state_path, ambient_authority()).context("Opening state dir")?; state_dir .atomic_write( @@ -147,11 +165,9 @@ pub(crate) fn write_composefs_state( std::fs::create_dir_all(COMPOSEFS_TRANSIENT_STATE_DIR) .with_context(|| format!("Creating {COMPOSEFS_TRANSIENT_STATE_DIR}"))?; - let staged_depl_dir = cap_std::fs::Dir::open_ambient_dir( - COMPOSEFS_TRANSIENT_STATE_DIR, - cap_std::ambient_authority(), - ) - .with_context(|| format!("Opening {COMPOSEFS_TRANSIENT_STATE_DIR}"))?; + let staged_depl_dir = + Dir::open_ambient_dir(COMPOSEFS_TRANSIENT_STATE_DIR, ambient_authority()) + .with_context(|| format!("Opening {COMPOSEFS_TRANSIENT_STATE_DIR}"))?; staged_depl_dir .atomic_write( diff --git a/crates/lib/src/bootc_composefs/status.rs b/crates/lib/src/bootc_composefs/status.rs index fc82524a0..3f8747569 100644 --- a/crates/lib/src/bootc_composefs/status.rs +++ b/crates/lib/src/bootc_composefs/status.rs @@ -7,9 +7,9 @@ use fn_error_context::context; use crate::{ bootc_composefs::boot::{get_esp_partition, get_sysroot_parent_dev, BootType}, - composefs_consts::{BOOT_LOADER_ENTRIES, COMPOSEFS_CMDLINE, USER_CFG}, + composefs_consts::{COMPOSEFS_CMDLINE, TYPE1_ENT_PATH, USER_CFG}, parsers::{ - bls_config::{parse_bls_config, BLSConfig}, + bls_config::{parse_bls_config, BLSConfig, BLSConfigType}, grub_menuconfig::{parse_grub_menuentry_file, MenuEntry}, }, spec::{BootEntry, BootOrder, Host, HostSpec, ImageReference, ImageStatus}, @@ -91,14 +91,14 @@ pub(crate) fn get_sorted_uki_boot_entries<'a>( parse_grub_menuentry_file(str) } -#[context("Getting sorted BLS entries")] -pub(crate) fn get_sorted_bls_boot_entries( +#[context("Getting sorted Type1 boot entries")] +pub(crate) fn get_sorted_type1_boot_entries( boot_dir: &Dir, ascending: bool, ) -> Result> { let mut all_configs = vec![]; - for entry in boot_dir.read_dir(format!("loader/{BOOT_LOADER_ENTRIES}"))? { + for entry in boot_dir.read_dir(TYPE1_ENT_PATH)? { let entry = entry?; let file_name = entry.file_name(); @@ -358,29 +358,63 @@ pub(crate) async fn composefs_deployment_status() -> Result { } }; - match boot_type { - BootType::Bls => { - host.status.rollback_queued = !get_sorted_bls_boot_entries(&boot_dir, false)? - .first() - .ok_or(anyhow::anyhow!("First boot entry not found"))? - .options - .as_ref() - .ok_or(anyhow::anyhow!("options key not found in bls config"))? - .contains(composefs_digest.as_ref()); - } + let is_rollback_queued = match booted.bootloader { + Bootloader::Grub => match boot_type { + BootType::Bls => { + let bls_config = get_sorted_type1_boot_entries(&boot_dir, false)?; + let bls_config = bls_config + .first() + .ok_or(anyhow::anyhow!("First boot entry not found"))?; + + match &bls_config.cfg_type { + BLSConfigType::NonEFI { options, .. } => !options + .as_ref() + .ok_or(anyhow::anyhow!("options key not found in bls config"))? + .contains(composefs_digest.as_ref()), + + BLSConfigType::EFI { .. } => { + anyhow::bail!("Found 'efi' field in Type1 boot entry") + } + BLSConfigType::Unknown => anyhow::bail!("Unknown BLS Config Type"), + } + } - BootType::Uki => { - let mut s = String::new(); + BootType::Uki => { + let mut s = String::new(); + + !get_sorted_uki_boot_entries(&boot_dir, &mut s)? + .first() + .ok_or(anyhow::anyhow!("First boot entry not found"))? + .body + .chainloader + .contains(composefs_digest.as_ref()) + } + }, - host.status.rollback_queued = !get_sorted_uki_boot_entries(&boot_dir, &mut s)? + // We will have BLS stuff and the UKI stuff in the same DIR + Bootloader::Systemd => { + let bls_config = get_sorted_type1_boot_entries(&boot_dir, false)?; + let bls_config = bls_config .first() - .ok_or(anyhow::anyhow!("First boot entry not found"))? - .body - .chainloader - .contains(composefs_digest.as_ref()) + .ok_or(anyhow::anyhow!("First boot entry not found"))?; + + match &bls_config.cfg_type { + // For UKI boot + BLSConfigType::EFI { efi } => efi.contains(composefs_digest.as_ref()), + + // For boot entry Type1 + BLSConfigType::NonEFI { options, .. } => !options + .as_ref() + .ok_or(anyhow::anyhow!("options key not found in bls config"))? + .contains(composefs_digest.as_ref()), + + BLSConfigType::Unknown => anyhow::bail!("Unknown BLS Config Type"), + } } }; + host.status.rollback_queued = is_rollback_queued; + if host.status.rollback_queued { host.spec.boot_order = BootOrder::Rollback }; @@ -392,7 +426,7 @@ pub(crate) async fn composefs_deployment_status() -> Result { mod tests { use cap_std_ext::{cap_std, dirext::CapStdExtDirExt}; - use crate::parsers::grub_menuconfig::MenuentryBody; + use crate::parsers::{bls_config::BLSConfigType, grub_menuconfig::MenuentryBody}; use super::*; @@ -437,26 +471,30 @@ mod tests { tempdir.atomic_write("loader/entries/entry1.conf", entry1)?; tempdir.atomic_write("loader/entries/entry2.conf", entry2)?; - let result = get_sorted_bls_boot_entries(&tempdir, true).unwrap(); + let result = get_sorted_type1_boot_entries(&tempdir, true).unwrap(); let mut config1 = BLSConfig::default(); config1.title = Some("Fedora 42.20250623.3.1 (CoreOS)".into()); config1.sort_key = Some("1".into()); - config1.linux = "/boot/7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6/vmlinuz-5.14.10".into(); - config1.initrd = vec!["/boot/7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6/initramfs-5.14.10.img".into()]; - config1.options = Some("root=UUID=abc123 rw composefs=7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6".into()); + config1.cfg_type = BLSConfigType::NonEFI { + linux: "/boot/7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6/vmlinuz-5.14.10".into(), + initrd: vec!["/boot/7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6/initramfs-5.14.10.img".into()], + options: Some("root=UUID=abc123 rw composefs=7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6".into()), + }; let mut config2 = BLSConfig::default(); config2.title = Some("Fedora 41.20250214.2.0 (CoreOS)".into()); config2.sort_key = Some("2".into()); - config2.linux = "/boot/febdf62805de2ae7b6b597f2a9775d9c8a753ba1e5f09298fc8fbe0b0d13bf01/vmlinuz-5.14.10".into(); - config2.initrd = vec!["/boot/febdf62805de2ae7b6b597f2a9775d9c8a753ba1e5f09298fc8fbe0b0d13bf01/initramfs-5.14.10.img".into()]; - config2.options = Some("root=UUID=abc123 rw composefs=febdf62805de2ae7b6b597f2a9775d9c8a753ba1e5f09298fc8fbe0b0d13bf01".into()); + config2.cfg_type = BLSConfigType::NonEFI { + linux: "/boot/febdf62805de2ae7b6b597f2a9775d9c8a753ba1e5f09298fc8fbe0b0d13bf01/vmlinuz-5.14.10".into(), + initrd: vec!["/boot/febdf62805de2ae7b6b597f2a9775d9c8a753ba1e5f09298fc8fbe0b0d13bf01/initramfs-5.14.10.img".into()], + options: Some("root=UUID=abc123 rw composefs=febdf62805de2ae7b6b597f2a9775d9c8a753ba1e5f09298fc8fbe0b0d13bf01".into()) + }; assert_eq!(result[0].sort_key.as_ref().unwrap(), "1"); assert_eq!(result[1].sort_key.as_ref().unwrap(), "2"); - let result = get_sorted_bls_boot_entries(&tempdir, false).unwrap(); + let result = get_sorted_type1_boot_entries(&tempdir, false).unwrap(); assert_eq!(result[0].sort_key.as_ref().unwrap(), "2"); assert_eq!(result[1].sort_key.as_ref().unwrap(), "1"); diff --git a/crates/lib/src/cfsctl.rs b/crates/lib/src/cfsctl.rs index 6daae5b23..7555180f4 100644 --- a/crates/lib/src/cfsctl.rs +++ b/crates/lib/src/cfsctl.rs @@ -13,7 +13,7 @@ use rustix::fs::CWD; use composefs_boot::{write_boot, BootOps}; use composefs::{ - fsverity::{FsVerityHashValue, Sha512HashValue}, + fsverity::{FsVerityHashValue, Sha256HashValue, Sha512HashValue}, repository::Repository, }; @@ -147,7 +147,7 @@ enum Command { }, } -fn verity_opt(opt: &Option) -> Result> { +fn verity_opt(opt: &Option) -> Result> { Ok(opt.as_ref().map(FsVerityHashValue::from_hex).transpose()?) } diff --git a/crates/lib/src/composefs_consts.rs b/crates/lib/src/composefs_consts.rs index 3c18287a1..828504a86 100644 --- a/crates/lib/src/composefs_consts.rs +++ b/crates/lib/src/composefs_consts.rs @@ -31,3 +31,8 @@ pub(crate) const STAGED_BOOT_LOADER_ENTRIES: &str = "entries.staged"; pub(crate) const USER_CFG: &str = "user.cfg"; /// Filename for staged grub user config pub(crate) const USER_CFG_STAGED: &str = "user.cfg.staged"; + +/// Path to the config files directory for Type1 boot entries +/// This is relative to the boot/efi directory +pub(crate) const TYPE1_ENT_PATH: &str = "loader/entries"; +pub(crate) const TYPE1_ENT_PATH_STAGED: &str = "loader/entries.staged"; diff --git a/crates/lib/src/install.rs b/crates/lib/src/install.rs index d5f917781..1ba26976e 100644 --- a/crates/lib/src/install.rs +++ b/crates/lib/src/install.rs @@ -256,6 +256,12 @@ pub(crate) struct InstallComposefsOpts { #[clap(long, default_value_t)] #[serde(default)] pub(crate) bootloader: Bootloader, + + /// Name of the UKI addons to install without the ".efi.addon" suffix. + /// This option can be provided multiple times if multiple addons are to be installed. + #[clap(long)] + #[serde(default)] + pub(crate) uki_addon: Option>, } #[cfg(feature = "install-to-disk")] @@ -366,6 +372,14 @@ pub(crate) struct InstallToFilesystemOpts { #[clap(flatten)] pub(crate) config_opts: InstallConfigOpts, + + #[clap(long)] + #[cfg(feature = "composefs-backend")] + pub(crate) composefs_native: bool, + + #[cfg(feature = "composefs-backend")] + #[clap(flatten)] + pub(crate) compoesfs_opts: InstallComposefsOpts, } #[derive(Debug, Clone, clap::Parser, PartialEq, Eq)] @@ -987,6 +1001,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, @@ -1530,6 +1545,9 @@ async fn install_to_filesystem_impl( } } + println!("state: {state:#?}"); + println!("root_setup: {rootfs:#?}"); + #[cfg(feature = "composefs-backend")] if state.composefs_options.is_some() { // Load a fd for the mounted target physical root @@ -1566,6 +1584,8 @@ pub(crate) async fn install_to_disk(mut opts: InstallToDiskOpts) -> Result<()> { #[cfg(feature = "composefs-backend")] opts.validate()?; + println!("install to disk opts: {opts:#?}"); + // Log the disk installation operation to systemd journal const INSTALL_DISK_JOURNAL_ID: &str = "8b7c6d5e4f3a2b1c0d9e8f7a6b5c4d3e2"; let source_image = opts @@ -1850,12 +1870,28 @@ pub(crate) async fn install_to_filesystem( target_path ); + println!("opts: {opts:#?}"); + // Gather global state, destructuring the provided options. // IMPORTANT: We might re-execute the current process in this function (for SELinux among other things) // IMPORTANT: and hence anything that is done before MUST BE IDEMPOTENT. // IMPORTANT: In practice, we should only be gathering information before this point, // IMPORTANT: and not performing any mutations at all. - let state = prepare_install(opts.config_opts, opts.source_opts, opts.target_opts, None).await?; + let state = prepare_install( + opts.config_opts, + opts.source_opts, + opts.target_opts, + #[cfg(feature = "composefs-backend")] + if opts.composefs_native { + Some(opts.compoesfs_opts) + } else { + None + }, + #[cfg(not(feature = "composefs-backend"))] + None, + ) + .await?; + // And the last bit of state here is the fsopts, which we also destructure now. let mut fsopts = opts.filesystem_opts; @@ -2123,6 +2159,14 @@ pub(crate) async fn install_to_existing_root(opts: InstallToExistingRootOpts) -> source_opts: opts.source_opts, target_opts: opts.target_opts, config_opts: opts.config_opts, + #[cfg(feature = "composefs-backend")] + composefs_native: false, + #[cfg(feature = "composefs-backend")] + compoesfs_opts: InstallComposefsOpts { + insecure: false, + bootloader: Bootloader::Grub, + uki_addon: None + }, }; install_to_filesystem(opts, true, cleanup).await diff --git a/crates/lib/src/parsers/bls_config.rs b/crates/lib/src/parsers/bls_config.rs index 29b8f3e7b..6a6e704c8 100644 --- a/crates/lib/src/parsers/bls_config.rs +++ b/crates/lib/src/parsers/bls_config.rs @@ -5,10 +5,29 @@ #![allow(dead_code)] use anyhow::{anyhow, Result}; +use core::fmt; use std::collections::HashMap; use std::fmt::Display; use uapi_version::Version; +#[derive(Debug, PartialEq, PartialOrd, Eq, Default)] +pub enum BLSConfigType { + EFI { + /// The path to the EFI binary, usually a UKI + efi: String, + }, + NonEFI { + /// The path to the linux kernel to boot. + linux: String, + /// The paths to the initrd images. + initrd: Vec, + /// Kernel command line options. + options: Option, + }, + #[default] + Unknown, +} + /// Represents a single Boot Loader Specification config file. /// /// The boot loader should present the available boot menu entries to the user in a sorted list. @@ -24,12 +43,9 @@ pub(crate) struct BLSConfig { /// /// This is hidden and must be accessed via [`Self::version()`]; version: String, - /// The path to the linux kernel to boot. - pub(crate) linux: String, - /// The paths to the initrd images. - pub(crate) initrd: Vec, - /// Kernel command line options. - pub(crate) options: Option, + + pub(crate) cfg_type: BLSConfigType, + /// The machine ID of the OS. pub(crate) machine_id: Option, /// The sort key for the boot menu. @@ -79,13 +95,30 @@ impl Display for BLSConfig { } writeln!(f, "version {}", self.version)?; - writeln!(f, "linux {}", self.linux)?; - for initrd in self.initrd.iter() { - writeln!(f, "initrd {}", initrd)?; - } - if let Some(options) = self.options.as_deref() { - writeln!(f, "options {}", options)?; + + match &self.cfg_type { + BLSConfigType::EFI { efi } => { + writeln!(f, "efi {}", efi)?; + } + + BLSConfigType::NonEFI { + linux, + initrd, + options, + } => { + writeln!(f, "linux {}", linux)?; + for initrd in initrd.iter() { + writeln!(f, "initrd {}", initrd)?; + } + + if let Some(options) = options.as_deref() { + writeln!(f, "options {}", options)?; + } + } + + BLSConfigType::Unknown => return Err(fmt::Error), } + if let Some(machine_id) = self.machine_id.as_deref() { writeln!(f, "machine-id {}", machine_id)?; } @@ -114,16 +147,8 @@ impl BLSConfig { self.version = new_val; self } - pub(crate) fn with_linux(&mut self, new_val: String) -> &mut Self { - self.linux = new_val; - self - } - pub(crate) fn with_initrd(&mut self, new_val: Vec) -> &mut Self { - self.initrd = new_val; - self - } - pub(crate) fn with_options(&mut self, new_val: String) -> &mut Self { - self.options = Some(new_val); + pub(crate) fn with_cfg(&mut self, config: BLSConfigType) -> &mut Self { + self.cfg_type = config; self } #[allow(dead_code)] @@ -146,6 +171,7 @@ pub(crate) fn parse_bls_config(input: &str) -> Result { let mut title = None; let mut version = None; let mut linux = None; + let mut efi = None; let mut initrd = Vec::new(); let mut options = None; let mut machine_id = None; @@ -168,6 +194,7 @@ pub(crate) fn parse_bls_config(input: &str) -> Result { "options" => options = Some(value), "machine-id" => machine_id = Some(value), "sort-key" => sort_key = Some(value), + "efi" => efi = Some(value), _ => { extra.insert(key.to_string(), value); } @@ -175,15 +202,27 @@ pub(crate) fn parse_bls_config(input: &str) -> Result { } } - let linux = linux.ok_or_else(|| anyhow!("Missing 'linux' value"))?; let version = version.ok_or_else(|| anyhow!("Missing 'version' value"))?; + let cfg_type = match (linux, efi) { + (None, Some(efi)) => BLSConfigType::EFI { efi }, + + (Some(linux), None) => BLSConfigType::NonEFI { + linux, + initrd, + options, + }, + + // The spec makes no mention of whether both can be present or not + // Fow now, for us, we won't have both at the same time + (Some(_), Some(_)) => anyhow::bail!("'linux' and 'efi' values present"), + (None, None) => anyhow::bail!("Missing 'linux' or 'efi' value"), + }; + Ok(BLSConfig { title, version, - linux, - initrd, - options, + cfg_type, machine_id, sort_key, extra, @@ -208,14 +247,23 @@ mod tests { let config = parse_bls_config(input)?; + let BLSConfigType::NonEFI { + linux, + initrd, + options, + } = config.cfg_type + else { + panic!("Expected non EFI variant"); + }; + assert_eq!( config.title, Some("Fedora 42.20250623.3.1 (CoreOS)".to_string()) ); assert_eq!(config.version, "2"); - assert_eq!(config.linux, "/boot/7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6/vmlinuz-5.14.10"); - assert_eq!(config.initrd, vec!["/boot/7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6/initramfs-5.14.10.img"]); - assert_eq!(config.options, Some("root=UUID=abc123 rw composefs=7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6".to_string())); + assert_eq!(linux, "/boot/7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6/vmlinuz-5.14.10"); + assert_eq!(initrd, vec!["/boot/7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6/initramfs-5.14.10.img"]); + assert_eq!(options, Some("root=UUID=abc123 rw composefs=7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6".to_string())); assert_eq!(config.extra.get("custom1"), Some(&"value1".to_string())); assert_eq!(config.extra.get("custom2"), Some(&"value2".to_string())); @@ -235,8 +283,12 @@ mod tests { let config = parse_bls_config(input)?; + let BLSConfigType::NonEFI { initrd, .. } = config.cfg_type else { + panic!("Expected non EFI variant"); + }; + assert_eq!( - config.initrd, + initrd, vec!["/boot/initramfs-1.img", "/boot/initramfs-2.img"] ); diff --git a/crates/lib/src/store/mod.rs b/crates/lib/src/store/mod.rs index cab4167e2..42f4fd17e 100644 --- a/crates/lib/src/store/mod.rs +++ b/crates/lib/src/store/mod.rs @@ -35,8 +35,10 @@ use crate::spec::ImageStatus; use crate::utils::deployment_fd; /// See https://github.com/containers/composefs-rs/issues/159 +// pub type ComposefsRepository = +// composefs::repository::Repository; pub type ComposefsRepository = - composefs::repository::Repository; + composefs::repository::Repository; /// Path to the physical root pub const SYSROOT: &str = "sysroot"; diff --git a/examples/.gitignore b/examples/.gitignore new file mode 100644 index 000000000..b6219d124 --- /dev/null +++ b/examples/.gitignore @@ -0,0 +1,11 @@ +*.addon.efi +*.ign +*.img +*.qcow2 +backups +bootc +bootc-bls/iid +bootc-bls/secureboot +bootc-bls/tmp +bootc-initramfs-setup +systemd-bootx64.efi diff --git a/examples/bootc-bls/Containerfile b/examples/bootc-bls/Containerfile new file mode 100644 index 000000000..45d531383 --- /dev/null +++ b/examples/bootc-bls/Containerfile @@ -0,0 +1,71 @@ +FROM quay.io/fedora/fedora-bootc:42 +COPY . / + +RUN < /etc/kernel/cmdline + + rm -f "/etc/yum.repos.d/fedora-cisco-openh264.repo" + dnf install -y systemd-ukify sbsigntools systemd-boot-unsigned + + kver=$(cd /usr/lib/modules && echo *) + mkdir -p "/boot/EFI/Linux" + mkdir -p "/boot/EFI/Linux/$kver.efi.extra.d" + + ukify build \ + --linux "/usr/lib/modules/$kver/vmlinuz" \ + --initrd "/usr/lib/modules/$kver/initramfs.img" \ + --uname="${kver}" \ + --cmdline "@/etc/kernel/cmdline" \ + --os-release "@/etc/os-release" \ + --signtool sbsign \ + --secureboot-private-key "/run/secrets/key" \ + --secureboot-certificate "/run/secrets/cert" \ + --measure \ + --json pretty \ + --output "/boot/EFI/Linux/$kver.efi" + + ukify build \ + --cmdline "ignition.firstboot ignition.platform.id=qemu" \ + --signtool sbsign \ + --secureboot-private-key "/run/secrets/key" \ + --secureboot-certificate "/run/secrets/cert" \ + --output "/boot/EFI/Linux/$kver.efi.extra.d/ignition.addon.efi" + + # sbsign \ + # --key "/run/secrets/key" \ + # --cert "/run/secrets/cert" \ + # "/usr/lib/systemd/boot/efi/systemd-bootx64.efi" \ + # --output "/boot/systemd-bootx64.efi" +EOF + +FROM base as final + +RUN --mount=type=bind,from=kernel,target=/_mount/kernel < /etc/kernel/cmdline + + dnf install -y \ + systemd-ukify \ + sbsigntools \ + systemd-boot-unsigned + + kver=$(cd /usr/lib/modules && echo *) + ukify build \ + --linux "/usr/lib/modules/$kver/vmlinuz" \ + --initrd "/usr/lib/modules/$kver/initramfs.img" \ + --uname="${kver}" \ + --cmdline "@/etc/kernel/cmdline" \ + --os-release "@/etc/os-release" \ + --signtool sbsign \ + --secureboot-private-key "/run/secrets/key" \ + --secureboot-certificate "/run/secrets/cert" \ + --measure \ + --json pretty \ + --output "/boot/EFI/Linux/$kver.efi" +EOF + +FROM base as final +COPY --from=final /boot /boot diff --git a/examples/bootc-bls/bootc-install-to-filesystem b/examples/bootc-bls/bootc-install-to-filesystem new file mode 100755 index 000000000..4aa408ef3 --- /dev/null +++ b/examples/bootc-bls/bootc-install-to-filesystem @@ -0,0 +1,3 @@ +#!/bin/bash +cd .. +./to-filesystem-uki-cocl.sh diff --git a/examples/bootc-bls/build-bootc-bls b/examples/bootc-bls/build-bootc-bls new file mode 100755 index 000000000..61f8f0182 --- /dev/null +++ b/examples/bootc-bls/build-bootc-bls @@ -0,0 +1,26 @@ +#!/bin/bash + +set -eux + +cd "${0%/*}" + +FROM="${FROM:-quay.io/fedora/fedora-bootc:42}" +TAG="${TAG:-quay.io/fedora/fedora-bootc-bls:42}" +EXTRA="${EXTRA:-extra}" +CONTAINERFILE="${CONTAINERFILE:-Containerfile}" + +# cargo build --release --features=composefs-backend + +mkdir -p "${EXTRA}/usr/bin/" +cp ../../target/release/bootc "${EXTRA}/usr/bin/" +cp ../../target/release/bootc-initramfs-setup "${EXTRA}/usr/lib/dracut/modules.d/37bootc/" + +mkdir -p tmp + +podman build \ + --from "${FROM}" \ + --build-arg BASE="${FROM}" \ + -t "${TAG}" \ + -f "${CONTAINERFILE}" \ + --iidfile=iid \ + "${EXTRA}" diff --git a/examples/bootc-bls/build-bootc-uki b/examples/bootc-bls/build-bootc-uki new file mode 100755 index 000000000..c43a6f5c9 --- /dev/null +++ b/examples/bootc-bls/build-bootc-uki @@ -0,0 +1,74 @@ +#!/bin/bash + +set -eux + +cd "${0%/*}" + +# cargo build --release --features=composefs-backend + +FROM="${FROM:-quay.io/fedora/fedora-bootc-bls:42}" +TAG="${TAG:-quay.io/fedora/fedora-bootc-uki:42}" + +# See: https://wiki.archlinux.org/title/Unified_Extensible_Firmware_Interface/Secure_Boot +# Alternative to generate keys for testing: `sbctl create-keys` +if [[ ! -d "secureboot" ]]; then + echo "Generating test Secure Boot keys" + mkdir secureboot + pushd secureboot > /dev/null + uuidgen --random > GUID.txt + openssl req -newkey rsa:4096 -nodes -keyout PK.key -new -x509 -sha256 -days 3650 -subj "/CN=Test Platform Key/" -out PK.crt + openssl x509 -outform DER -in PK.crt -out PK.cer + openssl req -newkey rsa:4096 -nodes -keyout KEK.key -new -x509 -sha256 -days 3650 -subj "/CN=Test Key Exchange Key/" -out KEK.crt + openssl x509 -outform DER -in KEK.crt -out KEK.cer + openssl req -newkey rsa:4096 -nodes -keyout db.key -new -x509 -sha256 -days 3650 -subj "/CN=Test Signature Database key/" -out db.crt + openssl x509 -outform DER -in db.crt -out db.cer + popd > /dev/null +fi + +if [[ ! -f "systemd-bootx64.efi" ]]; then + # Sign systemd-boot once and re-use it for all builds to keep it unchanged + sudo podman run --rm \ + --security-opt label=disable \ + --volume "$PWD/secureboot/db.key:/run/secrets/key" \ + --volume "$PWD/secureboot/db.crt:/run/secrets/cert" \ + --volume "$PWD:/var/srv" \ + --workdir "/var/srv" \ + "${FROM}" \ + bash -c "rm -f '/etc/yum.repos.d/fedora-cisco-openh264.repo'; dnf install -y sbsigntools systemd-boot-unsigned; sbsign --key '/run/secrets/key' --cert '/run/secrets/cert' '/usr/lib/systemd/boot/efi/systemd-bootx64.efi' --output '/var/srv/systemd-bootx64.efi'" +fi + +# Replace GRUB with a signed systemd-boot binary +sudo podman build \ + --from "${FROM}" \ + -t "${FROM}-systemdboot" \ + --iidfile=iid \ + -f Containerfile.systemdboot + +cp ../../target/release/bootc . + +# Workaround: Mount a filesystem where fs-verity is enabled +mount /dev/vdb3 tmp + +# rm -rf tmp/sysroot +mkdir -p tmp/sysroot/composefs + +IMAGE_ID="$(sed s/sha256:// iid)" +./bootc internals cfs --repo tmp/sysroot/composefs oci pull containers-storage:"${IMAGE_ID}" +COMPOSEFS_FSVERITY=$(./bootc internals cfs --repo tmp/sysroot/composefs oci compute-id --bootable "${IMAGE_ID}") + +# For debugging, add --no-cache to podman command +sudo podman build \ + --from "${FROM}-systemdboot" \ + -t "${TAG}" \ + --build-arg=COMPOSEFS_FSVERITY="${COMPOSEFS_FSVERITY}" \ + -f Containerfile.uki \ + --secret=id=key,src=secureboot/db.key \ + --secret=id=cert,src=secureboot/db.crt + +# rm -rf tmp/efi +# mkdir -p tmp/efi +# ./bootc internals cfs --repo tmp/sysroot/composefs oci pull containers-storage:"${IMAGE_ID}" +# ./bootc internals cfs --repo tmp/sysroot/composefs oci compute-id --bootable "${IMAGE_ID}" +# ./bootc internals cfs --repo tmp/sysroot/composefs oci prepare-boot "${IMAGE_ID}" --bootdir tmp/efi + +umount tmp diff --git a/examples/bootc-bls/build-fcos-bls b/examples/bootc-bls/build-fcos-bls new file mode 100755 index 000000000..eeed3885e --- /dev/null +++ b/examples/bootc-bls/build-fcos-bls @@ -0,0 +1,5 @@ +#!/bin/bash + +export FROM="quay.io/fedora/fedora-coreos:42.20250901.3.0" +export TAG="quay.io/fedora/fedora-coreos-bls:42.20250901.3.0" +exec ./build-bootc-bls diff --git a/examples/bootc-bls/build-fcos-bls-cocl b/examples/bootc-bls/build-fcos-bls-cocl new file mode 100755 index 000000000..d112f34dd --- /dev/null +++ b/examples/bootc-bls/build-fcos-bls-cocl @@ -0,0 +1,8 @@ +#!/bin/bash + +export FROM="quay.io/fedora/fedora-coreos:42.20250901.3.0" +export TAG="quay.io/fedora/fedora-coreos-bls-cocl:42.20250901.3.0" +# export TAG="quay.io/fedora/fedora-coreos-bls-cocl:42.20250901.3.1" +export CONTAINERFILE="Containerfile.cocl" +export EXTRA="extra-cocl" +exec ./build-bootc-bls diff --git a/examples/bootc-bls/build-fcos-uki b/examples/bootc-bls/build-fcos-uki new file mode 100755 index 000000000..d6d1f898b --- /dev/null +++ b/examples/bootc-bls/build-fcos-uki @@ -0,0 +1,5 @@ +#!/bin/bash + +export FROM="quay.io/fedora/fedora-coreos-bls:42.20250901.3.0" +export TAG="quay.io/fedora/fedora-coreos-uki:42.20250901.3.0" +exec ./build-bootc-uki diff --git a/examples/bootc-bls/build-fcos-uki-cocl b/examples/bootc-bls/build-fcos-uki-cocl new file mode 100755 index 000000000..809b99952 --- /dev/null +++ b/examples/bootc-bls/build-fcos-uki-cocl @@ -0,0 +1,7 @@ +#!/bin/bash + +export FROM="quay.io/fedora/fedora-coreos-bls-cocl:42.20250901.3.0" +export TAG="quay.io/fedora/fedora-coreos-uki-cocl:42.20250901.3.0" +# export FROM="quay.io/fedora/fedora-coreos-bls-cocl:42.20250901.3.1" +# export TAG="quay.io/fedora/fedora-coreos-uki-cocl:42.20250901.3.1" +exec ./build-bootc-uki diff --git a/examples/bootc-bls/build-uki-addon b/examples/bootc-bls/build-uki-addon new file mode 100755 index 000000000..93dd45786 --- /dev/null +++ b/examples/bootc-bls/build-uki-addon @@ -0,0 +1,26 @@ +#!/bin/bash + +set -euxo pipefail + +cd "${0%/*}" + +mkdir -p ../addons + +declare -A addons=( + ["luks"]="rd.luks.name=8ec9cda3-6b77-45d7-bb56-a95cd9e83234=root" + ["console-tty0"]="console=tty0" + ["console-ttyS0"]="console=ttyS0,115000n" + ["debug-tty0"]="rd.systemd.debug_shell=1 rd.systemd.default_debug_tty=tty0" + ["rd.neednet"]="rd.neednet=1 ip=dhcp" +) + +for addon in "${!addons[@]}"; do + echo "Building kernel command line UKI addon '${addon}': ${addons[${addon}]}" + ukify build \ + --cmdline "${addons[${addon}]}" \ + --signtool sbsign \ + --secureboot-private-key "secureboot/db.key" \ + --secureboot-certificate "secureboot/db.crt" \ + --output "../addons/${addon}.addon.efi" + +done diff --git a/examples/bootc-bls/extra-cocl/usr/lib/dracut/dracut.conf.d/50trustee.conf b/examples/bootc-bls/extra-cocl/usr/lib/dracut/dracut.conf.d/50trustee.conf new file mode 100644 index 000000000..ca57b40a2 --- /dev/null +++ b/examples/bootc-bls/extra-cocl/usr/lib/dracut/dracut.conf.d/50trustee.conf @@ -0,0 +1 @@ +force_drivers+=" sev-guest " diff --git a/examples/bootc-bls/extra-cocl/usr/lib/dracut/modules.d/37bootc/bootc-initramfs-setup.service b/examples/bootc-bls/extra-cocl/usr/lib/dracut/modules.d/37bootc/bootc-initramfs-setup.service new file mode 100644 index 000000000..15fdc5801 --- /dev/null +++ b/examples/bootc-bls/extra-cocl/usr/lib/dracut/modules.d/37bootc/bootc-initramfs-setup.service @@ -0,0 +1,34 @@ +# Copyright (C) 2013 Colin Walters +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. If not, see . + +[Unit] +DefaultDependencies=no +ConditionKernelCommandLine=composefs +ConditionPathExists=/etc/initrd-release +After=sysroot.mount +Requires=sysroot.mount +Before=initrd-root-fs.target +Before=initrd-switch-root.target + +OnFailure=emergency.target +OnFailureJobMode=isolate + +[Service] +Type=oneshot +ExecStart=/usr/bin/bootc-initramfs-setup +StandardInput=null +StandardOutput=journal +StandardError=journal+console +RemainAfterExit=yes diff --git a/examples/bootc-bls/extra-cocl/usr/lib/dracut/modules.d/37bootc/module-setup.sh b/examples/bootc-bls/extra-cocl/usr/lib/dracut/modules.d/37bootc/module-setup.sh new file mode 100755 index 000000000..b1c56206f --- /dev/null +++ b/examples/bootc-bls/extra-cocl/usr/lib/dracut/modules.d/37bootc/module-setup.sh @@ -0,0 +1,20 @@ +#!/usr/bin/bash + +check() { + return 0 +} + +depends() { + return 0 +} + +install() { + inst \ + "${moddir}/bootc-initramfs-setup" /usr/bin/bootc-initramfs-setup + inst \ + "${moddir}/bootc-initramfs-setup.service" \ + "${systemdsystemunitdir}/bootc-initramfs-setup.service" + + $SYSTEMCTL -q --root "${initdir}" add-wants \ + 'initrd-root-fs.target' 'bootc-initramfs-setup.service' +} diff --git a/examples/bootc-bls/extra-cocl/usr/lib/dracut/modules.d/65clevispin/module-setup.sh b/examples/bootc-bls/extra-cocl/usr/lib/dracut/modules.d/65clevispin/module-setup.sh new file mode 100644 index 000000000..688e96e5c --- /dev/null +++ b/examples/bootc-bls/extra-cocl/usr/lib/dracut/modules.d/65clevispin/module-setup.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +check() { + return 0 +} + +depends() { + return 0 +} + +install () { + inst /usr/bin/trustee-attester + inst /usr/bin/clevis-pin-trustee + inst /usr/bin/clevis-encrypt-trustee + inst /usr/bin/clevis-decrypt-trustee +} diff --git a/examples/bootc-bls/extra/usr/lib/dracut/modules.d/37bootc/bootc-initramfs-setup.service b/examples/bootc-bls/extra/usr/lib/dracut/modules.d/37bootc/bootc-initramfs-setup.service new file mode 100644 index 000000000..15fdc5801 --- /dev/null +++ b/examples/bootc-bls/extra/usr/lib/dracut/modules.d/37bootc/bootc-initramfs-setup.service @@ -0,0 +1,34 @@ +# Copyright (C) 2013 Colin Walters +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. If not, see . + +[Unit] +DefaultDependencies=no +ConditionKernelCommandLine=composefs +ConditionPathExists=/etc/initrd-release +After=sysroot.mount +Requires=sysroot.mount +Before=initrd-root-fs.target +Before=initrd-switch-root.target + +OnFailure=emergency.target +OnFailureJobMode=isolate + +[Service] +Type=oneshot +ExecStart=/usr/bin/bootc-initramfs-setup +StandardInput=null +StandardOutput=journal +StandardError=journal+console +RemainAfterExit=yes diff --git a/examples/bootc-bls/extra/usr/lib/dracut/modules.d/37bootc/module-setup.sh b/examples/bootc-bls/extra/usr/lib/dracut/modules.d/37bootc/module-setup.sh new file mode 100755 index 000000000..b1c56206f --- /dev/null +++ b/examples/bootc-bls/extra/usr/lib/dracut/modules.d/37bootc/module-setup.sh @@ -0,0 +1,20 @@ +#!/usr/bin/bash + +check() { + return 0 +} + +depends() { + return 0 +} + +install() { + inst \ + "${moddir}/bootc-initramfs-setup" /usr/bin/bootc-initramfs-setup + inst \ + "${moddir}/bootc-initramfs-setup.service" \ + "${systemdsystemunitdir}/bootc-initramfs-setup.service" + + $SYSTEMCTL -q --root "${initdir}" add-wants \ + 'initrd-root-fs.target' 'bootc-initramfs-setup.service' +} diff --git a/examples/bootc-bls/generate-ovmf-vars b/examples/bootc-bls/generate-ovmf-vars new file mode 100755 index 000000000..5f79e9c86 --- /dev/null +++ b/examples/bootc-bls/generate-ovmf-vars @@ -0,0 +1,20 @@ +#!/bin/bash + +set -eux + +cd "${0%/*}" + +if [[ ! -d "secureboot" ]]; then + echo "fail" + exit 1 +fi + +# See: https://github.com/rhuefi/qemu-ovmf-secureboot +# $ dnf install -y python3-virt-firmware +GUID=$(cat secureboot/GUID.txt) +virt-fw-vars --input "/usr/share/edk2/ovmf/OVMF_VARS_4M.secboot.qcow2" \ + --secure-boot \ + --set-pk $GUID "secureboot/PK.crt" \ + --add-kek $GUID "secureboot/KEK.crt" \ + --add-db $GUID "secureboot/db.crt" \ + -o "OVMF_VARS_CUSTOM.qcow2" diff --git a/examples/bootc-bls/podman-build-uki b/examples/bootc-bls/podman-build-uki new file mode 100755 index 000000000..6ce98539d --- /dev/null +++ b/examples/bootc-bls/podman-build-uki @@ -0,0 +1,2 @@ +#!/bin/bash +./build-fcos-uki-cocl diff --git a/examples/config.bu b/examples/config.bu new file mode 100644 index 000000000..3a019e8fb --- /dev/null +++ b/examples/config.bu @@ -0,0 +1,23 @@ +variant: fcos +version: 1.6.0 +storage: + luks: + - name: root + label: luks-root + device: /dev/disk/by-partlabel/root + uuid: "8ec9cda3-6b77-45d7-bb56-a95cd9e83234" + clevis: + tpm2: true + wipe_volume: true + filesystems: + - device: /dev/mapper/root + format: ext4 + wipe_filesystem: true + label: root + uuid: "910678ff-f77e-4a7d-8d53-86f2ac47a823" + options: [ "-O", "verity"] +systemd: + units: + - name: zincati.service + enabled: false + mask: true diff --git a/examples/libvirt.sh b/examples/libvirt.sh new file mode 100755 index 000000000..58592b166 --- /dev/null +++ b/examples/libvirt.sh @@ -0,0 +1,77 @@ +#!/bin/bash + +set -euo pipefail +# set -x + +main() { + local image="test.img" + if [[ "${#}" -eq 1 ]]; then + image="${1}" + fi + + local -r name="fedora-bootc-ignition" + local -r config="config.ign" + + IGNITION_CONFIG="$(realpath "${config}")" + IMAGE="$(realpath "${image}")" + + # Default to the stable stream as this is only used for os-variant + local -r STREAM="stable" + + VCPUS="2" + RAM_MB="4096" + DISK_GB="20" + + IGNITION_DEVICE_ARG=(--qemu-commandline="-fw_cfg name=opt/com.coreos/config,file=${IGNITION_CONFIG}") + + chcon --verbose --type svirt_home_t "${IGNITION_CONFIG}" + + virsh --connect="qemu:///system" \ + destroy "${name}" || true + virsh --connect="qemu:///system" \ + undefine "${name}" --nvram --managed-save || true + + cp "$PWD/bootc-bls/OVMF_VARS_CUSTOM.qcow2" . + + OVMF_CODE="/usr/share/edk2/ovmf/OVMF_CODE_4M.secboot.qcow2" + OVMF_VARS_TEMPLATE="/usr/share/edk2/ovmf/OVMF_VARS_4M.secboot.qcow2" + OVMF_VARS="$PWD/OVMF_VARS_CUSTOM.qcow2" + + local args=() + secureboot=true + if [[ "${secureboot}" == "true" ]]; then + loader="loader=${OVMF_CODE},loader.readonly=yes,loader.type=pflash" + nvram="nvram=${OVMF_VARS},nvram.template=${OVMF_VARS_TEMPLATE},loader_secure=yes" + features="firmware.feature0.name=secure-boot,firmware.feature0.enabled=yes,firmware.feature1.name=enrolled-keys,firmware.feature1.enabled=yes" + args+=("--boot") + args+=("uefi,${loader},${nvram},${features}") + args+=("--tpm") + args+=("backend.type=emulator,backend.version=2.0,model=tpm-tis") + else + args+=("--boot") + args+=("uefi,firmware.feature0.name=secure-boot,firmware.feature0.enabled=no") + fi + + connect_to_console="true" + if [[ "${connect_to_console}" == "true" ]]; then + args+=('--autoconsole') + args+=('text') + else + args+=('--noautoconsole') + fi + + set -x + virt-install --connect="qemu:///system" \ + --name="${name}" \ + --vcpus="${VCPUS}" \ + --memory="${RAM_MB}" \ + --os-variant="fedora-coreos-${STREAM}" \ + --import \ + --disk="size=${DISK_GB},backing_store=${IMAGE}" \ + --network bridge=virbr0 \ + "${IGNITION_DEVICE_ARG[@]}" \ + --machine q35 \ + "${args[@]}" +} + +main "${@}" diff --git a/examples/simple.bu b/examples/simple.bu new file mode 100644 index 000000000..d8081593f --- /dev/null +++ b/examples/simple.bu @@ -0,0 +1,7 @@ +variant: fcos +version: 1.6.0 +systemd: + units: + - name: zincati.service + enabled: false + mask: true diff --git a/examples/to-disk.sh b/examples/to-disk.sh new file mode 100755 index 000000000..68e384a25 --- /dev/null +++ b/examples/to-disk.sh @@ -0,0 +1,60 @@ +#!/bin/bash + +set -euxo pipefail + +IMAGE="${IMAGE:-quay.io/fedora/fedora-bootc-bls:42}" +DISKIMAGE="${DISKIMAGE:-test-disk.img}" + +if [[ ! -f systemd-bootx64.efi ]]; then + echo "Needs /srv/bootc/examples/systemd-bootx64.efi to exists for now" + exit 1 +fi + +umount -R efi || true +losetup --detach-all || true + +rm -rf "${DISKIMAGE}" +truncate -s 15G "${DISKIMAGE}" + +# -v /srv/bootc/target/release/bootc:/usr/bin/bootc:ro,Z \ +podman run \ + --rm --privileged \ + --pid=host \ + -v /dev:/dev \ + -v /var/lib/containers:/var/lib/containers \ + -v /var/tmp:/var/tmp \ + -v $PWD:/output \ + --env RUST_BACKTRACE=1 \ + --env RUST_LOG=debug \ + --security-opt label=type:unconfined_t \ + "${IMAGE}" \ + bootc install to-disk \ + --composefs-native \ + --bootloader=systemd \ + --source-imgref "containers-storage:$IMAGE" \ + --target-imgref="$IMAGE" \ + --target-transport="docker" \ + --filesystem=ext4 \ + --wipe \ + --generic-image \ + --via-loopback \ + --karg "selinux=1" \ + --karg "enforcing=0" \ + --karg "audit=0" \ + --karg "ignition.firstboot" \ + --karg "ignition.platform.id=qemu" \ + /output/"${DISKIMAGE}" + +# Manual systemd-boot installation +losetup /dev/loop0 "${DISKIMAGE}" +partx --update /dev/loop0 +mkdir -p efi +mount /dev/loop0p2 efi + +# cp systemd-bootx64.efi efi/EFI/fedora/grubx64.efi +mkdir -p efi/loader +echo "timeout 5" > efi/loader/loader.conf +rm -rf efi/EFI/fedora/grub.cfg + +umount efi +losetup -d /dev/loop0 diff --git a/examples/to-filesystem-fcos.sh b/examples/to-filesystem-fcos.sh new file mode 100755 index 000000000..7e4f5de0a --- /dev/null +++ b/examples/to-filesystem-fcos.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +set -euxo pipefail + +export IMAGE="quay.io/fedora/fedora-coreos-bls:stable" +export DISKIMAGE="${DISKIMAGE:-test-filesystem-fcos-bls.img}" +exec ./to-filesystem.sh diff --git a/examples/to-filesystem-uki-cocl.sh b/examples/to-filesystem-uki-cocl.sh new file mode 100755 index 000000000..5f4cbda83 --- /dev/null +++ b/examples/to-filesystem-uki-cocl.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +set -euxo pipefail + +export IMAGE="quay.io/fedora/fedora-coreos-uki-cocl:42.20250901.3.0" +export TARGET="quay.io/travier/fedora-coreos-uki-cocl:42.20250901.3.0" +export DISKIMAGE="${DISKIMAGE:-test-filesystem-fcos-uki-cocl.img}" +export ADDONS="--uki-addon ignition" +exec ./to-filesystem-uki.sh diff --git a/examples/to-filesystem-uki-fcos.sh b/examples/to-filesystem-uki-fcos.sh new file mode 100755 index 000000000..9d5de332f --- /dev/null +++ b/examples/to-filesystem-uki-fcos.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +set -euxo pipefail + +export IMAGE="quay.io/fedora/fedora-coreos-uki:stable" +export DISKIMAGE="${DISKIMAGE:-test-filesystem-fcos-uki.img}" +export ADDONS="--uki-addon ignition" +exec ./to-filesystem-uki.sh diff --git a/examples/to-filesystem-uki.sh b/examples/to-filesystem-uki.sh new file mode 100755 index 000000000..fcc3440a4 --- /dev/null +++ b/examples/to-filesystem-uki.sh @@ -0,0 +1,74 @@ +#!/bin/bash + +set -euxo pipefail + +IMAGE="${IMAGE:-quay.io/fedora/fedora-bootc-uki:42}" +TARGET="${TARGET:-quay.io/fedora/fedora-bootc-uki:42}" +DISKIMAGE="${DISKIMAGE:-test-filesystem-uki.img}" +ADDONS="${ADDONS:-}" + +umount -R ./mnt || true +losetup --detach-all || true + +rm -rf "${DISKIMAGE}" +truncate -s 15G "${DISKIMAGE}" + +BOOTFS_UUID="96d15588-3596-4b3c-adca-a2ff7279ea63" +ROOTFS_UUID="910678ff-f77e-4a7d-8d53-86f2ac47a823" + +cat > buf < ./mnt//loader/loader.conf + +umount -R ./mnt +losetup -d /dev/loop0 diff --git a/examples/to-filesystem.sh b/examples/to-filesystem.sh new file mode 100755 index 000000000..b70fba3e1 --- /dev/null +++ b/examples/to-filesystem.sh @@ -0,0 +1,65 @@ +#!/bin/bash + +set -euxo pipefail + +IMAGE="${IMAGE:-quay.io/fedora/fedora-bootc-bls:42}" +DISKIMAGE="${DISKIMAGE:-test-filesystem-bls.img}" + +umount -R ./mnt || true +losetup --detach-all || true + +rm -rf "${DISKIMAGE}" +truncate -s 15G "${DISKIMAGE}" + +BOOTFS_UUID="96d15588-3596-4b3c-adca-a2ff7279ea63" +ROOTFS_UUID="910678ff-f77e-4a7d-8d53-86f2ac47a823" + +cat > buf < ./mnt//loader/loader.conf +# ignition.firstboot ignition.platform.id=qemu +# rd.systemd.default_debug_tty=ttyS0 +sed -i "s;options ;options console=ttyS0,115000n selinux=1 enforcing=0 audit=0 ignition.firstboot ignition.platform.id=qemu rd.systemd.debug_shell=1 rd.systemd.default_debug_tty=tty0 ;" ./mnt/loader/entries/bootc-composefs-1.conf + +umount -R ./mnt +losetup -d /dev/loop0