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
274 changes: 250 additions & 24 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ clap_mangen = { version = "0.3.0" }
# [patch."https://github.com/composefs/composefs-rs"]
# composefs-ctl = { path = "/path/to/composefs-rs/crates/composefs-ctl" }
# The Justfile will auto-detect these and bind-mount them into container builds.
composefs-ctl = { git = "https://github.com/composefs/composefs-rs", rev = "e2770757762ec5091bb183bf0e778fe97c8d5694" }
composefs-ctl = { git = "https://github.com/composefs/composefs-rs", rev = "469f6d635abcd5604ac99ca65ab37f58c516045d" }
fn-error-context = "0.2.1"
futures-util = "0.3"
hex = "0.4.3"
Expand Down
3 changes: 2 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,7 @@ ARG variant
ARG filesystem
ARG seal_state
ARG boot_type
ARG erofs_version=v2
# 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 @@ -295,7 +296,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", "v2")
# 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: v2)
erofs_version=${1:-v2}
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[@]}"
1 change: 1 addition & 0 deletions crates/etc-merge/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ impl From<(&cap_std::fs::Metadata, Xattrs)> for MyStat {
st_uid: value.0.uid(),
st_gid: value.0.gid(),
st_mtim_sec: value.0.mtime(),
st_mtim_nsec: value.0.mtime_nsec() as u32,
xattrs: value.1,
})
}
Expand Down
19 changes: 14 additions & 5 deletions crates/initramfs/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ use composefs::{
mountcompat::{overlayfs_set_fd, overlayfs_set_lower_and_data_fds, prepare_mount},
repository::Repository,
};
use composefs_boot::cmdline::get_cmdline_composefs;
use composefs_boot::cmdline::ComposefsCmdline;
use composefs_ctl::composefs;
use composefs_ctl::composefs_boot;

Expand Down Expand Up @@ -463,11 +463,17 @@ pub fn setup_root(args: Args) -> Result<()> {
config
};

let (image, insecure) = get_cmdline_composefs::<Sha512HashValue>(&cmdline)?;
let composefs_info = ComposefsCmdline::<Sha512HashValue>::from_cmdline(&cmdline)
.context("Failed to parse composefs cmdline")?
.ok_or_else(|| anyhow::anyhow!("No composefs image in cmdline"))?;

let new_root = match &args.root_fs {
Some(path) => open_root_fs(path).context("Failed to clone specified root fs")?,
None => mount_composefs_image(&sysroot, &image.to_hex(), insecure)?,
None => mount_composefs_image(
&sysroot,
&composefs_info.digest().to_hex(),
composefs_info.is_insecure(),
)?,
};

// we need to clone this before the next step to make sure we get the old one
Expand Down Expand Up @@ -497,7 +503,7 @@ pub fn setup_root(args: Args) -> Result<()> {
let transient_overlay_fd: Option<OwnedFd> = if config.root.transient {
let overlay_fd = overlay_transient(
&new_root,
&format!("transient:composefs={}", image.to_hex()),
&format!("transient:composefs={}", composefs_info.digest().to_hex()),
None,
)?;

Expand Down Expand Up @@ -533,7 +539,10 @@ pub fn setup_root(args: Args) -> Result<()> {
}

// etc + var
let state = open_dir(open_dir(&sysroot, "state/deploy")?, image.to_hex())?;
let state = open_dir(
open_dir(&sysroot, "state/deploy")?,
composefs_info.digest().to_hex(),
)?;
mount_subdir(visible_root, &state, "etc", config.etc, MountType::Bind)?;
// /var is bind-mounted from the deployment state directory by default.
// The systemd.volatile=state cmdline detection above (or an explicit
Expand Down
121 changes: 95 additions & 26 deletions crates/lib/src/bootc_composefs/boot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,9 @@ use composefs_boot::bootloader::{
BootEntry as ComposefsBootEntry, EFI_ADDON_DIR_EXT, EFI_ADDON_FILE_EXT, EFI_EXT, PEType,
UsrLibModulesVmlinuz, get_boot_resources,
};
use composefs_boot::{cmdline::get_cmdline_composefs, os_release::OsReleaseInfo, uki};
use composefs_boot::{
cmdline::ComposefsCmdline as BootComposefsCmdline, os_release::OsReleaseInfo, uki,
};
use composefs_ctl::composefs;
use composefs_ctl::composefs_boot;
use composefs_ctl::composefs_oci;
Expand Down Expand Up @@ -783,6 +785,12 @@ 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. This is the authoritative deployment key 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_cmdline: Sha512HashValue,
}

/// Writes a PortableExecutable to ESP along with any PE specific or Global addons
Expand All @@ -793,6 +801,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 @@ -811,8 +820,11 @@ 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_cmdline, missing_verity_allowed_cmdline) =
get_cmdline_composefs::<Sha512HashValue>(&cmdline).context("Parsing composefs=")?;
let composefs_info = BootComposefsCmdline::<Sha512HashValue>::from_cmdline(&cmdline)
.context("Parsing composefs=")?
.ok_or_else(|| anyhow::anyhow!("No composefs= or composefs.digest.v1= karg found in UKI cmdline"))?;
let composefs_cmdline = 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
// NOTE: This will only be checked for new installs and now upgrades/switches
Expand All @@ -830,11 +842,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.iter().copied())
.context("Validating UKI composefs digest")?;

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

Expand Down Expand Up @@ -884,8 +895,18 @@ 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 => {
let deploy_digest = &boot_label.as_ref().unwrap().composefs_cmdline;
pe_name_owned = get_uki_name(&deploy_digest.to_hex());
&pe_name_owned
}
PEType::UkiAddon => file_path
.components()
.last()
Expand Down Expand Up @@ -1066,8 +1087,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 @@ -1147,7 +1169,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 @@ -1164,17 +1187,30 @@ 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 the V2 (default) or V1 EROFS digest.
let deploy_id = uki_info.composefs_cmdline.clone();

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

Bootloader::Systemd => write_systemd_uki_config(&esp_mount.fd, &setup_type, uki_info, id)?,
Bootloader::Systemd => {
write_systemd_uki_config(&esp_mount.fd, &setup_type, uki_info, &deploy_id)?
}

Bootloader::None => unreachable!("Checked at install time"),
};

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 @@ -1345,6 +1381,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 @@ -1397,25 +1442,49 @@ 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 V2 digest (matching the default EROFS format), falling back
// to the primary id for legacy repos with no separate V2 boot ref.
//
// * 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 V2 (default) or V1 EROFS digest.
let provisional_deploy_id = boot_id_v2.as_ref().cloned().unwrap_or_else(|| id.clone());

// Collect whichever boot image refs exist; the UKI cmdline may carry either.
let boot_ids_owned: Vec<Sha512HashValue> =
[boot_id_v1, boot_id_v2].into_iter().flatten().collect();
let boot_ids: Vec<&Sha512HashValue> = boot_ids_owned.iter().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,
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