diff --git a/crates/boot/src/entries.rs b/crates/boot/src/entries.rs index a6e106c..e6c1284 100644 --- a/crates/boot/src/entries.rs +++ b/crates/boot/src/entries.rs @@ -1,5 +1,6 @@ use crate::boot_counter::BootCounterTarget; use crate::context::SproutContext; +use alloc::format; use alloc::rc::Rc; use alloc::string::{String, ToString}; use edera_sprout_config::entries::EntryDeclaration; @@ -132,6 +133,13 @@ impl BootableEntry { glob_match(needle, &self.name) || glob_match(needle, &self.title) } + /// Determine if this entry matches `needle` by comparing to the id of the entry, which is + /// the name with or without the `.conf` suffix. This is how loader.conf selects entries. + /// The `needle` is a glob pattern, where `*` matches any sequence of characters. + pub fn is_match_id(&self, needle: &str) -> bool { + glob_match(needle, &self.name) || glob_match(needle, &format!("{}.conf", self.name)) + } + /// Create a variant of this entry, with the name suffixed by `suffix` and using `context`. /// The actions of the entry are stamped with `context`, the same as the list generator. /// Variants of the same entry keep the sort key of this entry, so they stay grouped diff --git a/crates/boot/src/main.rs b/crates/boot/src/main.rs index 1c3b6fb..cb70b18 100644 --- a/crates/boot/src/main.rs +++ b/crates/boot/src/main.rs @@ -12,13 +12,13 @@ use crate::{ use alloc::{ collections::{BTreeMap, BTreeSet}, format, - string::ToString, + string::{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 edera_sprout_bls::{LoaderConf, LoaderTimeout, compare_versions}; +use edera_sprout_config::{DEFAULT_MENU_TIMEOUT_SECONDS, RootConfiguration}; use eficore::{ bootloader_interface::{BootloaderInterface, BootloaderInterfaceTimeout}, partition::PartitionGuidForm, @@ -83,6 +83,31 @@ fn advisory(result: Result) -> T { }) } +/// Loads the loader.conf of the partition Sprout was loaded from. +/// A missing loader.conf has no settings. +fn load_loader_conf(context: &SproutContext) -> Result { + let Ok(content) = eficore::path::read_file_contents( + Some(context.root().loaded_image_path()?), + "\\loader\\loader.conf", + ) else { + return Ok(LoaderConf::default()); + }; + + // Measure the loader.conf into the TPM, as it changes how Sprout boots. + PlatformTpm::log_event( + PlatformTpm::PCR_BOOT_LOADER_CONFIG, + &content, + "sprout: loader.conf", + ) + .context("unable to measure the loader.conf file into the TPM")?; + + let Ok(content) = String::from_utf8(content) else { + warn!("ignoring loader.conf that is not valid UTF-8"); + return Ok(LoaderConf::default()); + }; + Ok(LoaderConf::parse(&content)) +} + /// 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. @@ -295,14 +320,6 @@ fn run() -> Result<()> { 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 }); @@ -316,9 +333,9 @@ fn run() -> Result<()> { .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 timeouts from the bootloader interface. + let bootloader_interface_timeouts = advisory( + BootloaderInterface::get_timeouts().context("unable to get bootloader interface timeouts"), ); // Acquire the default entry from the bootloader interface. @@ -333,24 +350,53 @@ fn run() -> Result<()> { .context("unable to get bootloader interface oneshot entry"), ); + // Load the loader.conf of the partition Sprout was loaded from. + let loader_conf = load_loader_conf(&context).context("unable to load loader.conf")?; + for warning in &loader_conf.warnings { + warn!("{}", warning); + } + // 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); + // Pick the menu timeout from the first source that specifies one, in this order: + // the one-shot timeout, --menu-timeout, the configuration, LoaderConfigTimeout, loader.conf. + let seconds = |seconds: Option| { + seconds.map_or( + BootloaderInterfaceTimeout::Unspecified, + BootloaderInterfaceTimeout::Timeout, + ) + }; + let loader_conf_timeout = match loader_conf.timeout { + None => BootloaderInterfaceTimeout::Unspecified, + Some(LoaderTimeout::Seconds(0)) | Some(LoaderTimeout::Hidden) => { + BootloaderInterfaceTimeout::MenuHidden + } + Some(LoaderTimeout::Seconds(timeout)) => BootloaderInterfaceTimeout::Timeout(timeout), + Some(LoaderTimeout::Disabled) => BootloaderInterfaceTimeout::MenuDisabled, + Some(LoaderTimeout::Force) => BootloaderInterfaceTimeout::MenuForce, + }; + let timeout = [ + bootloader_interface_timeouts.oneshot, + seconds(context.root().options().menu_timeout), + seconds(config.options.menu_timeout), + bootloader_interface_timeouts.direct, + loader_conf_timeout, + ] + .into_iter() + .find(|timeout| !matches!(timeout, BootloaderInterfaceTimeout::Unspecified)) + .unwrap_or_default(); + + // The menu timeout in seconds, if no source specified one. + let mut menu_timeout = DEFAULT_MENU_TIMEOUT_SECONDS; // 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 { + // Apply the chosen timeout. + match timeout { BootloaderInterfaceTimeout::MenuForce => { // Force the boot menu, and wait for the user to choose an entry. force_boot_menu = true; @@ -378,30 +424,54 @@ fn run() -> Result<()> { } } - // 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. + // The loader.conf default entry, where @saved selects the entry of the previous boot. + let use_saved_entry = loader_conf.default.as_deref() == Some("@saved"); + let loader_conf_default_entry = if use_saved_entry { + advisory( + BootloaderInterface::get_last_booted_entry() + .context("unable to get last booted entry from bootloader interface"), + ) + } else { + loader_conf.default.clone() + }; + + // Pick the default entry from the first source that matches an entry, in this order: + // the configuration, LoaderEntryDefault, loader.conf. A source that matches no entry, + // such as when the entry was removed, is ignored so that the next source is used. + for (source, pattern, id_only) in [ + ("configuration", config.options.default_entry.clone(), false), + ( + "bootloader interface", + bootloader_interface_default_entry, + false, + ), + ("loader.conf", loader_conf_default_entry, true), + ] { + let Some(pattern) = pattern else { + continue; + }; + let matches = |entry: &BootableEntry| { + if id_only { + entry.is_match_id(&pattern) + } else { + entry.is_match(&pattern) + } + }; + if entries.iter().any(matches) { + // Mark the matching entries as the default and unmark all the others. 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) { + if matches(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 - ); + break; } + warn!( + "ignoring {} default entry '{}': no matching entry", + source, pattern + ); } // Apply bootloader interface oneshot entry settings. @@ -474,6 +544,14 @@ fn run() -> Result<()> { .context("unable to set selected entry in bootloader interface"), ); + // Save the selected entry for the next boot when loader.conf asks for it. + if use_saved_entry { + advisory( + BootloaderInterface::set_last_booted_entry(entry.name()) + .context("unable to save last booted 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")?; @@ -483,7 +561,18 @@ fn run() -> Result<()> { && !target.counter.is_bad() { match target.consume() { - Ok(path) => info!("updated boot counter of entry {}: {}", entry.name(), path), + Ok(path) => { + info!("updated boot counter of entry {}: {}", entry.name(), path); + // Tell the system where the counter is, so it can mark the boot as good. + let path = format!( + "/{}", + path.to_string().replace('\\', "/").trim_start_matches('/') + ); + advisory( + BootloaderInterface::set_boot_count_path(&path) + .context("unable to set boot count path in bootloader interface"), + ); + } Err(error) => warn!( "unable to update boot counter of entry {}: {:#}", entry.name(), diff --git a/crates/config/src/lib.rs b/crates/config/src/lib.rs index 5c773c6..4dafce4 100644 --- a/crates/config/src/lib.rs +++ b/crates/config/src/lib.rs @@ -81,9 +81,10 @@ pub struct OptionsConfiguration { /// The entry to mark as the default entry, instead of the first entry. #[serde(rename = "default-entry", default)] pub default_entry: Option, - /// The timeout of the boot menu. - #[serde(rename = "menu-timeout", default = "default_menu_timeout")] - pub menu_timeout: u64, + /// The timeout of the boot menu in seconds. When unset, the timeout comes from the + /// bootloader interface or `loader.conf`, falling back to [DEFAULT_MENU_TIMEOUT_SECONDS]. + #[serde(rename = "menu-timeout", default)] + pub menu_timeout: Option, /// The style of boot menu to display. #[serde(rename = "menu-style", default)] pub menu_style: MenuStyle, @@ -114,7 +115,7 @@ impl Default for OptionsConfiguration { fn default() -> Self { Self { default_entry: None, - menu_timeout: default_menu_timeout(), + menu_timeout: None, menu_style: Default::default(), autoconfigure: false, } @@ -141,10 +142,6 @@ pub fn latest_version() -> u32 { LATEST_VERSION } -fn default_menu_timeout() -> u64 { - DEFAULT_MENU_TIMEOUT_SECONDS -} - #[cfg(test)] mod tests { use super::*; @@ -167,11 +164,14 @@ mod tests { assert_eq!(default.options.autoconfigure, empty.options.autoconfigure); } + #[test] + fn menu_timeout_is_unset_by_default() { + assert_eq!(RootConfiguration::default().options.menu_timeout, None); + assert_eq!(empty().options.menu_timeout, None); + } + #[test] fn default_menu_timeout_is_not_zero() { - assert_eq!( - RootConfiguration::default().options.menu_timeout, - DEFAULT_MENU_TIMEOUT_SECONDS - ); + assert_ne!(DEFAULT_MENU_TIMEOUT_SECONDS, 0); } } diff --git a/crates/eficore/src/bootloader_interface.rs b/crates/eficore/src/bootloader_interface.rs index 1fd9929..a3ca694 100644 --- a/crates/eficore/src/bootloader_interface.rs +++ b/crates/eficore/src/bootloader_interface.rs @@ -34,6 +34,16 @@ pub enum BootloaderInterfaceTimeout { Unspecified, } +/// The timeouts set in the bootloader interface. +/// The one-shot timeout only applies to this boot, so it ranks above everything else. +#[derive(Default)] +pub struct BootloaderInterfaceTimeouts { + /// The timeout from LoaderConfigTimeoutOneShot. + pub oneshot: BootloaderInterfaceTimeout, + /// The timeout from LoaderConfigTimeout. + pub direct: BootloaderInterfaceTimeout, +} + /// Bootloader Interface support. pub struct BootloaderInterface; @@ -54,6 +64,8 @@ impl BootloaderInterface { | LoaderFeatures::MenuDisable | LoaderFeatures::EntryDefault | LoaderFeatures::EntryOneShot + | LoaderFeatures::BootCounting + | LoaderFeatures::SavedEntry } /// Tell the system that Sprout was initialized at the current time. @@ -263,45 +275,62 @@ impl BootloaderInterface { Ok(Some(BootloaderInterfaceTimeout::Timeout(value))) } - /// Get the timeout from the bootloader interface. + /// Get the timeouts from the bootloader interface. /// This indicates how the menu should behave. - /// If no values are set, Unspecified is returned. - pub fn get_timeout() -> Result { + /// A timeout that is not set is Unspecified. + pub fn get_timeouts() -> Result { // Attempt to acquire the value of the LoaderConfigTimeoutOneShot variable. - // This should take precedence over the LoaderConfigTimeout variable. - let oneshot = Self::get_timeout_value("LoaderConfigTimeoutOneShot", true) - .context("unable to check for LoaderConfigTimeoutOneShot variable")?; - - // If oneshot was found, return it. // The specification says the one-shot timeout shows the menu on this boot, // and a value of 0 means the menu waits for the user. - if let Some(oneshot) = oneshot { - return Ok(match oneshot { + let oneshot = Self::get_timeout_value("LoaderConfigTimeoutOneShot", true) + .context("unable to check for LoaderConfigTimeoutOneShot variable")? + .map(|oneshot| match oneshot { BootloaderInterfaceTimeout::Timeout(0) => BootloaderInterfaceTimeout::MenuForce, BootloaderInterfaceTimeout::Timeout(value) => { BootloaderInterfaceTimeout::MenuForceTimeout(value) } other => other, - }); - } + }) + .unwrap_or_default(); // Attempt to acquire the value of the LoaderConfigTimeout variable. - // This will be used if the LoaderConfigTimeoutOneShot variable is not set. - let direct = Self::get_timeout_value("LoaderConfigTimeout", false) - .context("unable to check for LoaderConfigTimeout variable")?; - - // If direct was found, return it. // The specification says that a value of 0 means that the menu should be hidden. - if let Some(direct) = direct { - return Ok(match direct { + let direct = Self::get_timeout_value("LoaderConfigTimeout", false) + .context("unable to check for LoaderConfigTimeout variable")? + .map(|direct| match direct { BootloaderInterfaceTimeout::Timeout(0) => BootloaderInterfaceTimeout::MenuHidden, other => other, - }); - } + }) + .unwrap_or_default(); - // If we reach here, we know that neither variable was set. - // We provide the unspecified value instead. - Ok(BootloaderInterfaceTimeout::Unspecified) + Ok(BootloaderInterfaceTimeouts { oneshot, direct }) + } + + /// Tell the system the path of the entry file whose boot counter was updated, relative + /// to the partition root, so that it can be marked as good once the boot succeeds. + pub fn set_boot_count_path(path: &str) -> Result<()> { + Self::VENDOR.set_cstr16( + "LoaderBootCountPath", + path, + VariableClass::BootAndRuntimeTemporary, + ) + } + + /// Get the entry that was saved by the previous boot, if any. + pub fn get_last_booted_entry() -> Result> { + Ok(Self::VENDOR + .get_cstr16("LoaderEntryLastBooted") + .context("unable to get last booted entry from bootloader interface")? + .filter(|value| !value.is_empty())) + } + + /// Save the entry that is booting, so a later boot can select it again. + pub fn set_last_booted_entry(entry: &str) -> Result<()> { + Self::VENDOR.set_cstr16( + "LoaderEntryLastBooted", + entry, + VariableClass::BootAndRuntimePersistent, + ) } /// Get the default entry set by the bootloader interface. diff --git a/crates/eficore/src/variables.rs b/crates/eficore/src/variables.rs index 5fe860c..00bded3 100644 --- a/crates/eficore/src/variables.rs +++ b/crates/eficore/src/variables.rs @@ -14,6 +14,8 @@ use uefi_raw::table::runtime::{VariableAttributes, VariableVendor}; pub enum VariableClass { /// The variable is available in Boot Services and Runtime Services and is not persistent. BootAndRuntimeTemporary, + /// The variable is available in Boot Services and Runtime Services and is persistent. + BootAndRuntimePersistent, } impl VariableClass { @@ -23,6 +25,11 @@ impl VariableClass { VariableClass::BootAndRuntimeTemporary => { VariableAttributes::BOOTSERVICE_ACCESS | VariableAttributes::RUNTIME_ACCESS } + VariableClass::BootAndRuntimePersistent => { + VariableAttributes::BOOTSERVICE_ACCESS + | VariableAttributes::RUNTIME_ACCESS + | VariableAttributes::NON_VOLATILE + } } } }