Skip to content

Commit 3b90847

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 2c5fbb5 commit 3b90847

10 files changed

Lines changed: 240 additions & 86 deletions

File tree

crates/initramfs/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -464,7 +464,7 @@ pub fn setup_root(args: Args) -> Result<()> {
464464
};
465465

466466
let composefs = ComposefsCmdline::<Sha512HashValue>::from_cmdline(&cmdline)?
467-
.context("No composefs= or composefs.digest.v1= karg found")?;
467+
.context("No composefs= or composefs.digest= karg found")?;
468468
let image = composefs.digest();
469469
let insecure = composefs.is_insecure();
470470

crates/lib/src/bootc_composefs/boot.rs

Lines changed: 67 additions & 19 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>> {
@@ -815,7 +822,7 @@ fn write_pe_to_esp(
815822

816823
let parsed_cmdline = BootComposefsCmdline::<Sha512HashValue>::from_cmdline(&cmdline)
817824
.context("Parsing composefs=")?
818-
.context("No composefs= or composefs.digest.v1= karg found in UKI cmdline")?;
825+
.context("No composefs= or composefs.digest= karg found in UKI cmdline")?;
819826
let composefs_cmdline = parsed_cmdline.digest().clone();
820827
let missing_verity_allowed_cmdline = parsed_cmdline.is_insecure();
821828

@@ -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+
parsed_cmdline
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

@@ -1071,8 +1077,9 @@ pub(crate) fn setup_composefs_uki_boot(
10711077
setup_type: BootSetupType,
10721078
repo: crate::store::ComposefsRepository,
10731079
id: &Sha512HashValue,
1080+
boot_ids: &[&Sha512HashValue],
10741081
entries: Vec<ComposefsBootEntry<Sha512HashValue>>,
1075-
) -> Result<String> {
1082+
) -> Result<(String, Sha512HashValue)> {
10761083
let (root_path, esp_device, bootloader, missing_fsverity_allowed, uki_addons) = match setup_type
10771084
{
10781085
BootSetupType::Setup((root_setup, state, postfetch)) => {
@@ -1152,7 +1159,8 @@ pub(crate) fn setup_composefs_uki_boot(
11521159
&entry.file,
11531160
utf8_file_path,
11541161
entry.pe_type,
1155-
&id,
1162+
id,
1163+
boot_ids,
11561164
missing_fsverity_allowed,
11571165
esp_mount.dir.path(),
11581166
)?;
@@ -1169,6 +1177,13 @@ pub(crate) fn setup_composefs_uki_boot(
11691177

11701178
let boot_digest = uki_info.boot_digest.clone();
11711179

1180+
// The deploy key for a UKI boot is the composefs digest baked into the UKI
1181+
// cmdline (already validated against `boot_ids` in `write_pe_to_esp`).
1182+
// setup-root opens `state/deploy/<this>` using that same karg, so we must
1183+
// key the deployment off exactly this value -- whether the UKI was sealed
1184+
// with the V2 (default) or V1 EROFS digest.
1185+
let deploy_id = uki_info.composefs_cmdline.clone();
1186+
11721187
match bootloader {
11731188
Bootloader::Grub => {
11741189
write_grub_uki_menuentry(root_path, &setup_type, uki_info.boot_label, id, &esp_device)?
@@ -1179,7 +1194,7 @@ pub(crate) fn setup_composefs_uki_boot(
11791194
Bootloader::None => unreachable!("Checked at install time"),
11801195
};
11811196

1182-
Ok(boot_digest)
1197+
Ok((boot_digest, deploy_id))
11831198
}
11841199

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

1368+
// Open the OCI image to read both stored boot EROFS digests. The UKI may
1369+
// have been sealed with either the V1 or V2 boot image digest, so we need
1370+
// both for verification.
1371+
let oci_img =
1372+
composefs_oci::oci_image::OciImage::open(&*repo, &pull_result.manifest_digest, None)
1373+
.context("Opening OCI image to read boot image refs")?;
1374+
let boot_id_v1 = oci_img.boot_image_ref_v1().cloned();
1375+
let boot_id_v2 = oci_img.boot_image_ref_v2().cloned();
1376+
13531377
// Reconstruct the OCI filesystem to discover boot entries (kernel, initramfs, etc.).
13541378
let fs = composefs_oci::image::create_filesystem(&*repo, &pull_result.config_digest, None)
13551379
.context("Creating composefs filesystem for boot entry discovery")?;
@@ -1402,25 +1426,49 @@ pub(crate) async fn setup_composefs_boot(
14021426
)
14031427
})?;
14041428

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-
)?,
1429+
// The deployment key is the hash that setup-root looks for in
1430+
// state/deploy/<hash>, derived from the composefs karg. The two boot types
1431+
// establish that karg differently:
1432+
//
1433+
// * BLS: bootc writes the karg itself, so we are free to choose the key.
1434+
// Prefer the V2 digest (matching the default EROFS format), falling back
1435+
// to the primary id for legacy repos with no separate V2 boot ref.
1436+
//
1437+
// * UKI: the karg is baked into the UKI at seal time, so the key is
1438+
// whatever digest the UKI carries. `setup_composefs_uki_boot` parses and
1439+
// validates that against the boot refs and returns it, so we override the
1440+
// provisional value below. This keeps us correct whether the UKI was
1441+
// sealed with the V2 (default) or V1 EROFS digest.
1442+
let provisional_deploy_id = boot_id_v2.as_ref().cloned().unwrap_or_else(|| id.clone());
1443+
1444+
// Collect whichever boot image refs exist; the UKI cmdline may carry either.
1445+
let boot_ids_owned: Vec<Sha512HashValue> =
1446+
[boot_id_v1, boot_id_v2].into_iter().flatten().collect();
1447+
let boot_ids: Vec<&Sha512HashValue> = boot_ids_owned.iter().collect();
1448+
1449+
let (boot_digest, deploy_id) = match boot_type {
1450+
BootType::Bls => (
1451+
setup_composefs_bls_boot(
1452+
BootSetupType::Setup((&root_setup, &state, &postfetch)),
1453+
repo,
1454+
&provisional_deploy_id,
1455+
entry,
1456+
mounted_root.dir(),
1457+
)?,
1458+
provisional_deploy_id,
1459+
),
14131460
BootType::Uki => setup_composefs_uki_boot(
14141461
BootSetupType::Setup((&root_setup, &state, &postfetch)),
14151462
repo,
1416-
&id,
1463+
&provisional_deploy_id,
1464+
&boot_ids,
14171465
entries,
14181466
)?,
14191467
};
14201468

14211469
write_composefs_state(
14221470
&root_setup.physical_root_path,
1423-
&id,
1471+
&deploy_id,
14241472
&crate::spec::ImageReference::from(state.target_imgref.clone()),
14251473
None,
14261474
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() {
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: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ pub(crate) async fn initialize_composefs_repository(
9999

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

102-
let config = {
102+
let mut config = {
103103
let c =
104104
composefs::repository::RepositoryConfig::new(composefs::fsverity::Algorithm::SHA512);
105105
if allow_missing_fsverity {
@@ -108,6 +108,13 @@ pub(crate) async fn initialize_composefs_repository(
108108
c
109109
}
110110
};
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(),
117+
};
111118
let (repo, _created) =
112119
crate::store::ComposefsRepository::init_path(rootfs_dir, "composefs", config)
113120
.context("Failed to initialize composefs repository")?;

0 commit comments

Comments
 (0)