diff --git a/crates/bls/src/lib.rs b/crates/bls/src/lib.rs index 94148f0..7808da9 100644 --- a/crates/bls/src/lib.rs +++ b/crates/bls/src/lib.rs @@ -11,7 +11,7 @@ mod loader_conf; mod pe; mod uki; -pub use loader_conf::{LoaderConf, LoaderTimeout}; +pub use loader_conf::{LoaderConf, LoaderTimeout, RebootOnError}; pub use pe::{ MAX_SECTION_SIZE, PE_MACHINE_AARCH64, PE_MACHINE_X86_64, PeImage, ReadAt, read_pe, read_sections, @@ -958,6 +958,75 @@ mod tests { assert_eq!(conf.default.as_deref(), Some("\"foo")); } + #[test] + fn reboot_on_error_defaults_to_auto() { + assert_eq!(LoaderConf::parse("").reboot_on_error, RebootOnError::Auto); + } + + #[test] + fn reboot_on_error_reads_booleans_and_auto() { + for (value, expected) in [ + ("auto", RebootOnError::Auto), + ("yes", RebootOnError::Yes), + ("true", RebootOnError::Yes), + ("1", RebootOnError::Yes), + ("on", RebootOnError::Yes), + ("y", RebootOnError::Yes), + ("t", RebootOnError::Yes), + ("no", RebootOnError::No), + ("off", RebootOnError::No), + ("0", RebootOnError::No), + ("f", RebootOnError::No), + ("false", RebootOnError::No), + ("n", RebootOnError::No), + ("\"no\"", RebootOnError::No), + ] { + let conf = LoaderConf::parse(&format!("reboot-on-error {value}\n")); + assert_eq!(conf.reboot_on_error, expected, "{value}"); + assert!(conf.warnings.is_empty(), "{value}"); + } + } + + #[test] + fn reboot_on_error_ignores_invalid_values_with_a_warning() { + // The values are case sensitive, as they are in systemd-boot. + for value in ["YES", "Auto", "maybe", ""] { + let conf = LoaderConf::parse(&format!("reboot-on-error {value}\n")); + assert_eq!(conf.reboot_on_error, RebootOnError::Auto, "{value}"); + assert_eq!(conf.warnings.len(), 1, "{value}"); + } + } + + #[test] + fn reboot_on_error_last_value_wins() { + let conf = LoaderConf::parse("reboot-on-error no\nreboot-on-error yes\n"); + assert_eq!(conf.reboot_on_error, RebootOnError::Yes); + } + + #[test] + fn reboot_on_error_decides_from_the_tries_that_were_left() { + assert!(!RebootOnError::Auto.should_reboot(None)); + assert!(!RebootOnError::Auto.should_reboot(Some(0))); + assert!(RebootOnError::Auto.should_reboot(Some(1))); + assert!(RebootOnError::Auto.should_reboot(Some(3))); + assert!(RebootOnError::Yes.should_reboot(None)); + assert!(!RebootOnError::No.should_reboot(Some(3))); + } + + #[test] + fn auto_reboots_are_bounded_by_the_boot_counter() { + // An entry with two tries that keeps failing is rebooted twice, and then stays put. + let mut counter = BootCounter::new(2, 0); + let mut reboots = 0; + for _ in 0..10 { + if RebootOnError::Auto.should_reboot(Some(counter.tries_left)) { + reboots += 1; + } + counter = counter.decremented(); + } + assert_eq!(reboots, 2); + } + #[test] fn loader_conf_empty_has_no_settings() { let conf = LoaderConf::parse(""); diff --git a/crates/bls/src/loader_conf.rs b/crates/bls/src/loader_conf.rs index 27a5001..9244f73 100644 --- a/crates/bls/src/loader_conf.rs +++ b/crates/bls/src/loader_conf.rs @@ -30,6 +30,43 @@ impl LoaderTimeout { } } +/// What to do when the selected entry fails to start, from the `reboot-on-error` setting. +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +pub enum RebootOnError { + /// Never reboot. + No, + /// Always reboot, which can loop forever if the entry never starts. + Yes, + /// Reboot only if a boot counter try was just used up and tries were left, so that the + /// entry eventually runs out of tries and is not booted any more. + #[default] + Auto, +} + +impl RebootOnError { + /// Parses a `reboot-on-error` value, which is `auto` or a boolean. The words are case + /// sensitive, as in systemd-boot. + fn parse(value: &str) -> Option { + match value { + "auto" => Some(Self::Auto), + "1" | "yes" | "y" | "true" | "t" | "on" => Some(Self::Yes), + "0" | "no" | "n" | "false" | "f" | "off" => Some(Self::No), + _ => None, + } + } + + /// Whether to reboot after the entry failed to start. `consumed_tries_left` is the number of + /// tries that were left before this boot used one up, or None if the entry has no boot + /// counter or the try could not be used up, as then a reboot would not make progress. + pub fn should_reboot(self, consumed_tries_left: Option) -> bool { + match self { + Self::Yes => true, + Self::No => false, + Self::Auto => consumed_tries_left.is_some_and(|left| left > 0), + } + } +} + /// The settings Sprout understands from a `loader.conf` file. /// Reference: #[derive(Debug, Default, Clone, PartialEq, Eq)] @@ -41,6 +78,8 @@ pub struct LoaderConf { pub preferred: Option, /// How long to show the boot menu. pub timeout: Option, + /// What to do when the selected entry fails to start. + pub reboot_on_error: RebootOnError, /// Problems found while parsing, such as keys that are not supported. pub warnings: Vec, } @@ -88,6 +127,13 @@ impl LoaderConf { .warnings .push(format!("ignoring invalid loader.conf timeout '{}'", value)), }, + "reboot-on-error" => match RebootOnError::parse(value) { + Some(reboot_on_error) => conf.reboot_on_error = reboot_on_error, + None => conf.warnings.push(format!( + "ignoring invalid loader.conf reboot-on-error '{}'", + value + )), + }, "default" | "preferred" => conf .warnings .push(format!("ignoring loader.conf {} without a value", key)), diff --git a/crates/boot/src/main.rs b/crates/boot/src/main.rs index 26973cf..bb392fc 100644 --- a/crates/boot/src/main.rs +++ b/crates/boot/src/main.rs @@ -27,7 +27,7 @@ use eficore::{ setup, }; use log::{error, info, warn}; -use uefi::{entry, proto::device_path::LoadedImageDevicePath}; +use uefi::{entry, proto::device_path::LoadedImageDevicePath, runtime::ResetType}; use uefi_raw::Status; /// actions: Code that can be configured and executed by Sprout. @@ -112,7 +112,7 @@ fn load_loader_conf(context: &SproutContext) -> Result { } /// Run Sprout, returning an error if one occurs. -fn run() -> Result<()> { +fn run(reboot_on_error: &mut bool) -> 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."); @@ -644,10 +644,13 @@ fn run() -> Result<()> { // Failing to do so must not prevent the entry from booting. // An entry with no tries left, which was picked by hand, is counted too, so that it can // still be marked as good. + // The tries that were left before this boot used one up, if it did. + let mut consumed_tries_left = None; if let Some(target) = entry.boot_counter() { match target.consume() { Ok(path) => { info!("updated boot counter of entry {}: {}", entry.name(), path); + consumed_tries_left = Some(target.counter.tries_left); // Tell the system where the counter is, so it can mark the boot as good. let path = edera_sprout_bls::boot_path(&path.to_string()); advisory( @@ -663,6 +666,12 @@ fn run() -> Result<()> { } } + // Decide whether a failure to start the entry reboots the machine. This is only decided now, + // as an earlier failure did not use up a try, so a reboot would not make any progress. + *reboot_on_error = loader_conf + .reboot_on_error + .should_reboot(consumed_tries_left); + // Execute all the actions for the selected entry. for action in &entry.declaration().actions { let action = entry.context().stamp(action); @@ -689,7 +698,8 @@ fn efi_main() -> Status { } // Run Sprout, then handle the error. - let result = run(); + let mut reboot_on_error = false; + let result = run(&mut reboot_on_error); if let Err(ref error) = result { // Print an error trace. error!("sprout encountered an error: {}", error); @@ -698,6 +708,12 @@ fn efi_main() -> Status { } // Sleep to allow the user to read the error. uefi::boot::stall(DELAY_ON_ERROR); + + // Reboot when asked to, so that the next boot can use the next try or entry. + if reboot_on_error { + error!("rebooting after a failure to start the boot entry"); + uefi::runtime::reset(ResetType::COLD, Status::SUCCESS, None); + } return Status::ABORTED; }