Skip to content

Commit 69757fc

Browse files
committed
composefs: Generate both V1 and V2 EROFS images on install
composefs-rs landed support for V1 EROFS, which we need to enable composefs on RHEL9. Make new installs produce both V1 and V2 EROFS images for committed composefs images so a deployment can be booted via either the composefs= (V2) or composefs.digest.v1= (V1) karg. Karg generation and digest computation default to V2 for now; this change only ensures both digests are available on disk. Assisted-by: OpenCode (Claude Sonnet 4.6) Signed-off-by: Colin Walters <walters@verbum.org>
1 parent 656cb4f commit 69757fc

16 files changed

Lines changed: 465 additions & 97 deletions

File tree

Dockerfile

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,7 @@ ARG variant
277277
ARG filesystem
278278
ARG seal_state
279279
ARG boot_type
280+
ARG erofs_version=v2
280281
# Install our bootc package (only needed for the compute-composefs-digest command)
281282
RUN --network=none --mount=type=tmpfs,target=/run --mount=type=tmpfs,target=/tmp \
282283
--mount=type=bind,from=packages,src=/,target=/run/packages \
@@ -295,7 +296,7 @@ if [[ $filesystem == "xfs" ]]; then
295296
fi
296297

297298
if test "${boot_type}" = "uki"; then
298-
/run/packaging/seal-uki /run/target /out /run/secrets $allow_missing_verity $seal_state
299+
/run/packaging/seal-uki /run/target /out /run/secrets $allow_missing_verity $seal_state $erofs_version
299300
fi
300301
EORUN
301302

Justfile

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ filesystem := env("BOOTC_filesystem", "ext4")
4343
boot_type := env("BOOTC_boot_type", "bls")
4444
# Only used for composefs tests
4545
seal_state := env("BOOTC_seal_state", "unsealed")
46+
# Only used for composefs UKI tests: "v1" or "v2"
47+
erofs_version := env("BOOTC_erofs_version", "v2")
4648
# Baseconfigs to inject into the image for testing (e.g. "etc-transient" or "root-transient")
4749
baseconfigs := env("BOOTC_baseconfigs", "")
4850
# Base container image to build from
@@ -72,6 +74,7 @@ base_buildargs := generic_buildargs + " " + _extra_src_args \
7274
+ " --build-arg=boot_type=" + boot_type \
7375
+ " --build-arg=seal_state=" + seal_state \
7476
+ " --build-arg=filesystem=" + filesystem \
77+
+ " --build-arg=erofs_version=" + erofs_version \
7578
+ " --build-arg=baseconfigs=" + baseconfigs
7679
buildargs := base_buildargs \
7780
+ " --cap-add=all --security-opt=label=type:container_runtime_t --device /dev/fuse" \
@@ -271,7 +274,7 @@ test-container-export: build
271274
# Run tmt tests without rebuilding (for fast iteration)
272275
[group('testing')]
273276
test-tmt-nobuild *ARGS:
274-
cargo xtask run-tmt --env=BOOTC_variant={{variant}} {{_baseconfigs_env}} --upgrade-image={{upgrade_img}} {{base_img}} {{ARGS}}
277+
cargo xtask run-tmt --env=BOOTC_variant={{variant}} --env=BOOTC_erofs_version={{erofs_version}} {{_baseconfigs_env}} --upgrade-image={{upgrade_img}} {{base_img}} {{ARGS}}
275278

276279
# Run readonly tests with a baseconfig baked into the image at build time.
277280
# Requires composefs variant. Example: just variant=composefs test-tmt-baseconfig root-transient
@@ -489,6 +492,7 @@ _build-upgrade-image:
489492
--build-arg "boot_type={{boot_type}}" \
490493
--build-arg "seal_state={{seal_state}}" \
491494
--build-arg "filesystem={{filesystem}}" \
495+
--build-arg "erofs_version={{erofs_version}}" \
492496
--secret=id=secureboot_key,src=target/test-secureboot/db.key \
493497
--secret=id=secureboot_cert,src=target/test-secureboot/db.crt \
494498
"${extra_args[@]}" \

contrib/packaging/seal-uki

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ allow_missing_verity=$1
1515
shift
1616
seal_state=$1
1717
shift
18+
# EROFS format version to pass to bootc container ukify (optional, default: v2)
19+
erofs_version=${1:-v2}
20+
shift || true
1821

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

5457
# Build the UKI using bootc container ukify
5558
# This computes the composefs digest, reads kargs from kargs.d, and invokes ukify
56-
bootc container ukify "${containerukifyargs[@]}" "${missing_verity[@]}" -- "${ukifyargs[@]}"
59+
bootc container ukify "${containerukifyargs[@]}" "${missing_verity[@]}" --erofs-version="${erofs_version}" -- "${ukifyargs[@]}"

crates/lib/src/bootc_composefs/boot.rs

Lines changed: 88 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -785,6 +785,12 @@ struct UKIInfo {
785785
version: Option<String>,
786786
os_id: Option<String>,
787787
boot_digest: String,
788+
/// The composefs image digest parsed from (and validated against) the UKI's
789+
/// own cmdline. This is the authoritative deployment key for UKI boots:
790+
/// setup-root opens `state/deploy/<this>` using the karg baked into the UKI,
791+
/// so the deploy directory must be named after exactly this value regardless
792+
/// of which EROFS format (V1 or V2) was sealed.
793+
composefs_cmdline: Sha512HashValue,
788794
}
789795

790796
/// Writes a PortableExecutable to ESP along with any PE specific or Global addons
@@ -795,6 +801,7 @@ fn write_pe_to_esp(
795801
file_path: &Utf8Path,
796802
pe_type: PEType,
797803
uki_id: &Sha512HashValue,
804+
boot_ids: &[&Sha512HashValue],
798805
missing_fsverity_allowed: bool,
799806
mounted_efi: impl AsRef<Path>,
800807
) -> Result<Option<UKIInfo>> {
@@ -816,7 +823,7 @@ fn write_pe_to_esp(
816823
let composefs_info = BootComposefsCmdline::<Sha512HashValue>::from_cmdline(&cmdline)
817824
.context("Parsing composefs=")?
818825
.ok_or_else(|| anyhow::anyhow!("No composefs= or composefs.digest.v1= karg found in UKI cmdline"))?;
819-
let composefs_cmdline = composefs_info.digest();
826+
let composefs_cmdline = composefs_info.digest().clone();
820827
let missing_verity_allowed_cmdline = composefs_info.is_insecure();
821828

822829
// If the UKI cmdline does not match what the user has passed as cmdline option
@@ -835,11 +842,9 @@ fn write_pe_to_esp(
835842
_ => { /* no-op */ }
836843
}
837844

838-
if *composefs_cmdline != *uki_id {
839-
anyhow::bail!(
840-
"The UKI has the wrong composefs= parameter (is '{composefs_cmdline:?}', should be {uki_id:?})"
841-
);
842-
}
845+
composefs_info
846+
.validate_digest(boot_ids.iter().copied())
847+
.context("Validating UKI composefs digest")?;
843848

844849
uki_reader.seek(SeekFrom::Start(0))?;
845850
let osrel = uki::get_text_section_buffered(&mut uki_reader, ".osrel")?;
@@ -856,6 +861,7 @@ fn write_pe_to_esp(
856861
version: parsed_osrel.get_version(),
857862
os_id: parsed_osrel.get_value(&["ID"]),
858863
boot_digest,
864+
composefs_cmdline,
859865
});
860866
}
861867

@@ -889,8 +895,18 @@ fn write_pe_to_esp(
889895
let pe_dir = Dir::open_ambient_dir(&final_pe_path, ambient_authority())
890896
.with_context(|| format!("Opening {final_pe_path:?}"))?;
891897

898+
// For UKIs, name the .efi file after the composefs cmdline digest (the
899+
// deploy key that setup-root uses), NOT the provisional uki_id. When the
900+
// UKI was sealed with --erofs-version=v1, uki_id (v2) and the cmdline
901+
// digest (v1) differ; using the cmdline digest keeps the filename, BLS
902+
// config, and state directory in agreement.
903+
let pe_name_owned;
892904
let pe_name = match pe_type {
893-
PEType::Uki => &get_uki_name(&uki_id.to_hex()),
905+
PEType::Uki => {
906+
let deploy_digest = &boot_label.as_ref().unwrap().composefs_cmdline;
907+
pe_name_owned = get_uki_name(&deploy_digest.to_hex());
908+
&pe_name_owned
909+
}
894910
PEType::UkiAddon => file_path
895911
.components()
896912
.last()
@@ -1071,8 +1087,9 @@ pub(crate) fn setup_composefs_uki_boot(
10711087
setup_type: BootSetupType,
10721088
repo: crate::store::ComposefsRepository,
10731089
id: &Sha512HashValue,
1090+
boot_ids: &[&Sha512HashValue],
10741091
entries: Vec<ComposefsBootEntry<Sha512HashValue>>,
1075-
) -> Result<String> {
1092+
) -> Result<(String, Sha512HashValue)> {
10761093
let (root_path, esp_device, bootloader, missing_fsverity_allowed, uki_addons) = match setup_type
10771094
{
10781095
BootSetupType::Setup((root_setup, state, postfetch)) => {
@@ -1152,7 +1169,8 @@ pub(crate) fn setup_composefs_uki_boot(
11521169
&entry.file,
11531170
utf8_file_path,
11541171
entry.pe_type,
1155-
&id,
1172+
id,
1173+
boot_ids,
11561174
missing_fsverity_allowed,
11571175
esp_mount.dir.path(),
11581176
)?;
@@ -1169,17 +1187,30 @@ pub(crate) fn setup_composefs_uki_boot(
11691187

11701188
let boot_digest = uki_info.boot_digest.clone();
11711189

1190+
// The deploy key for a UKI boot is the composefs digest baked into the UKI
1191+
// cmdline (already validated against `boot_ids` in `write_pe_to_esp`).
1192+
// setup-root opens `state/deploy/<this>` using that same karg, so we must
1193+
// key the deployment off exactly this value -- whether the UKI was sealed
1194+
// with the V2 (default) or V1 EROFS digest.
1195+
let deploy_id = uki_info.composefs_cmdline.clone();
1196+
11721197
match bootloader {
1173-
Bootloader::Grub => {
1174-
write_grub_uki_menuentry(root_path, &setup_type, uki_info.boot_label, id, &esp_device)?
1175-
}
1198+
Bootloader::Grub => write_grub_uki_menuentry(
1199+
root_path,
1200+
&setup_type,
1201+
uki_info.boot_label,
1202+
&deploy_id,
1203+
&esp_device,
1204+
)?,
11761205

1177-
Bootloader::Systemd => write_systemd_uki_config(&esp_mount.fd, &setup_type, uki_info, id)?,
1206+
Bootloader::Systemd => {
1207+
write_systemd_uki_config(&esp_mount.fd, &setup_type, uki_info, &deploy_id)?
1208+
}
11781209

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

1182-
Ok(boot_digest)
1213+
Ok((boot_digest, deploy_id))
11831214
}
11841215

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

1384+
// Open the OCI image to read both stored boot EROFS digests. The UKI may
1385+
// have been sealed with either the V1 or V2 boot image digest, so we need
1386+
// both for verification.
1387+
let oci_img =
1388+
composefs_oci::oci_image::OciImage::open(&*repo, &pull_result.manifest_digest, None)
1389+
.context("Opening OCI image to read boot image refs")?;
1390+
let boot_id_v1 = oci_img.boot_image_ref_v1().cloned();
1391+
let boot_id_v2 = oci_img.boot_image_ref_v2().cloned();
1392+
13531393
// Reconstruct the OCI filesystem to discover boot entries (kernel, initramfs, etc.).
13541394
let fs = composefs_oci::image::create_filesystem(&*repo, &pull_result.config_digest, None)
13551395
.context("Creating composefs filesystem for boot entry discovery")?;
@@ -1402,25 +1442,49 @@ pub(crate) async fn setup_composefs_boot(
14021442
)
14031443
})?;
14041444

1405-
let boot_digest = match boot_type {
1406-
BootType::Bls => setup_composefs_bls_boot(
1407-
BootSetupType::Setup((&root_setup, &state, &postfetch)),
1408-
repo,
1409-
&id,
1410-
entry,
1411-
mounted_root.dir(),
1412-
)?,
1445+
// The deployment key is the hash that setup-root looks for in
1446+
// state/deploy/<hash>, derived from the composefs karg. The two boot types
1447+
// establish that karg differently:
1448+
//
1449+
// * BLS: bootc writes the karg itself, so we are free to choose the key.
1450+
// Prefer the V2 digest (matching the default EROFS format), falling back
1451+
// to the primary id for legacy repos with no separate V2 boot ref.
1452+
//
1453+
// * UKI: the karg is baked into the UKI at seal time, so the key is
1454+
// whatever digest the UKI carries. `setup_composefs_uki_boot` parses and
1455+
// validates that against the boot refs and returns it, so we override the
1456+
// provisional value below. This keeps us correct whether the UKI was
1457+
// sealed with the V2 (default) or V1 EROFS digest.
1458+
let provisional_deploy_id = boot_id_v2.as_ref().cloned().unwrap_or_else(|| id.clone());
1459+
1460+
// Collect whichever boot image refs exist; the UKI cmdline may carry either.
1461+
let boot_ids_owned: Vec<Sha512HashValue> =
1462+
[boot_id_v1, boot_id_v2].into_iter().flatten().collect();
1463+
let boot_ids: Vec<&Sha512HashValue> = boot_ids_owned.iter().collect();
1464+
1465+
let (boot_digest, deploy_id) = match boot_type {
1466+
BootType::Bls => (
1467+
setup_composefs_bls_boot(
1468+
BootSetupType::Setup((&root_setup, &state, &postfetch)),
1469+
repo,
1470+
&provisional_deploy_id,
1471+
entry,
1472+
mounted_root.dir(),
1473+
)?,
1474+
provisional_deploy_id,
1475+
),
14131476
BootType::Uki => setup_composefs_uki_boot(
14141477
BootSetupType::Setup((&root_setup, &state, &postfetch)),
14151478
repo,
1416-
&id,
1479+
&provisional_deploy_id,
1480+
&boot_ids,
14171481
entries,
14181482
)?,
14191483
};
14201484

14211485
write_composefs_state(
14221486
&root_setup.physical_root_path,
1423-
&id,
1487+
&deploy_id,
14241488
&crate::spec::ImageReference::from(state.target_imgref.clone()),
14251489
None,
14261490
boot_type,

crates/lib/src/bootc_composefs/digest.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@ use crate::store::ComposefsRepository;
2323
/// Creates a temporary composefs repository for computing digests.
2424
///
2525
/// The `erofs_version` controls which EROFS format the digest is computed for:
26-
/// use `FormatVersion::V1` to get a `composefs.digest.v1=` karg (V1 EROFS, C-tool
27-
/// compatible) or `FormatVersion::V2` for the legacy `composefs=` karg.
26+
/// use `FormatVersion::V1` to get a `composefs.digest=v1-sha256-12:<hex>` karg (V1 EROFS,
27+
/// C-tool compatible) or `FormatVersion::V2` for the legacy `composefs=<hex>` karg.
2828
///
2929
/// Returns the TempDir guard (must be kept alive for the repo to remain valid)
3030
/// and the repository wrapped in Arc.

crates/lib/src/bootc_composefs/gc.rs

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -402,16 +402,20 @@ pub(crate) async fn composefs_gc(
402402
ref_digest,
403403
None,
404404
) {
405-
if let Some(img_ref) = img.image_ref(booted_cfs.repo.erofs_version()) {
406-
if img_ref.to_hex() == *verity {
407-
tracing::info!(
408-
"Deployment {verity} has no manifest_digest in origin; \
409-
found matching manifest {ref_digest} via image_ref"
410-
);
411-
live_manifest_digests.push(ref_digest.clone());
412-
found_manifest = true;
413-
break;
414-
}
405+
// Check both V1 and V2 slots: the deployment verity
406+
// may have been produced under either format.
407+
let img_ref_hex = img
408+
.image_ref_v1()
409+
.or_else(|| img.image_ref_v2())
410+
.map(|id| id.to_hex());
411+
if img_ref_hex.as_deref() == Some(verity.as_str()) {
412+
tracing::info!(
413+
"Deployment {verity} has no manifest_digest in origin; \
414+
found matching manifest {ref_digest} via image_ref"
415+
);
416+
live_manifest_digests.push(ref_digest.clone());
417+
found_manifest = true;
418+
break;
415419
}
416420
}
417421
}

crates/lib/src/bootc_composefs/repo.rs

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -100,11 +100,20 @@ pub(crate) async fn initialize_composefs_repository(
100100

101101
crate::store::ensure_composefs_dir(rootfs_dir)?;
102102

103-
let config = RepositoryConfig::new(composefs::fsverity::Algorithm::SHA512);
104-
let config = if allow_missing_fsverity {
105-
config.set_insecure()
106-
} else {
107-
config
103+
let mut config = {
104+
let c = RepositoryConfig::new(composefs::fsverity::Algorithm::SHA512);
105+
if allow_missing_fsverity {
106+
c.set_insecure()
107+
} else {
108+
c
109+
}
110+
};
111+
// Generate both V1 and V2 EROFS images so a deployment can be booted via
112+
// either the composefs= legacy shorthand (V2) or composefs.digest= (V1/V2)
113+
// karg. This makes both digests available for any boot path.
114+
config.erofs_formats = composefs::erofs::format::FormatConfig {
115+
default: composefs::erofs::format::FormatVersion::V1,
116+
extra: [composefs::erofs::format::FormatVersion::V2].into(),
108117
};
109118
let (repo, _created) =
110119
crate::store::ComposefsRepository::init_path(rootfs_dir, "composefs", config)

0 commit comments

Comments
 (0)