From 0888c8f1d5dea446b9868121830d053de621d67c Mon Sep 17 00:00:00 2001 From: Brian Fjeldstad Date: Fri, 28 Aug 2026 20:12:04 +0000 Subject: [PATCH 1/3] esp: copy slot-*.addon.efi verbatim instead of renaming verity-*.addon.efi ACL verity addon templates are renamed to slot-a.addon.efi/slot-b.addon.efi on the image ESP. Copy the selected slot template into the staged UKI addon directory verbatim (no rename to verity.addon.efi), matching its source filename. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- crates/trident/src/engine/boot/uki.rs | 34 +++++++++++++++------------ 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/crates/trident/src/engine/boot/uki.rs b/crates/trident/src/engine/boot/uki.rs index 851835af4..fcfed37a0 100644 --- a/crates/trident/src/engine/boot/uki.rs +++ b/crates/trident/src/engine/boot/uki.rs @@ -534,13 +534,15 @@ pub fn find_previous_uki(esp_dir_path: &Path) -> Result { /// the first-boot addon (see `enforce_firstboot_addon_policy`). const ACL_ADDON_TEMPLATES_DIR: &str = "acl/uki-addons"; -/// Filename of the active verity addon placed in the UKI's `.extra.d/` directory. -const VERITY_ADDON_FILENAME: &str = "verity.addon.efi"; /// After staging the UKI, activate the correct verity addon for the target /// A/B volume. ACL images ship with slot-A active by default and include /// templates for both slots in `acl/uki-addons/` on the ESP image. /// +/// The template is copied verbatim (no rename) into the staged addon +/// directory as `slot-a.addon.efi` or `slot-b.addon.efi`, matching its +/// source filename. +/// /// This is ACL-specific: if no verity addon templates exist on the image /// (i.e. a non-ACL image), this function is a silent no-op. However, if /// templates exist but the selected slot's template is missing, an error @@ -562,8 +564,8 @@ pub fn activate_verity_addon_for_target_volume( } let template_name = match target_volume { - AbVolumeSelection::VolumeA => "verity-a.addon.efi", - AbVolumeSelection::VolumeB => "verity-b.addon.efi", + AbVolumeSelection::VolumeA => "slot-a.addon.efi", + AbVolumeSelection::VolumeB => "slot-b.addon.efi", }; let template_path = template_dir.join(template_name); @@ -588,7 +590,9 @@ pub fn activate_verity_addon_for_target_volume( })?; } - let dest = staging_addon_dir.join(VERITY_ADDON_FILENAME); + // Copied verbatim (no rename) — the slot suffix in the filename is + // preserved in the staged addon directory. + let dest = staging_addon_dir.join(template_name); debug!( "Activating verity addon for {:?}: '{}' → '{}'", target_volume, @@ -1164,8 +1168,8 @@ mod tests { fn setup_image_with_verity_templates(image_esp: &Path) -> (PathBuf, PathBuf) { let template_dir = image_esp.join(ACL_ADDON_TEMPLATES_DIR); fs::create_dir_all(&template_dir).unwrap(); - let a_path = template_dir.join("verity-a.addon.efi"); - let b_path = template_dir.join("verity-b.addon.efi"); + let a_path = template_dir.join("slot-a.addon.efi"); + let b_path = template_dir.join("slot-b.addon.efi"); fs::write(&a_path, b"verity-a-content").unwrap(); fs::write(&b_path, b"verity-b-content").unwrap(); (a_path, b_path) @@ -1194,7 +1198,7 @@ mod tests { ) .unwrap(); - let active = staged_addon_dir.join(VERITY_ADDON_FILENAME); + let active = staged_addon_dir.join("slot-a.addon.efi"); assert!(active.exists()); assert_eq!(fs::read(&active).unwrap(), b"verity-a-content"); } @@ -1221,7 +1225,7 @@ mod tests { ) .unwrap(); - let active = staged_addon_dir.join(VERITY_ADDON_FILENAME); + let active = staged_addon_dir.join("slot-b.addon.efi"); assert!(active.exists()); assert_eq!(fs::read(&active).unwrap(), b"verity-b-content"); } @@ -1257,8 +1261,8 @@ mod tests { let image_esp = tempdir().unwrap(); let template_dir = image_esp.path().join(ACL_ADDON_TEMPLATES_DIR); fs::create_dir_all(&template_dir).unwrap(); - // Only write verity-a, not verity-b - fs::write(template_dir.join("verity-a.addon.efi"), b"a-content").unwrap(); + // Only write slot-a, not slot-b + fs::write(template_dir.join("slot-a.addon.efi"), b"a-content").unwrap(); let mount_point = tempdir().unwrap(); prepare_esp_for_uki(mount_point.path(), Path::new(DEFAULT_ESP_MOUNT_POINT_PATH)).unwrap(); @@ -1275,7 +1279,7 @@ mod tests { result .unwrap_err() .to_string() - .contains("verity-b.addon.efi"), + .contains("slot-b.addon.efi"), "Error should mention the missing template" ); } @@ -1301,9 +1305,9 @@ mod tests { let staged_addon_dir = join_relative(mount_point.path(), DEFAULT_ESP_MOUNT_POINT_PATH) .join(UKI_DIRECTORY) .join(TMP_UKI_ADDON_DIR_NAME); - assert!(staged_addon_dir.join(VERITY_ADDON_FILENAME).exists()); + assert!(staged_addon_dir.join("slot-b.addon.efi").exists()); assert_eq!( - fs::read(staged_addon_dir.join(VERITY_ADDON_FILENAME)).unwrap(), + fs::read(staged_addon_dir.join("slot-b.addon.efi")).unwrap(), b"verity-b-content" ); } @@ -1338,7 +1342,7 @@ mod tests { // Verity addon should be activated assert_eq!( - fs::read(staged_addon_dir.join(VERITY_ADDON_FILENAME)).unwrap(), + fs::read(staged_addon_dir.join("slot-a.addon.efi")).unwrap(), b"verity-a-content" ); // Other addon should be untouched From 2744e0e7d5780ae192d4ea27dc1a4127519b934f Mon Sep 17 00:00:00 2001 From: Brian Fjeldstad Date: Fri, 28 Aug 2026 20:29:26 +0000 Subject: [PATCH 2/3] engine/boot/uki: extract slot addon filenames into constants Introduce SLOT_A_ADDON_FILENAME and SLOT_B_ADDON_FILENAME constants for slot-a.addon.efi/slot-b.addon.efi, replacing string literals in activate_verity_addon_for_target_volume and its tests. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- crates/trident/src/engine/boot/uki.rs | 33 +++++++++++++++++---------- 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/crates/trident/src/engine/boot/uki.rs b/crates/trident/src/engine/boot/uki.rs index fcfed37a0..cab3d8379 100644 --- a/crates/trident/src/engine/boot/uki.rs +++ b/crates/trident/src/engine/boot/uki.rs @@ -530,10 +530,19 @@ pub fn find_previous_uki(esp_dir_path: &Path) -> Result { } /// Path within the image ESP where ACL stores addon templates shared across -/// servicing scenarios: per-slot verity addons (`verity-a`/`verity-b`) and +/// servicing scenarios: per-slot verity addons (`slot-a`/`slot-b`) and /// the first-boot addon (see `enforce_firstboot_addon_policy`). const ACL_ADDON_TEMPLATES_DIR: &str = "acl/uki-addons"; +/// Filename of the verity addon template for slot A, both in +/// `ACL_ADDON_TEMPLATES_DIR` and in the staged UKI addon directory once +/// activated (copied verbatim, never renamed). +const SLOT_A_ADDON_FILENAME: &str = "slot-a.addon.efi"; + +/// Filename of the verity addon template for slot B, both in +/// `ACL_ADDON_TEMPLATES_DIR` and in the staged UKI addon directory once +/// activated (copied verbatim, never renamed). +const SLOT_B_ADDON_FILENAME: &str = "slot-b.addon.efi"; /// After staging the UKI, activate the correct verity addon for the target /// A/B volume. ACL images ship with slot-A active by default and include @@ -564,8 +573,8 @@ pub fn activate_verity_addon_for_target_volume( } let template_name = match target_volume { - AbVolumeSelection::VolumeA => "slot-a.addon.efi", - AbVolumeSelection::VolumeB => "slot-b.addon.efi", + AbVolumeSelection::VolumeA => SLOT_A_ADDON_FILENAME, + AbVolumeSelection::VolumeB => SLOT_B_ADDON_FILENAME, }; let template_path = template_dir.join(template_name); @@ -1168,8 +1177,8 @@ mod tests { fn setup_image_with_verity_templates(image_esp: &Path) -> (PathBuf, PathBuf) { let template_dir = image_esp.join(ACL_ADDON_TEMPLATES_DIR); fs::create_dir_all(&template_dir).unwrap(); - let a_path = template_dir.join("slot-a.addon.efi"); - let b_path = template_dir.join("slot-b.addon.efi"); + let a_path = template_dir.join(SLOT_A_ADDON_FILENAME); + let b_path = template_dir.join(SLOT_B_ADDON_FILENAME); fs::write(&a_path, b"verity-a-content").unwrap(); fs::write(&b_path, b"verity-b-content").unwrap(); (a_path, b_path) @@ -1198,7 +1207,7 @@ mod tests { ) .unwrap(); - let active = staged_addon_dir.join("slot-a.addon.efi"); + let active = staged_addon_dir.join(SLOT_A_ADDON_FILENAME); assert!(active.exists()); assert_eq!(fs::read(&active).unwrap(), b"verity-a-content"); } @@ -1225,7 +1234,7 @@ mod tests { ) .unwrap(); - let active = staged_addon_dir.join("slot-b.addon.efi"); + let active = staged_addon_dir.join(SLOT_B_ADDON_FILENAME); assert!(active.exists()); assert_eq!(fs::read(&active).unwrap(), b"verity-b-content"); } @@ -1262,7 +1271,7 @@ mod tests { let template_dir = image_esp.path().join(ACL_ADDON_TEMPLATES_DIR); fs::create_dir_all(&template_dir).unwrap(); // Only write slot-a, not slot-b - fs::write(template_dir.join("slot-a.addon.efi"), b"a-content").unwrap(); + fs::write(template_dir.join(SLOT_A_ADDON_FILENAME), b"a-content").unwrap(); let mount_point = tempdir().unwrap(); prepare_esp_for_uki(mount_point.path(), Path::new(DEFAULT_ESP_MOUNT_POINT_PATH)).unwrap(); @@ -1279,7 +1288,7 @@ mod tests { result .unwrap_err() .to_string() - .contains("slot-b.addon.efi"), + .contains(SLOT_B_ADDON_FILENAME), "Error should mention the missing template" ); } @@ -1305,9 +1314,9 @@ mod tests { let staged_addon_dir = join_relative(mount_point.path(), DEFAULT_ESP_MOUNT_POINT_PATH) .join(UKI_DIRECTORY) .join(TMP_UKI_ADDON_DIR_NAME); - assert!(staged_addon_dir.join("slot-b.addon.efi").exists()); + assert!(staged_addon_dir.join(SLOT_B_ADDON_FILENAME).exists()); assert_eq!( - fs::read(staged_addon_dir.join("slot-b.addon.efi")).unwrap(), + fs::read(staged_addon_dir.join(SLOT_B_ADDON_FILENAME)).unwrap(), b"verity-b-content" ); } @@ -1342,7 +1351,7 @@ mod tests { // Verity addon should be activated assert_eq!( - fs::read(staged_addon_dir.join("slot-a.addon.efi")).unwrap(), + fs::read(staged_addon_dir.join(SLOT_A_ADDON_FILENAME)).unwrap(), b"verity-a-content" ); // Other addon should be untouched From 477ba3facc71726303d7069a2cf07780beb52dbe Mon Sep 17 00:00:00 2001 From: Brian Fjeldstad Date: Fri, 28 Aug 2026 20:38:46 +0000 Subject: [PATCH 3/3] engine/boot/uki: address Copilot review feedback on PR 759 - Fix ACL_ADDON_TEMPLATES_DIR doc comment: still referenced the old verity-a/verity-b template names instead of slot-a/slot-b. - Fix activate_verity_addon_for_target_volume to remove a stale addon for the other slot from the staged addon dir before activating the target slot. stage_uki_on_esp copies the images live .extra.d/ verbatim, and ACL images ship slot A active by default, so an update targeting slot B could otherwise leave both slot-a.addon.efi and slot-b.addon.efi staged, letting systemd-boot apply both (conflicting) verity cmdlines. - Add regression test covering stale other-slot addon removal. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- crates/trident/src/engine/boot/uki.rs | 66 +++++++++++++++++++++++++-- 1 file changed, 61 insertions(+), 5 deletions(-) diff --git a/crates/trident/src/engine/boot/uki.rs b/crates/trident/src/engine/boot/uki.rs index cab3d8379..8813f10a1 100644 --- a/crates/trident/src/engine/boot/uki.rs +++ b/crates/trident/src/engine/boot/uki.rs @@ -530,8 +530,9 @@ pub fn find_previous_uki(esp_dir_path: &Path) -> Result { } /// Path within the image ESP where ACL stores addon templates shared across -/// servicing scenarios: per-slot verity addons (`slot-a`/`slot-b`) and -/// the first-boot addon (see `enforce_firstboot_addon_policy`). +/// servicing scenarios: per-slot verity addons (`slot-a.addon.efi`/ +/// `slot-b.addon.efi`) and the first-boot addon (see +/// `enforce_firstboot_addon_policy`). const ACL_ADDON_TEMPLATES_DIR: &str = "acl/uki-addons"; /// Filename of the verity addon template for slot A, both in @@ -572,9 +573,9 @@ pub fn activate_verity_addon_for_target_volume( return Ok(()); } - let template_name = match target_volume { - AbVolumeSelection::VolumeA => SLOT_A_ADDON_FILENAME, - AbVolumeSelection::VolumeB => SLOT_B_ADDON_FILENAME, + let (template_name, other_slot_addon_name) = match target_volume { + AbVolumeSelection::VolumeA => (SLOT_A_ADDON_FILENAME, SLOT_B_ADDON_FILENAME), + AbVolumeSelection::VolumeB => (SLOT_B_ADDON_FILENAME, SLOT_A_ADDON_FILENAME), }; let template_path = template_dir.join(template_name); @@ -599,6 +600,21 @@ pub fn activate_verity_addon_for_target_volume( })?; } + // The staged addon dir may already carry the other slot's addon — + // e.g. copied verbatim from the image's live .extra.d/ by + // stage_uki_on_esp, which ships slot A active. Leaving it in place + // would let systemd-boot apply both slots' (conflicting) verity + // cmdlines, so remove it before activating the target slot. + let other_slot_addon_path = staging_addon_dir.join(other_slot_addon_name); + if other_slot_addon_path.exists() { + fs::remove_file(&other_slot_addon_path).with_context(|| { + format!( + "Failed to remove stale verity addon '{}'", + other_slot_addon_path.display() + ) + })?; + } + // Copied verbatim (no rename) — the slot suffix in the filename is // preserved in the staged addon directory. let dest = staging_addon_dir.join(template_name); @@ -1361,6 +1377,46 @@ mod tests { ); } + /// Activating a slot removes a stale addon for the *other* slot already + /// present in the staged dir (e.g. copied verbatim from the image's live + /// .extra.d/, which ships slot A active), so systemd-boot never applies + /// both slots' conflicting verity cmdlines. + #[test] + fn test_activate_verity_addon_removes_stale_other_slot_addon() { + let image_esp = tempdir().unwrap(); + setup_image_with_verity_templates(image_esp.path()); + + let mount_point = tempdir().unwrap(); + prepare_esp_for_uki(mount_point.path(), Path::new(DEFAULT_ESP_MOUNT_POINT_PATH)).unwrap(); + + let staged_addon_dir = join_relative(mount_point.path(), DEFAULT_ESP_MOUNT_POINT_PATH) + .join(UKI_DIRECTORY) + .join(TMP_UKI_ADDON_DIR_NAME); + fs::create_dir_all(&staged_addon_dir).unwrap(); + // Stale slot A addon staged verbatim from the image's live .extra.d/ + fs::write( + staged_addon_dir.join(SLOT_A_ADDON_FILENAME), + b"stale-a-content", + ) + .unwrap(); + + activate_verity_addon_for_target_volume( + image_esp.path(), + mount_point.path(), + Path::new(DEFAULT_ESP_MOUNT_POINT_PATH), + AbVolumeSelection::VolumeB, + ) + .unwrap(); + + // Target slot's addon is activated + assert_eq!( + fs::read(staged_addon_dir.join(SLOT_B_ADDON_FILENAME)).unwrap(), + b"verity-b-content" + ); + // Stale other-slot addon is removed + assert!(!staged_addon_dir.join(SLOT_A_ADDON_FILENAME).exists()); + } + // ── enforce_firstboot_addon_policy tests ──────────────────────────────── /// Clean install: firstboot addon already staged (as if copied verbatim