feat(bls): one entry for each profile of a unified kernel image

A unified kernel image can have several profiles, each started by a
.profile section and made of the sections after it on top of the base.
Every profile is an entry. The first has the id of the file, and the
others add @ and the identifier or the number of the profile, with a
title that names the profile and the profile number as the load options.
The profiles share the boot counter of the file, sort in the order they
are in the image, and a profile after the first is not picked as the
default entry when no default was asked for.

The PE reader keeps every section in order, so repeated names and the
sections that a profile removes can be told apart, and reads images with
more than 96 sections.
This commit is contained in:
alex authored and Alex Zenla committed 2026-10-03 20:23:57 -07:00
1 parent ac49899294
commit 50f62f5bb6
7 files changed
+628 -86

No files matched your search

+67 -39
View File
@@ -9,7 +9,9 @@ use alloc::{
};
use anyhow::{Context, Result};
use core::{cmp::Ordering, str::FromStr};
use edera_sprout_bls::{BlsEntry, BootCounter, is_reserved_entry_name, sort_bls, strip_extension};
use edera_sprout_bls::{
BlsEntry, BootCounter, is_reserved_entry_name, profile_id_suffix, sort_bls, strip_extension,
};
use edera_sprout_config::generators::bls::BlsConfiguration;
use log::{info, warn};
use uefi::{
@@ -54,7 +56,7 @@ fn quirk_initrd_remove_tuned(paths: Vec<String>) -> Vec<String> {
fn sort_entries(a: &(BlsEntry, BootableEntry), b: &(BlsEntry, BootableEntry)) -> Ordering {
let (a_bls, a_boot) = a;
let (b_bls, b_boot) = b;
sort_bls(a_bls, a_boot.name(), b_bls, b_boot.name())
sort_bls(a_bls, a_boot.sort_name(), b_bls, b_boot.sort_name())
}
/// Produces the bootable entry for the BLS `entry` with the id `name` and the `initrds`.
@@ -297,8 +299,7 @@ fn generate_type2(
let mut found: Vec<(BlsEntry, BootableEntry)> = Vec::new();
for image in uki::scan(resolved.filesystem_handle, &directory_name)? {
// Only unified kernel images are entries. Images built for another architecture, and
// other EFI programs that have no os-release section, are skipped.
// Only unified kernel images that are built for this machine are entries.
if image.machine != PE_MACHINE {
info!(
"skipping {} as it is built for another architecture",
@@ -306,13 +307,6 @@ fn generate_type2(
);
continue;
}
if !image.has_osrel {
info!(
"skipping {} as it has no .osrel section, so it is not a unified kernel image",
image.file_name
);
continue;
}
// The id is the file name without the extension and the boot counter.
let file_name = image.file_name;
@@ -329,37 +323,71 @@ fn generate_type2(
}
let id = id.to_string();
let mut entry = image.entry;
entry.boot_counter = boot_counter;
// Every profile of the image is an entry. They share the file, and so the boot counter.
let mut suffixes: Vec<String> = Vec::new();
for profile in image.profiles {
// A profile without an os-release section is not a unified kernel image, such as
// other EFI programs.
if !profile.has_osrel {
info!(
"skipping {} as it has no .osrel section, so it is not a unified kernel image",
file_name
);
continue;
}
let mut boot = bootable_entry(context, bls, &id, &entry, &[]);
boot.set_id_suffix(&extension);
// The first profile has the id of the file, and the others name their profile.
// A profile that repeats the identifier of an earlier one is told apart by its number.
let index = profile.index.unwrap_or(0);
let mut suffix = profile_id_suffix(index, &profile.info);
if let Some(ref name) = suffix
&& suffixes.contains(name)
{
suffix = Some(index.to_string());
}
if let Some(ref name) = suffix {
suffixes.push(name.clone());
}
let name = match suffix {
Some(ref suffix) => format!("{}@{}", id, suffix),
None => id.clone(),
};
// An image with the same id as another entry, such as a leftover copy with another boot
// counter, would be ambiguous. Type 1 and type 2 entries have different ids.
if entries
.iter()
.chain(found.iter())
.any(|(_, other)| other.id() == boot.id())
{
warn!(
"unified kernel image {} has the same id as another entry, skipping",
file_name
);
continue;
let mut entry = profile.entry;
entry.boot_counter = boot_counter;
let mut boot = bootable_entry(context, bls, &name, &entry, &[]);
boot.set_id_suffix(&extension);
if let Some(ref suffix) = suffix {
boot.set_id_profile(suffix);
}
// An image with the same id as another entry, such as a leftover copy with another
// boot counter, would be ambiguous. Type 1 and type 2 entries have different ids.
if entries
.iter()
.chain(found.iter())
.any(|(_, other)| other.id() == boot.id())
{
warn!(
"unified kernel image {} has the same id as another entry, skipping",
file_name
);
continue;
}
if let Some(counter) = boot_counter {
boot.set_boot_counter(BootCounterTarget {
counter,
filesystem: resolved.filesystem_handle,
directory: PathBuf::from(directory.clone()),
id: id.clone(),
file_name: file_name.clone(),
extension: extension.clone(),
});
}
found.push((entry, boot));
}
if let Some(counter) = boot_counter {
boot.set_boot_counter(BootCounterTarget {
counter,
filesystem: resolved.filesystem_handle,
directory: PathBuf::from(directory.clone()),
id,
file_name,
extension,
});
}
found.push((entry, boot));
}
Ok(found)
}
+25 -6
View File
@@ -4,7 +4,9 @@ use alloc::{
vec::Vec,
};
use anyhow::{Context, Result, anyhow, bail};
use edera_sprout_bls::{BlsEntry, PeImage, ReadAt, UKI_SECTIONS, read_pe, strip_extension};
use edera_sprout_bls::{
BlsEntry, PeImage, ProfileInfo, ReadAt, UKI_SECTIONS, read_pe, strip_extension, uki_profiles,
};
use log::warn;
use uefi::{
CString16, Handle, Status,
@@ -18,9 +20,19 @@ pub struct UkiFile {
pub file_name: String,
/// The machine type of the image.
pub machine: u16,
/// Whether the image has an `.osrel` section, which unified kernel images always have.
/// The profiles of the image. An image without profiles has one.
pub profiles: Vec<UkiProfileEntry>,
}
/// One profile of a unified kernel image.
pub struct UkiProfileEntry {
/// The number of the profile, or None if the image has no profiles.
pub index: Option<u32>,
/// The metadata of the profile.
pub info: ProfileInfo,
/// Whether the profile has an `.osrel` section, which unified kernel images always have.
pub has_osrel: bool,
/// The entry made from the sections of the image.
/// The entry made from the sections of the profile.
pub entry: BlsEntry,
}
@@ -94,9 +106,16 @@ pub fn scan(filesystem: Handle, directory: &str) -> Result<Vec<UkiFile>> {
Ok(image) => found.push(UkiFile {
file_name,
machine: image.machine,
has_osrel: image.sections.contains_key(".osrel"),
// The sections are dropped here, so only the small entry is kept per image.
entry: BlsEntry::from_uki(&image.sections, &path),
// The sections are dropped here, so only the small entries are kept per image.
profiles: uki_profiles(&image.sections)
.into_iter()
.map(|profile| UkiProfileEntry {
index: profile.index,
has_osrel: profile.sections.contains_key(".osrel"),
entry: BlsEntry::from_uki_profile(&profile, &path),
info: profile.info,
})
.collect(),
}),
Err(error) => warn!("unable to read unified kernel image {}: {:#}", path, error),
}