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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -225,9 +225,14 @@ jobs:
seal_state: ["sealed", "unsealed"]

exclude:
# https://github.com/bootc-dev/bootc/issues/1812
# centos-9 composefs: only sealed UKI is supported (V1 EROFS).
# BLS and unsealed modes require newer dracut/systemd features.
- test_os: centos-9
variant: composefs
boot_type: bls
- test_os: centos-9
variant: composefs
seal_state: unsealed
- seal_state: "sealed"
boot_type: bls
- seal_state: "sealed"
Expand Down
3 changes: 2 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,7 @@ ARG variant
ARG filesystem
ARG seal_state
ARG boot_type
ARG erofs_version=v1
# Install our bootc package (only needed for the compute-composefs-digest command)
RUN --network=none --mount=type=tmpfs,target=/run --mount=type=tmpfs,target=/tmp \
--mount=type=bind,from=packages,src=/,target=/run/packages \
Expand All @@ -338,7 +339,7 @@ if [[ $filesystem == "xfs" ]]; then
fi

if test "${boot_type}" = "uki"; then
/run/packaging/seal-uki /run/target /out /run/secrets $allow_missing_verity $seal_state
/run/packaging/seal-uki /run/target /out /run/secrets $allow_missing_verity $seal_state $erofs_version
fi
EORUN

Expand Down
6 changes: 5 additions & 1 deletion Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ filesystem := env("BOOTC_filesystem", "ext4")
boot_type := env("BOOTC_boot_type", "bls")
# Only used for composefs tests
seal_state := env("BOOTC_seal_state", "unsealed")
# Only used for composefs UKI tests: "v1" or "v2"
erofs_version := env("BOOTC_erofs_version", "v1")
# Baseconfigs to inject into the image for testing (e.g. "etc-transient" or "root-transient")
baseconfigs := env("BOOTC_baseconfigs", "")
# Base container image to build from
Expand Down Expand Up @@ -72,6 +74,7 @@ base_buildargs := generic_buildargs + " " + _extra_src_args \
+ " --build-arg=boot_type=" + boot_type \
+ " --build-arg=seal_state=" + seal_state \
+ " --build-arg=filesystem=" + filesystem \
+ " --build-arg=erofs_version=" + erofs_version \
+ " --build-arg=baseconfigs=" + baseconfigs
buildargs := base_buildargs \
+ " --cap-add=all --security-opt=label=type:container_runtime_t --device /dev/fuse" \
Expand Down Expand Up @@ -271,7 +274,7 @@ test-container-export: build
# Run tmt tests without rebuilding (for fast iteration)
[group('testing')]
test-tmt-nobuild *ARGS:
cargo xtask run-tmt --env=BOOTC_variant={{variant}} {{_baseconfigs_env}} --upgrade-image={{upgrade_img}} {{base_img}} {{ARGS}}
cargo xtask run-tmt --env=BOOTC_variant={{variant}} --env=BOOTC_erofs_version={{erofs_version}} {{_baseconfigs_env}} --upgrade-image={{upgrade_img}} {{base_img}} {{ARGS}}

# Run readonly tests with a baseconfig baked into the image at build time.
# Requires composefs variant. Example: just variant=composefs test-tmt-baseconfig root-transient
Expand Down Expand Up @@ -489,6 +492,7 @@ _build-upgrade-image:
--build-arg "boot_type={{boot_type}}" \
--build-arg "seal_state={{seal_state}}" \
--build-arg "filesystem={{filesystem}}" \
--build-arg "erofs_version={{erofs_version}}" \
--secret=id=secureboot_key,src=target/test-secureboot/db.key \
--secret=id=secureboot_cert,src=target/test-secureboot/db.crt \
"${extra_args[@]}" \
Expand Down
5 changes: 4 additions & 1 deletion contrib/packaging/seal-uki
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ allow_missing_verity=$1
shift
seal_state=$1
shift
# EROFS format version to pass to bootc container ukify (optional, default: v1)
erofs_version=${1:-v1}
shift || true

if [[ $seal_state == "sealed" && $allow_missing_verity == "true" ]]; then
echo "Cannot have missing verity with sealed UKI" >&2
Expand Down Expand Up @@ -53,4 +56,4 @@ fi

# Build the UKI using bootc container ukify
# This computes the composefs digest, reads kargs from kargs.d, and invokes ukify
bootc container ukify "${containerukifyargs[@]}" "${missing_verity[@]}" -- "${ukifyargs[@]}"
bootc container ukify "${containerukifyargs[@]}" "${missing_verity[@]}" --erofs-version="${erofs_version}" -- "${ukifyargs[@]}"
157 changes: 121 additions & 36 deletions crates/lib/src/bootc_composefs/boot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ use cap_std_ext::{
dirext::CapStdExtDirExt,
};
use clap::ValueEnum;
use composefs::erofs::format::FormatVersion;
use composefs::fs::read_file;
use composefs::fsverity::{FsVerityHashValue, Sha512HashValue};
use composefs::tree::RegularFile;
Expand All @@ -83,7 +84,7 @@ use composefs_boot::bootloader::{
UsrLibModulesVmlinuz, get_boot_resources,
};
use composefs_boot::{
cmdline::ComposefsCmdline as ComposefsBootCmdline, os_release::OsReleaseInfo, uki,
cmdline::ComposefsCmdline as BootComposefsCmdline, os_release::OsReleaseInfo, uki,
};
use composefs_ctl::composefs;
use composefs_ctl::composefs_boot;
Expand All @@ -94,7 +95,7 @@ use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

use crate::bootc_composefs::state::{get_booted_bls, write_composefs_state};
use crate::bootc_composefs::status::ComposefsCmdline;
use crate::bootc_composefs::status::build_composefs_karg;
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, EFIKey};
Expand Down Expand Up @@ -134,6 +135,7 @@ const AUTH_EXT: &str = "auth";
/// This is relative to the ESP
pub(crate) const BOOTC_UKI_DIR: &str = "EFI/Linux/bootc";

#[derive(Copy, Clone)]
pub(crate) enum BootSetupType<'a> {
/// For initial setup, i.e. install to-disk
Setup((&'a RootSetup, &'a State, &'a PostFetchState)),
Expand Down Expand Up @@ -493,6 +495,7 @@ pub(crate) fn setup_composefs_bls_boot(
setup_type: BootSetupType,
repo: crate::store::ComposefsRepository,
id: &Sha512HashValue,
format_version: FormatVersion,
entry: &ComposefsBootEntry<Sha512HashValue>,
mounted_erofs: &Dir,
) -> Result<String> {
Expand All @@ -505,9 +508,12 @@ pub(crate) fn setup_composefs_bls_boot(

cmdline_options.extend(&root_setup.kargs);

let composefs_cmdline =
ComposefsCmdline::build(&id_hex, state.composefs_options.allow_missing_verity);
cmdline_options.extend(&Cmdline::from(&composefs_cmdline.to_string()));
let composefs_cmdline = build_composefs_karg(
id.clone(),
format_version,
state.composefs_options.allow_missing_verity,
);
cmdline_options.extend(&Cmdline::from(&composefs_cmdline));

// If there's a separate /boot partition, add a systemd.mount-extra
// karg so systemd mounts it after reboot. This avoids writing to
Expand Down Expand Up @@ -554,9 +560,11 @@ pub(crate) fn setup_composefs_bls_boot(
};

// Copy all cmdline args, replacing only `composefs=`
let cfs_cmdline =
ComposefsCmdline::build(&id_hex, booted_cfs.cmdline.allow_missing_fsverity)
.to_string();
let cfs_cmdline = build_composefs_karg(
id.clone(),
format_version,
booted_cfs.cmdline.allow_missing_fsverity,
);

let param = Parameter::parse(&cfs_cmdline)
.context("Failed to create 'composefs=' parameter")?;
Expand Down Expand Up @@ -784,6 +792,11 @@ struct UKIInfo {
version: Option<String>,
os_id: Option<String>,
boot_digest: String,
/// The composefs image digest parsed from (and validated against) the UKI's
/// own cmdline. For UKI boots, setup-root opens `state/deploy/<this>` using
/// the karg baked into the UKI, so the deploy directory must be named after
/// exactly this value regardless of which EROFS format (V1 or V2) was sealed.
composefs_digest: Sha512HashValue,
}

/// Writes a PortableExecutable to ESP along with any PE specific or Global addons
Expand All @@ -794,6 +807,7 @@ fn write_pe_to_esp(
file_path: &Utf8Path,
pe_type: PEType,
uki_id: &Sha512HashValue,
boot_ids: &[Sha512HashValue],
missing_fsverity_allowed: bool,
mounted_efi: impl AsRef<Path>,
) -> Result<Option<UKIInfo>> {
Expand All @@ -812,10 +826,12 @@ fn write_pe_to_esp(
if matches!(pe_type, PEType::Uki) {
let cmdline = uki::get_cmdline_buffered(&mut uki_reader).context("Getting UKI cmdline")?;

let composefs_info = ComposefsBootCmdline::<Sha512HashValue>::from_cmdline(&cmdline)
let composefs_info = BootComposefsCmdline::<Sha512HashValue>::from_cmdline(&cmdline)
.context("Parsing composefs=")?
.ok_or_else(|| anyhow::anyhow!("No composefs image in UKI cmdline"))?;
let composefs_cmdline = composefs_info.digest();
.ok_or_else(|| {
anyhow::anyhow!("No composefs= or composefs.digest.v1= karg found in UKI cmdline")
})?;
let composefs_digest = composefs_info.digest().clone();
let missing_verity_allowed_cmdline = composefs_info.is_insecure();

// If the UKI cmdline does not match what the user has passed as cmdline option
Expand All @@ -834,11 +850,9 @@ fn write_pe_to_esp(
_ => { /* no-op */ }
}

if *composefs_cmdline != *uki_id {
anyhow::bail!(
"The UKI has the wrong composefs= parameter (is '{composefs_cmdline:?}', should be {uki_id:?})"
);
}
composefs_info
.validate_digest(boot_ids)
.context("Validating UKI composefs digest")?;

uki_reader.seek(SeekFrom::Start(0))?;
let osrel = uki::get_text_section_buffered(&mut uki_reader, ".osrel")?;
Expand All @@ -855,6 +869,7 @@ fn write_pe_to_esp(
version: parsed_osrel.get_version(),
os_id: parsed_osrel.get_value(&["ID"]),
boot_digest,
composefs_digest,
});
}

Expand Down Expand Up @@ -888,8 +903,19 @@ fn write_pe_to_esp(
let pe_dir = Dir::open_ambient_dir(&final_pe_path, ambient_authority())
.with_context(|| format!("Opening {final_pe_path:?}"))?;

// For UKIs, name the .efi file after the composefs cmdline digest (the
// deploy key that setup-root uses), NOT the provisional uki_id. When the
// UKI was sealed with --erofs-version=v1, uki_id (v2) and the cmdline
// digest (v1) differ; using the cmdline digest keeps the filename, BLS
// config, and state directory in agreement.
let pe_name_owned;
let pe_name = match pe_type {
PEType::Uki => &get_uki_name(&uki_id.to_hex()),
PEType::Uki => {
// SAFETY: boot_label is always set to Some above when pe_type == Uki
let deploy_digest = &boot_label.as_ref().unwrap().composefs_digest;
pe_name_owned = get_uki_name(&deploy_digest.to_hex());
&pe_name_owned
}
PEType::UkiAddon => file_path
.components()
.last()
Expand Down Expand Up @@ -1074,8 +1100,9 @@ pub(crate) fn setup_composefs_uki_boot(
setup_type: BootSetupType,
repo: crate::store::ComposefsRepository,
id: &Sha512HashValue,
boot_ids: &[Sha512HashValue],
entries: Vec<ComposefsBootEntry<Sha512HashValue>>,
) -> Result<String> {
) -> Result<(String, Sha512HashValue)> {
let (root_path, esp_device, bootloader, missing_fsverity_allowed, uki_addons) = match setup_type
{
BootSetupType::Setup((root_setup, state, postfetch)) => {
Expand Down Expand Up @@ -1155,7 +1182,8 @@ pub(crate) fn setup_composefs_uki_boot(
&entry.file,
utf8_file_path,
entry.pe_type,
&id,
id,
boot_ids,
missing_fsverity_allowed,
esp_mount.dir.path(),
)?;
Expand All @@ -1172,17 +1200,32 @@ pub(crate) fn setup_composefs_uki_boot(

let boot_digest = uki_info.boot_digest.clone();

// The deploy key for a UKI boot is the composefs digest baked into the UKI
// cmdline (already validated against `boot_ids` in `write_pe_to_esp`).
// setup-root opens `state/deploy/<this>` using that same karg, so we must
// key the deployment off exactly this value -- whether the UKI was sealed
// with a V1 or V2 EROFS digest.
let deploy_id = uki_info.composefs_digest.clone();

match bootloader.kind()? {
BootloaderKind::GRUBClassic => {
write_grub_uki_menuentry(root_path, &setup_type, uki_info.boot_label, id, &esp_device)?
}
BootloaderKind::GRUBClassic => write_grub_uki_menuentry(
root_path,
&setup_type,
uki_info.boot_label,
&deploy_id,
&esp_device,
)?,

BootloaderKind::BLSCompatible => {
write_systemd_uki_config(&esp_mount.fd, &setup_type, uki_info, id, &bootloader)?
}
BootloaderKind::BLSCompatible => write_systemd_uki_config(
&esp_mount.fd,
&setup_type,
uki_info,
&deploy_id,
&bootloader,
)?,
};

Ok(boot_digest)
Ok((boot_digest, deploy_id))
}

/// A composefs image attached to a temporary directory with the ESP and a
Expand Down Expand Up @@ -1353,6 +1396,15 @@ pub(crate) async fn setup_composefs_boot(
let id = composefs_oci::generate_boot_image(&repo, &pull_result.manifest_digest)
.context("Generating bootable EROFS image")?;

// Open the OCI image to read both stored boot EROFS digests. The UKI may
// have been sealed with either the V1 or V2 boot image digest, so we need
// both for verification.
let oci_img =
composefs_oci::oci_image::OciImage::open(&*repo, &pull_result.manifest_digest, None)
.context("Opening OCI image to read boot image refs")?;
let boot_id_v1 = oci_img.boot_image_ref_v1().cloned();
let boot_id_v2 = oci_img.boot_image_ref_v2().cloned();

// Reconstruct the OCI filesystem to discover boot entries (kernel, initramfs, etc.).
let fs = composefs_oci::image::create_filesystem(&*repo, &pull_result.config_digest, None)
.context("Creating composefs filesystem for boot entry discovery")?;
Expand Down Expand Up @@ -1446,25 +1498,58 @@ pub(crate) async fn setup_composefs_boot(
)
})?;

let boot_digest = match boot_type {
BootType::Bls => setup_composefs_bls_boot(
BootSetupType::Setup((&root_setup, &state, &postfetch)),
repo,
&id,
entry,
mounted_root.dir(),
)?,
// The deployment key is the hash that setup-root looks for in
// state/deploy/<hash>, derived from the composefs karg. The two boot types
// establish that karg differently:
//
// * BLS: bootc writes the karg itself, so we are free to choose the key.
// Prefer the V1 digest, falling back to the primary id for legacy repos
// with no separate V1 boot ref. V1 is the preferred default because it
// works on both RHEL9 (which lacks V2 support) and newer kernels; this
// must stay in sync with the same choice made for upgrades in
// `do_upgrade`.
//
// * UKI: the karg is baked into the UKI at seal time, so the key is
// whatever digest the UKI carries. `setup_composefs_uki_boot` parses and
// validates that against the boot refs and returns it, so we override the
// provisional value below. This keeps us correct whether the UKI was
// sealed with the V1 (default) or V2 EROFS digest.
//
// `provisional_format` tracks which EROFS format `provisional_deploy_id`
// actually is, so the BLS karg we write can be tagged correctly (see
// `build_composefs_karg`).
let (provisional_deploy_id, provisional_format) = match boot_id_v1.as_ref() {
Some(v1) => (v1.clone(), FormatVersion::V1),
None => (id.clone(), repo.erofs_version()),
};

// Collect whichever boot image refs exist; the UKI cmdline may carry either.
let boot_ids: Vec<Sha512HashValue> = [boot_id_v1, boot_id_v2].into_iter().flatten().collect();

let (boot_digest, deploy_id) = match boot_type {
BootType::Bls => (
setup_composefs_bls_boot(
BootSetupType::Setup((&root_setup, &state, &postfetch)),
repo,
&provisional_deploy_id,
provisional_format,
entry,
mounted_root.dir(),
)?,
provisional_deploy_id,
),
BootType::Uki => setup_composefs_uki_boot(
BootSetupType::Setup((&root_setup, &state, &postfetch)),
repo,
&id,
&provisional_deploy_id,
&boot_ids,
entries,
)?,
};

write_composefs_state(
&root_setup.physical_root_path,
&id,
&deploy_id,
&crate::spec::ImageReference::from(state.target_imgref.clone()),
None,
boot_type,
Expand Down
Loading
Loading