@@ -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,
0 commit comments