mirror of
https://github.com/edera-dev/sprout.git
synced 2026-11-04 17:52:20 +00:00
feat(bls): reboot when an entry with boot counting fails to start
Add the reboot-on-error loader.conf setting. With auto, which is the default, the machine resets after a failed start only if a boot counter try was just used up and there were tries left, so the entry eventually runs out of tries and the next boot picks another one. yes always resets and no never does.
This commit is contained in:
1 parent
14bcfc2653
commit
d41bcaa292
3 files changed
+135
-4
No files matched your search
+70
-1
@@ -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("");
|
||||
|
||||
@@ -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<Self> {
|
||||
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<u32>) -> 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: <https://www.freedesktop.org/software/systemd/man/latest/loader.conf.html>
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq)]
|
||||
@@ -41,6 +78,8 @@ pub struct LoaderConf {
|
||||
pub preferred: Option<String>,
|
||||
/// How long to show the boot menu.
|
||||
pub timeout: Option<LoaderTimeout>,
|
||||
/// 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<String>,
|
||||
}
|
||||
@@ -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)),
|
||||
|
||||
+19
-3
@@ -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<LoaderConf> {
|
||||
}
|
||||
|
||||
/// 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;
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user