mirror of
https://github.com/edera-dev/sprout.git
synced 2026-11-04 18:42:21 +00:00
A value that refers to itself, such as a BLS entry with "options $options", never stops changing when the context is finalized. After the iteration limit, finalizing failed and every entry was lost, although the BLS generator already skips broken entries. Skip just that entry with a warning. A value that refers to itself twice doubles in length every iteration, so it could use up all memory before the limit was reached. Fail once a value grows past 64 KiB.
502 lines
19 KiB
Rust
502 lines
19 KiB
Rust
#![doc = include_str!("../README.md")]
|
|
#![no_std]
|
|
#![no_main]
|
|
extern crate alloc;
|
|
|
|
use crate::{
|
|
context::{RootContext, SproutContext},
|
|
entries::BootableEntry,
|
|
options::SproutOptions,
|
|
phases::phase,
|
|
};
|
|
use alloc::{collections::BTreeMap, format, string::ToString, vec::Vec};
|
|
use anyhow::{Context, Result, bail};
|
|
use core::{ops::Deref, time::Duration};
|
|
use edera_sprout_bls::compare_versions;
|
|
use edera_sprout_config::RootConfiguration;
|
|
use eficore::{
|
|
bootloader_interface::{BootloaderInterface, BootloaderInterfaceTimeout},
|
|
partition::PartitionGuidForm,
|
|
platform::{timer::PlatformTimer, tpm::PlatformTpm},
|
|
secure::SecureBoot,
|
|
setup,
|
|
};
|
|
use log::{error, info, warn};
|
|
use uefi::{entry, proto::device_path::LoadedImageDevicePath};
|
|
use uefi_raw::Status;
|
|
|
|
/// actions: Code that can be configured and executed by Sprout.
|
|
pub mod actions;
|
|
|
|
/// autoconfigure: Autoconfigure Sprout based on the detected environment.
|
|
pub mod autoconfigure;
|
|
|
|
/// config: Sprout configuration mechanism.
|
|
pub mod config;
|
|
|
|
/// context: Stored values that can be cheaply forked and cloned.
|
|
pub mod context;
|
|
|
|
/// drivers: EFI drivers to load and provide extra functionality.
|
|
pub mod drivers;
|
|
|
|
/// entries: Boot menu entries that have a title and can execute actions.
|
|
pub mod entries;
|
|
|
|
/// extractors: Runtime code that can extract values into the Sprout context.
|
|
pub mod extractors;
|
|
|
|
/// generators: Runtime code that can generate entries with specific values.
|
|
pub mod generators;
|
|
|
|
/// menu: Display a boot menu to select an entry to boot.
|
|
pub mod menu;
|
|
|
|
/// options: Parse the options of the Sprout executable.
|
|
pub mod options;
|
|
|
|
/// phases: Hooks into specific parts of the boot process.
|
|
pub mod phases;
|
|
|
|
/// sbat: Secure Boot Attestation section.
|
|
pub mod sbat;
|
|
|
|
/// The delay to wait for when an error occurs in Sprout.
|
|
const DELAY_ON_ERROR: Duration = Duration::from_secs(10);
|
|
|
|
/// Unwraps the `result` of a bootloader interface operation, logging the error if one occurs.
|
|
/// The bootloader interface only exchanges information with the operating system, so a
|
|
/// firmware variable that can't be read or written should not prevent booting.
|
|
/// If an error occurs, the default value is provided instead.
|
|
fn advisory<T: Default>(result: Result<T>) -> T {
|
|
result.unwrap_or_else(|error| {
|
|
warn!("{:#}", error);
|
|
T::default()
|
|
})
|
|
}
|
|
|
|
/// Run Sprout, returning an error if one occurs.
|
|
fn run() -> Result<()> {
|
|
// For safety reasons, we will note that Secure Boot is in beta on Sprout.
|
|
if SecureBoot::enabled().context("unable to determine Secure Boot status")? {
|
|
warn!("Sprout Secure Boot is in beta. Some functionality may not work as expected.");
|
|
}
|
|
|
|
// Start the platform timer.
|
|
let timer = PlatformTimer::start();
|
|
|
|
// Mark the initialization of Sprout in the bootloader interface.
|
|
advisory(
|
|
BootloaderInterface::mark_init(&timer)
|
|
.context("unable to mark initialization in bootloader interface"),
|
|
);
|
|
|
|
// Tell the bootloader interface what firmware we are running on.
|
|
advisory(
|
|
BootloaderInterface::set_firmware_info()
|
|
.context("unable to set firmware info in bootloader interface"),
|
|
);
|
|
|
|
// Tell the bootloader interface what loader is being used.
|
|
advisory(
|
|
BootloaderInterface::set_loader_info()
|
|
.context("unable to set loader info in bootloader interface"),
|
|
);
|
|
|
|
// Acquire the number of active PCR banks on the TPM.
|
|
// If no TPM is available, this will return zero.
|
|
// This is only reported to the bootloader interface, so a TPM that fails to report its
|
|
// PCR banks is treated as having none, rather than preventing boot.
|
|
let active_pcr_banks = PlatformTpm::active_pcr_banks().unwrap_or_else(|error| {
|
|
warn!("unable to determine the active TPM PCR banks: {}", error);
|
|
0
|
|
});
|
|
// Tell the bootloader interface what the number of active PCR banks is.
|
|
advisory(
|
|
BootloaderInterface::set_tpm2_active_pcr_banks(active_pcr_banks)
|
|
.context("unable to set tpm2 active PCR banks in bootloader interface"),
|
|
);
|
|
|
|
// Parse the options to the sprout executable.
|
|
let options = SproutOptions::parse().context("unable to parse options")?;
|
|
|
|
// If --autoconfigure is specified, we use a stub configuration.
|
|
let mut config = if options.autoconfigure {
|
|
info!("autoconfiguration enabled, configuration file will be ignored");
|
|
RootConfiguration::default()
|
|
} else {
|
|
// Load the configuration of sprout.
|
|
// At this point, the configuration has been validated and the specified
|
|
// version is checked to ensure compatibility.
|
|
config::loader::load(&options)?
|
|
};
|
|
|
|
// Grab the sprout.efi loaded image path.
|
|
// This is done in a block to ensure the release of the LoadedImageDevicePath protocol.
|
|
let loaded_image_path = {
|
|
let current_image_device_path_protocol = uefi::boot::open_protocol_exclusive::<
|
|
LoadedImageDevicePath,
|
|
>(uefi::boot::image_handle())
|
|
.context("unable to get loaded image device path")?;
|
|
current_image_device_path_protocol.deref().to_boxed()
|
|
};
|
|
|
|
// Grab the partition GUID of the ESP that sprout was loaded from.
|
|
// This is only reported to the bootloader interface.
|
|
let loaded_image_partition_guid = advisory(
|
|
eficore::partition::partition_guid(&loaded_image_path, PartitionGuidForm::Partition)
|
|
.context("unable to retrieve loaded image partition guid"),
|
|
);
|
|
|
|
// Set the partition GUID of the ESP that sprout was loaded from in the bootloader interface.
|
|
if let Some(loaded_image_partition_guid) = loaded_image_partition_guid {
|
|
// Tell the system about the partition GUID.
|
|
advisory(
|
|
BootloaderInterface::set_partition_guid(&loaded_image_partition_guid)
|
|
.context("unable to set partition guid in bootloader interface"),
|
|
);
|
|
}
|
|
|
|
// Tell the bootloader interface what the loaded image path is.
|
|
advisory(
|
|
BootloaderInterface::set_loader_path(&loaded_image_path)
|
|
.context("unable to set loader path in bootloader interface"),
|
|
);
|
|
|
|
// Create the root context.
|
|
let mut root = RootContext::new(loaded_image_path, timer, options);
|
|
|
|
// Insert the configuration actions into the root context.
|
|
root.actions_mut().extend(config.actions.clone());
|
|
|
|
// Create a new sprout context with the root context.
|
|
let mut context = SproutContext::new(root);
|
|
|
|
// Insert the configuration values into the sprout context.
|
|
context.insert(&config.values);
|
|
|
|
// Freeze the sprout context so it can be shared and cheaply cloned.
|
|
let context = context.freeze();
|
|
|
|
// Execute the early phase.
|
|
phase(context.clone(), &config.phases.early).context("unable to execute early phase")?;
|
|
|
|
// Load all configured drivers.
|
|
drivers::load(context.clone(), &config.drivers).context("unable to load drivers")?;
|
|
|
|
// If --autoconfigure is specified or the loaded configuration has autoconfigure enabled,
|
|
// trigger the autoconfiguration mechanism.
|
|
if context.root().options().autoconfigure || config.options.autoconfigure {
|
|
autoconfigure::autoconfigure(&mut config).context("unable to autoconfigure")?;
|
|
}
|
|
|
|
// Unload the context so that it can be modified.
|
|
let Some(mut context) = context.unload() else {
|
|
bail!("context safety violation while trying to unload context");
|
|
};
|
|
|
|
// Perform root context modification in a block to release the modification when complete.
|
|
{
|
|
// Modify the root context to include the autoconfigured actions.
|
|
let Some(root) = context.root_mut() else {
|
|
bail!("context safety violation while trying to modify root context");
|
|
};
|
|
|
|
// Extend the root context with the autoconfigured actions.
|
|
root.actions_mut().extend(config.actions);
|
|
|
|
// Insert any modified root values.
|
|
context.insert(&config.values);
|
|
}
|
|
|
|
// Refreeze the context to ensure that further operations can share the context.
|
|
let context = context.freeze();
|
|
|
|
// Run all the extractors declared in the configuration.
|
|
let mut extracted = BTreeMap::new();
|
|
for (name, extractor) in &config.extractors {
|
|
let value = extractors::extract(context.clone(), extractor)
|
|
.context(format!("unable to extract value {}", name))?;
|
|
info!("extracted value {}: {}", name, value);
|
|
extracted.insert(name.clone(), value);
|
|
}
|
|
let mut context = context.fork();
|
|
// Insert the extracted values into the sprout context.
|
|
context.insert(&extracted);
|
|
let context = context.freeze();
|
|
|
|
// Execute the startup phase.
|
|
phase(context.clone(), &config.phases.startup).context("unable to execute startup phase")?;
|
|
|
|
let mut entries = Vec::new();
|
|
|
|
// Insert all the static entries from the configuration into the entry list.
|
|
for (name, entry) in config.entries {
|
|
// Associate the main context with the static entry.
|
|
entries.push(BootableEntry::new(
|
|
name,
|
|
entry.title.clone(),
|
|
context.clone(),
|
|
entry,
|
|
));
|
|
}
|
|
|
|
// Run all the generators declared in the configuration.
|
|
for (name, generator) in config.generators {
|
|
let context = context.fork().freeze();
|
|
|
|
// We will prefix all entries with [name]-, provided the name is not pinned.
|
|
let prefix = format!("{}-", name);
|
|
|
|
// Add all the entries generated by the generator to the entry list.
|
|
// The generator specifies the context associated with the entry.
|
|
for mut entry in generators::generate(context.clone(), &generator)? {
|
|
// If the entry name is not pinned, prepend the name prefix.
|
|
if !entry.is_pin_name() {
|
|
entry.prepend_name_prefix(&prefix);
|
|
}
|
|
|
|
entries.push(entry);
|
|
}
|
|
}
|
|
|
|
// Entries whose context can't be finalized are skipped, as one broken entry
|
|
// should not prevent booting any of the other entries.
|
|
entries.retain_mut(|entry| {
|
|
let mut context = entry.context().fork();
|
|
// Insert the values from the entry configuration into the
|
|
// sprout context to use with the entry itself.
|
|
context.insert(&entry.declaration().values);
|
|
let context = match context.finalize() {
|
|
Ok(context) => context.freeze(),
|
|
Err(error) => {
|
|
warn!(
|
|
"skipping entry {}, unable to finalize context: {:#}",
|
|
entry.name(),
|
|
error
|
|
);
|
|
return false;
|
|
}
|
|
};
|
|
// Provide the new context to the bootable entry.
|
|
entry.swap_context(context);
|
|
// Restamp the title and sort key with any values.
|
|
entry.restamp_title();
|
|
entry.restamp_sort_key();
|
|
|
|
// Mark this entry as the default entry if it is declared as such.
|
|
if let Some(ref default_entry) = config.options.default_entry {
|
|
// If the entry matches the default entry, mark it as the default entry.
|
|
if entry.is_match(default_entry) {
|
|
entry.mark_default();
|
|
}
|
|
}
|
|
|
|
true
|
|
});
|
|
|
|
// Sort the entries by their sort key, finalizing the order to show entries. This happens
|
|
// in reverse order so that entries that would come last show up first in the menu.
|
|
entries.sort_by(|a, b| compare_versions(a.sort_key(), b.sort_key()).reverse());
|
|
|
|
// Tell the bootloader interface what entries are available.
|
|
advisory(
|
|
BootloaderInterface::set_entries(entries.iter().map(|entry| entry.name()))
|
|
.context("unable to set entries in bootloader interface"),
|
|
);
|
|
|
|
// Acquire the timeout setting from the bootloader interface.
|
|
let bootloader_interface_timeout = advisory(
|
|
BootloaderInterface::get_timeout().context("unable to get bootloader interface timeout"),
|
|
);
|
|
|
|
// Acquire the default entry from the bootloader interface.
|
|
let bootloader_interface_default_entry = advisory(
|
|
BootloaderInterface::get_default_entry()
|
|
.context("unable to get bootloader interface default entry"),
|
|
);
|
|
|
|
// Acquire the oneshot entry from the bootloader interface.
|
|
let bootloader_interface_oneshot_entry = advisory(
|
|
BootloaderInterface::get_oneshot_entry()
|
|
.context("unable to get bootloader interface oneshot entry"),
|
|
);
|
|
|
|
// If --boot is specified, boot that entry immediately.
|
|
let mut force_boot_entry = context.root().options().boot.clone();
|
|
// If --force-menu is specified, show the boot menu regardless of the value of --boot.
|
|
let mut force_boot_menu = context.root().options().force_menu;
|
|
|
|
// Determine the menu timeout in seconds based on the options or configuration.
|
|
// We prefer the options over the configuration to allow for overriding.
|
|
let mut menu_timeout = context
|
|
.root()
|
|
.options()
|
|
.menu_timeout
|
|
.unwrap_or(config.options.menu_timeout);
|
|
|
|
// Whether the boot menu should wait for the user, instead of counting down.
|
|
let mut wait_for_user = false;
|
|
|
|
// Apply bootloader interface timeout settings.
|
|
match bootloader_interface_timeout {
|
|
BootloaderInterfaceTimeout::MenuForce => {
|
|
// Force the boot menu, and wait for the user to choose an entry.
|
|
force_boot_menu = true;
|
|
wait_for_user = true;
|
|
}
|
|
|
|
BootloaderInterfaceTimeout::MenuForceTimeout(timeout) => {
|
|
// Force the boot menu with the specified timeout.
|
|
force_boot_menu = true;
|
|
menu_timeout = timeout;
|
|
}
|
|
|
|
BootloaderInterfaceTimeout::MenuHidden | BootloaderInterfaceTimeout::MenuDisabled => {
|
|
// Hide the boot menu by setting the timeout to zero.
|
|
menu_timeout = 0;
|
|
}
|
|
|
|
BootloaderInterfaceTimeout::Timeout(timeout) => {
|
|
// Configure the timeout to the specified value.
|
|
menu_timeout = timeout;
|
|
}
|
|
|
|
BootloaderInterfaceTimeout::Unspecified => {
|
|
// Do nothing.
|
|
}
|
|
}
|
|
|
|
// Apply bootloader interface default entry settings.
|
|
if let Some(ref bootloader_interface_default_entry) = bootloader_interface_default_entry {
|
|
if entries
|
|
.iter()
|
|
.any(|entry| entry.is_match(bootloader_interface_default_entry))
|
|
{
|
|
// Iterate over all the entries and mark the default entry as the one specified.
|
|
for entry in &mut entries {
|
|
// Mark the entry as the default entry if it matches the specified entry.
|
|
// If the entry does not match the specified entry, unmark it as the default entry.
|
|
if entry.is_match(bootloader_interface_default_entry) {
|
|
entry.mark_default();
|
|
} else {
|
|
entry.unmark_default();
|
|
}
|
|
}
|
|
} else {
|
|
// If no entry matches, such as when the entry was removed, the setting is ignored
|
|
// so that the configured default entry is still used.
|
|
warn!(
|
|
"ignoring bootloader interface default entry '{}': no matching entry",
|
|
bootloader_interface_default_entry
|
|
);
|
|
}
|
|
}
|
|
|
|
// Apply bootloader interface oneshot entry settings.
|
|
// If set, we will force booting the oneshot entry.
|
|
if let Some(ref bootloader_interface_oneshot_entry) = bootloader_interface_oneshot_entry {
|
|
force_boot_entry = Some(bootloader_interface_oneshot_entry.clone());
|
|
}
|
|
|
|
// If no entries were the default, pick the first entry as the default entry.
|
|
if entries.iter().all(|entry| !entry.is_default())
|
|
&& let Some(entry) = entries.first_mut()
|
|
{
|
|
entry.mark_default();
|
|
}
|
|
|
|
// Convert the menu timeout to a duration.
|
|
// A zero timeout boots without showing the menu, so a forced menu with a zero timeout
|
|
// waits for the user instead.
|
|
let menu_timeout = if wait_for_user || (force_boot_menu && menu_timeout == 0) {
|
|
None
|
|
} else {
|
|
Some(Duration::from_secs(menu_timeout))
|
|
};
|
|
|
|
// Determine the menu style based on the options or configuration.
|
|
// We prefer the options over the configuration to allow for overriding.
|
|
let menu_style = context
|
|
.root()
|
|
.options()
|
|
.menu_style
|
|
.unwrap_or(config.options.menu_style);
|
|
|
|
// Find the forced boot entry, unless the boot menu is forced.
|
|
// If the forced boot entry can't be found, such as when it was removed,
|
|
// the boot menu is used instead.
|
|
let forced_entry = force_boot_entry
|
|
.as_ref()
|
|
.filter(|_| !force_boot_menu)
|
|
.and_then(|force_boot_entry| {
|
|
let entry = BootableEntry::find(force_boot_entry, entries.iter());
|
|
if entry.is_none() {
|
|
warn!(
|
|
"unable to find entry '{}', using the boot menu",
|
|
force_boot_entry
|
|
);
|
|
}
|
|
entry
|
|
});
|
|
|
|
// Use the forced boot entry if possible, otherwise pick an entry using a boot menu.
|
|
let entry = match forced_entry {
|
|
Some(entry) => entry,
|
|
// Delegate to the menu to select an entry to boot.
|
|
None => menu::select(&timer, menu_timeout, menu_style, &entries)
|
|
.context("unable to select entry via boot menu")?,
|
|
};
|
|
|
|
// Tell the bootloader interface what the selected entry is.
|
|
advisory(
|
|
BootloaderInterface::set_selected_entry(entry.name().to_string())
|
|
.context("unable to set selected entry in bootloader interface"),
|
|
);
|
|
|
|
// Execute the late phase, now that the entry is chosen but before its actions are executed.
|
|
phase(context.clone(), &config.phases.late).context("unable to execute late phase")?;
|
|
|
|
// Execute all the actions for the selected entry.
|
|
for action in &entry.declaration().actions {
|
|
let action = entry.context().stamp(action);
|
|
actions::execute(entry.context().clone(), &action)
|
|
.context(format!("unable to execute action '{}'", action))?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// The main entrypoint of sprout.
|
|
/// It is possible this function will not return if actions that are executed
|
|
/// exit boot services or do not return control to sprout.
|
|
#[entry]
|
|
fn efi_main() -> Status {
|
|
// Initialize the basic UEFI environment.
|
|
// If initialization fails, we will return ABORTED.
|
|
// NOTE: This function will also initialize the logger.
|
|
// The logger will panic if it is unable to initialize.
|
|
// It is guaranteed that if this returns, the logger is initialized.
|
|
if let Err(error) = setup::init() {
|
|
error!("unable to initialize environment: {}", error);
|
|
return Status::ABORTED;
|
|
}
|
|
|
|
// Run Sprout, then handle the error.
|
|
let result = run();
|
|
if let Err(ref error) = result {
|
|
// Print an error trace.
|
|
error!("sprout encountered an error: {}", error);
|
|
for (index, stack) in error.chain().enumerate() {
|
|
error!("[{}]: {}", index, stack);
|
|
}
|
|
// Sleep to allow the user to read the error.
|
|
uefi::boot::stall(DELAY_ON_ERROR);
|
|
return Status::ABORTED;
|
|
}
|
|
|
|
// Sprout doesn't necessarily guarantee anything was booted.
|
|
// If we reach here, we will exit back to whoever called us.
|
|
Status::SUCCESS
|
|
}
|