From 1658ce0bef80c608316684a3c2e6bb83549a72a3 Mon Sep 17 00:00:00 2001 From: Alex Zenla Date: Sat, 3 Oct 2026 20:03:23 -0700 Subject: [PATCH] feat(bls): read the extended boot loader partition in the BLS generator Add an xbootldr option to the BLS generator. When it is enabled, the partition with the XBOOTLDR type on the same disk as Sprout's partition is read as well, and its entries and unified kernel images are sorted with the others. As their files are on that partition, every entry has an entry-root value, which is the device of the partition or nothing for Sprout's own, to use in a path like $entry-root\$chainload. It is off by default, as existing actions would look for the files on the wrong partition. --- crates/boot/src/autoconfigure/bls.rs | 3 + crates/boot/src/generators/bls.rs | 103 +++++++++++++++++++++++---- crates/config/src/generators/bls.rs | 32 +++++++-- 3 files changed, 120 insertions(+), 18 deletions(-) diff --git a/crates/boot/src/autoconfigure/bls.rs b/crates/boot/src/autoconfigure/bls.rs index 25e1240..3ee9de3 100644 --- a/crates/boot/src/autoconfigure/bls.rs +++ b/crates/boot/src/autoconfigure/bls.rs @@ -102,6 +102,9 @@ pub fn scan( }, path: format!("{}\\loader", root), uki_path: None, + // Every filesystem is scanned, including the Extended Boot Loader Partition, which + // gets a generator of its own. + xbootldr: false, pin_names: true, }; diff --git a/crates/boot/src/generators/bls.rs b/crates/boot/src/generators/bls.rs index 2a47ef4..56c493e 100644 --- a/crates/boot/src/generators/bls.rs +++ b/crates/boot/src/generators/bls.rs @@ -66,6 +66,7 @@ fn bootable_entry( name: &str, entry: &BlsEntry, initrds: &[String], + entry_root: &str, ) -> BootableEntry { // Produce a new sprout context for the entry with the extracted values. let mut context = context.fork(); @@ -84,6 +85,9 @@ fn bootable_entry( title_base.clone() }; + // The device that the files of the entry are on, for a path like "$entry-root\\$chainload". + // It is empty for the partition that Sprout was loaded from. + context.set("entry-root", entry_root.to_string()); context.set("title-base", title_base); context.set("title", title_full); context.set("chainload", chainload); @@ -132,6 +136,7 @@ fn generate_type1( context: &Rc, bls: &BlsConfiguration, path: &str, + entry_root: &str, ) -> Result> { let mut entries = Vec::new(); @@ -259,7 +264,7 @@ fn generate_type1( continue; } - let mut boot = bootable_entry(context, bls, &name, &entry, &initrds); + let mut boot = bootable_entry(context, bls, &name, &entry, &initrds, entry_root); boot.set_id_suffix(&extension); // Record where the boot counter lives so a try can be consumed when this entry boots. @@ -287,6 +292,7 @@ fn generate_type2( context: &Rc, bls: &BlsConfiguration, uki_path: &str, + entry_root: &str, entries: &[(BlsEntry, BootableEntry)], ) -> Result> { let resolved = eficore::path::resolve_path(Some(context.root().loaded_image_path()?), uki_path) @@ -356,7 +362,7 @@ fn generate_type2( let mut entry = profile.entry; entry.boot_counter = boot_counter; - let mut boot = bootable_entry(context, bls, &name, &entry, &[]); + let mut boot = bootable_entry(context, bls, &name, &entry, &[], entry_root); boot.set_id_suffix(&extension); if let Some(ref suffix) = suffix { boot.set_id_profile(suffix); @@ -392,18 +398,19 @@ fn generate_type2( Ok(found) } -/// Generates entries from the BLS entries directory and the unified kernel image directory -/// using the specified `bls` configuration and `context`. The BLS conversion is best-effort -/// and will ignore any unsupported entries. -pub fn generate(context: Rc, bls: &BlsConfiguration) -> Result> { - // Stamp the path to the BLS directory. - let path = context.stamp(&bls.path); - - let uki_path = bls.uki_path_for(&path); - +/// Generates the entries of one partition, from the BLS entries directory at `bls_path` and the +/// unified kernel image directory at `uki_path`, if there is one. The files of the entries are on +/// the device `entry_root`. +fn generate_partition( + context: &Rc, + bls: &BlsConfiguration, + bls_path: &str, + uki_path: Option<&str>, + entry_root: &str, +) -> Result> { // A problem reading the Type #1 entries only stops the unified kernel images when there // are none to add, so that either kind of entry can still boot. - let mut entries = match generate_type1(&context, bls, &path) { + let mut entries = match generate_type1(context, bls, bls_path, entry_root) { Ok(entries) => entries, Err(error) if uki_path.is_some() => { warn!("unable to generate bls entries: {:#}", error); @@ -415,8 +422,8 @@ pub fn generate(context: Rc, bls: &BlsConfiguration) -> Result entries.extend(found), Err(error) => warn!( "unable to generate unified kernel image entries: {:#}", @@ -424,6 +431,74 @@ pub fn generate(context: Rc, bls: &BlsConfiguration) -> Result) -> Result> { + let Some(path) = eficore::xbootldr::find_xbootldr(context.root().loaded_image_path()?)? else { + return Ok(None); + }; + let mut root = path + .to_string16(DisplayOnly(false), AllowShortcuts(false)) + .context("unable to convert the xbootldr device path to a string")? + .to_string(); + // Add a trailing forward-slash to the root to ensure the device root is completed. + root.push('/'); + Ok(Some(root)) +} + +/// Generates entries from the BLS entries directory and the unified kernel image directory +/// using the specified `bls` configuration and `context`, and from the Extended Boot Loader +/// Partition if that is enabled. The BLS conversion is best-effort and will ignore any +/// unsupported entries. +pub fn generate(context: Rc, bls: &BlsConfiguration) -> Result> { + // Stamp the path to the BLS directory. + let path = context.stamp(&bls.path); + + let uki_path = bls.uki_path_for(&path); + let mut entries = generate_partition( + &context, + bls, + &path, + uki_path.as_deref(), + BlsConfiguration::root_of(&path), + )?; + + // Add the entries of the Extended Boot Loader Partition, which are sorted together with the + // others. A problem with it should not prevent booting from the entries that are found. + if bls.xbootldr { + match xbootldr_root(&context) { + Ok(Some(root)) => { + let xbootldr_uki = uki_path.as_ref().map(|_| format!("{}\\EFI\\Linux", root)); + match generate_partition( + &context, + bls, + &format!("{}\\loader", root), + xbootldr_uki.as_deref(), + &root, + ) { + Ok(found) => { + for (entry, boot) in found { + // An entry that is on both partitions would be ambiguous. + if entries.iter().any(|(_, other)| other.id() == boot.id()) { + warn!( + "xbootldr entry {} has the same id as another entry, skipping", + boot.id() + ); + } else { + entries.push((entry, boot)); + } + } + } + Err(error) => warn!("unable to generate xbootldr entries: {:#}", error), + } + } + Ok(None) => info!("no xbootldr partition was found"), + Err(error) => warn!("unable to find the xbootldr partition: {:#}", error), + } + } // Sort all the entries according to the BLS sort system. entries.sort_by(sort_entries); diff --git a/crates/config/src/generators/bls.rs b/crates/config/src/generators/bls.rs index f69f709..be5d665 100644 --- a/crates/config/src/generators/bls.rs +++ b/crates/config/src/generators/bls.rs @@ -26,6 +26,12 @@ pub struct BlsConfiguration { /// with the same names, so they should not both scan it. #[serde(default, rename = "uki-path")] pub uki_path: Option, + /// Whether to also read the Extended Boot Loader Partition, which is the partition with the + /// XBOOTLDR type on the same disk as the partition Sprout was loaded from. Its entries are + /// sorted with the others, and their paths are on that partition, so an action has to use + /// the `$entry-root` value to find them, such as `$entry-root\\$chainload`. + #[serde(default)] + pub xbootldr: bool, /// Whether generated entries keep the name of the BLS entry file as-is. /// When enabled, the generator name is not prepended, so the entry names match /// the BLS entry ids used by tools like `bootctl set-default`. Variants still append @@ -38,6 +44,12 @@ pub struct BlsConfiguration { } impl BlsConfiguration { + /// The device of the stamped BLS path `bls_path`, which is everything up to the last slash, + /// and which is empty for a path on the partition Sprout was loaded from. + pub fn root_of(bls_path: &str) -> &str { + bls_path.rfind('/').map_or("", |index| &bls_path[..=index]) + } + /// The directory of unified kernel images for the stamped BLS path `bls_path`, /// or None if unified kernel images are disabled. pub fn uki_path_for(&self, bls_path: &str) -> Option { @@ -45,10 +57,7 @@ impl BlsConfiguration { Some("") => None, Some(path) => Some(path.to_string()), // Keep the device of the BLS path, which is everything up to the last slash. - None => { - let device = bls_path.rfind('/').map_or("", |index| &bls_path[..=index]); - Some(format!("{}{}", device, BLS_UKI_DIRECTORY)) - } + None => Some(format!("{}{}", Self::root_of(bls_path), BLS_UKI_DIRECTORY)), } } } @@ -60,6 +69,7 @@ impl Default for BlsConfiguration { entry: Default::default(), path: default_bls_path(), uki_path: None, + xbootldr: false, pin_names: default_pin_names(), } } @@ -77,6 +87,20 @@ fn default_bls_path() -> String { mod tests { use super::*; + #[test] + fn root_is_the_device_of_the_bls_path() { + assert_eq!(BlsConfiguration::root_of("\\loader"), ""); + assert_eq!( + BlsConfiguration::root_of("PciRoot(0x0)/HD(2,GPT,abc)/\\loader"), + "PciRoot(0x0)/HD(2,GPT,abc)/" + ); + } + + #[test] + fn xbootldr_is_off_by_default() { + assert!(!BlsConfiguration::default().xbootldr); + } + #[test] fn uki_path_is_next_to_the_default_bls_path() { let bls = BlsConfiguration::default();