feat(bootloader-interface): add support for LoaderConfigTimeout and LoaderConfigTimeoutOneShot

This commit is contained in:
2025-11-01 17:47:41 -04:00
parent 679b0c0290
commit f361570b0e
4 changed files with 197 additions and 5 deletions

View File

@@ -13,6 +13,20 @@ mod bitflags;
/// The name of the bootloader to tell the system. /// The name of the bootloader to tell the system.
const LOADER_NAME: &str = "Sprout"; const LOADER_NAME: &str = "Sprout";
/// Represents the configured timeout for the bootloader interface.
pub enum BootloaderInterfaceTimeout {
/// Force the menu to be shown.
MenuForce,
/// Hide the menu.
MenuHidden,
/// Disable the menu.
MenuDisabled,
/// Set a timeout for the menu.
Timeout(u64),
/// Timeout is unspecified.
Unspecified,
}
/// Bootloader Interface support. /// Bootloader Interface support.
pub struct BootloaderInterface; pub struct BootloaderInterface;
@@ -28,6 +42,9 @@ impl BootloaderInterface {
| LoaderFeatures::LoadDriver | LoaderFeatures::LoadDriver
| LoaderFeatures::Tpm2ActivePcrBanks | LoaderFeatures::Tpm2ActivePcrBanks
| LoaderFeatures::RetainShim | LoaderFeatures::RetainShim
| LoaderFeatures::ConfigTimeout
| LoaderFeatures::ConfigTimeoutOneShot
| LoaderFeatures::MenuDisable
} }
/// Tell the system that Sprout was initialized at the current time. /// Tell the system that Sprout was initialized at the current time.
@@ -185,4 +202,86 @@ impl BootloaderInterface {
VariableClass::BootAndRuntimeTemporary, VariableClass::BootAndRuntimeTemporary,
) )
} }
/// Retrieve the timeout value from the bootloader interface, using the specified `key`.
/// `remove` indicates whether, when found, we remove the variable.
fn get_timeout_value(key: &str, remove: bool) -> Result<Option<BootloaderInterfaceTimeout>> {
// Retrieve the timeout value from the bootloader interface.
let Some(value) = Self::VENDOR
.get_cstr16(key)
.context("unable to get timeout value")?
else {
return Ok(None);
};
// If we reach here, we know the value was specified.
// If `remove` is true, remove the variable.
if remove {
Self::VENDOR
.remove(key)
.context("unable to remove timeout variable")?;
}
// If the value is empty, return Unspecified.
if value.is_empty() {
return Ok(Some(BootloaderInterfaceTimeout::Unspecified));
}
// If the value is "menu-force", return MenuForce.
if value == "menu-force" {
return Ok(Some(BootloaderInterfaceTimeout::MenuForce));
}
// If the value is "menu-hidden", return MenuHidden.
if value == "menu-hidden" {
return Ok(Some(BootloaderInterfaceTimeout::MenuHidden));
}
// If the value is "menu-disabled", return MenuDisabled.
if value == "menu-disabled" {
return Ok(Some(BootloaderInterfaceTimeout::MenuDisabled));
}
// Parse the value as a u64 to decode an numeric value.
let value = value
.parse::<u64>()
.context("unable to parse timeout value")?;
// The specification says that a value of 0 means that the menu should be hidden.
if value == 0 {
return Ok(Some(BootloaderInterfaceTimeout::MenuHidden));
}
// If we reach here, we know it must be a real timeout value.
Ok(Some(BootloaderInterfaceTimeout::Timeout(value)))
}
/// Get the timeout from the bootloader interface.
/// This indicates how the menu should behave.
/// If no values are set, Unspecified is returned.
pub fn get_timeout() -> Result<BootloaderInterfaceTimeout> {
// 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.
if let Some(oneshot) = oneshot {
return Ok(oneshot);
}
// 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.
if let Some(direct) = direct {
return Ok(direct);
}
// If we reach here, we know that neither variable was set.
// We provide the unspecified value instead.
Ok(BootloaderInterfaceTimeout::Unspecified)
}
} }

View File

@@ -1,6 +1,5 @@
#![doc = include_str!("../README.md")] #![doc = include_str!("../README.md")]
#![feature(uefi_std)] #![feature(uefi_std)]
extern crate core;
/// The delay to wait for when an error occurs in Sprout. /// The delay to wait for when an error occurs in Sprout.
const DELAY_ON_ERROR: Duration = Duration::from_secs(10); const DELAY_ON_ERROR: Duration = Duration::from_secs(10);
@@ -8,7 +7,7 @@ const DELAY_ON_ERROR: Duration = Duration::from_secs(10);
use crate::config::RootConfiguration; use crate::config::RootConfiguration;
use crate::context::{RootContext, SproutContext}; use crate::context::{RootContext, SproutContext};
use crate::entries::BootableEntry; use crate::entries::BootableEntry;
use crate::integrations::bootloader_interface::BootloaderInterface; use crate::integrations::bootloader_interface::{BootloaderInterface, BootloaderInterfaceTimeout};
use crate::options::SproutOptions; use crate::options::SproutOptions;
use crate::options::parser::OptionsRepresentable; use crate::options::parser::OptionsRepresentable;
use crate::phases::phase; use crate::phases::phase;
@@ -288,18 +287,51 @@ fn run() -> Result<()> {
// Execute the late phase. // Execute the late phase.
phase(context.clone(), &config.phases.late).context("unable to execute late phase")?; phase(context.clone(), &config.phases.late).context("unable to execute late phase")?;
// Acquire the timeout setting from the bootloader interface.
let bootloader_interface_timeout =
BootloaderInterface::get_timeout().context("unable to get bootloader interface timeout")?;
// If --boot is specified, boot that entry immediately. // If --boot is specified, boot that entry immediately.
let force_boot_entry = context.root().options().boot.as_ref(); let force_boot_entry = context.root().options().boot.as_ref();
// If --force-menu is specified, show the boot menu regardless of the value of --boot. // If --force-menu is specified, show the boot menu regardless of the value of --boot.
let force_boot_menu = context.root().options().force_menu; let mut force_boot_menu = context.root().options().force_menu;
// Determine the menu timeout in seconds based on the options or configuration. // Determine the menu timeout in seconds based on the options or configuration.
// We prefer the options over the configuration to allow for overriding. // We prefer the options over the configuration to allow for overriding.
let menu_timeout = context let mut menu_timeout = context
.root() .root()
.options() .options()
.menu_timeout .menu_timeout
.unwrap_or(config.options.menu_timeout); .unwrap_or(config.options.menu_timeout);
// Apply bootloader interface timeout settings.
match bootloader_interface_timeout {
BootloaderInterfaceTimeout::MenuForce => {
// Force the boot menu.
force_boot_menu = true;
}
BootloaderInterfaceTimeout::MenuHidden => {
// Hide the boot menu by setting the timeout to zero.
menu_timeout = 0;
}
BootloaderInterfaceTimeout::MenuDisabled => {
// Disable 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.
}
}
// Convert the menu timeout to a duration.
let menu_timeout = Duration::from_secs(menu_timeout); let menu_timeout = Duration::from_secs(menu_timeout);
// Use the forced boot entry if possible, otherwise pick the first entry using a boot menu. // Use the forced boot entry if possible, otherwise pick the first entry using a boot menu.

View File

@@ -1,4 +1,4 @@
use anyhow::{Context, Result}; use anyhow::{Context, Result, bail};
use std::ops::Deref; use std::ops::Deref;
use uefi::boot::SearchType; use uefi::boot::SearchType;
use uefi::fs::{FileSystem, Path}; use uefi::fs::{FileSystem, Path};
@@ -272,3 +272,19 @@ pub fn find_handle(protocol: &Guid) -> Result<Option<Handle>> {
} }
} }
} }
/// Convert a byte slice into a CString16.
pub fn utf16_bytes_to_cstring16(bytes: &[u8]) -> Result<CString16> {
// Validate the input bytes are the right length.
if !bytes.len().is_multiple_of(2) {
bail!("utf16 bytes must be a multiple of 2");
}
// SAFETY: reinterpret &[u8] as &[u16].
// We just validated it has the right length.
let ptr = bytes.as_ptr() as *const u16;
let len = bytes.len() / 2;
let utf16 = unsafe { std::slice::from_raw_parts(ptr, len) };
CString16::try_from(utf16.to_vec()).context("unable to convert utf16 bytes to CString16")
}

View File

@@ -1,4 +1,6 @@
use crate::utils;
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use log::warn;
use uefi::{CString16, guid}; use uefi::{CString16, guid};
use uefi_raw::Status; use uefi_raw::Status;
use uefi_raw::table::runtime::{VariableAttributes, VariableVendor}; use uefi_raw::table::runtime::{VariableAttributes, VariableVendor};
@@ -44,6 +46,41 @@ impl VariableController {
CString16::try_from(key).context("unable to convert variable name to CString16") CString16::try_from(key).context("unable to convert variable name to CString16")
} }
/// Retrieve the cstr16 value specified by the `key`.
/// Returns None if the value isn't set.
/// If the value is not decodable, we will return None and log a warning.
pub fn get_cstr16(&self, key: &str) -> Result<Option<String>> {
let name = Self::name(key)?;
// Retrieve the variable data, handling variable not existing as None.
match uefi::runtime::get_variable_boxed(&name, &self.vendor) {
Ok((data, _)) => {
// Try to decode UTF-16 bytes to a CString16.
match utils::utf16_bytes_to_cstring16(&data) {
Ok(value) => {
// We have a value, so return the UTF-8 value.
Ok(Some(value.to_string()))
}
Err(error) => {
// We encountered an error, so warn and return None.
warn!("efi variable '{}' is not valid UTF-16: {}", key, error);
Ok(None)
}
}
}
Err(error) => {
// If the variable does not exist, we will return None.
if error.status() == Status::NOT_FOUND {
Ok(None)
} else {
Err(error).with_context(|| format!("unable to get efi variable {}", key))
}
}
}
}
/// Retrieve a boolean value specified by the `key`. /// Retrieve a boolean value specified by the `key`.
pub fn get_bool(&self, key: &str) -> Result<bool> { pub fn get_bool(&self, key: &str) -> Result<bool> {
let name = Self::name(key)?; let name = Self::name(key)?;
@@ -104,4 +141,12 @@ impl VariableController {
pub fn set_u64le(&self, key: &str, value: u64, class: VariableClass) -> Result<()> { pub fn set_u64le(&self, key: &str, value: u64, class: VariableClass) -> Result<()> {
self.set(key, &value.to_le_bytes(), class) self.set(key, &value.to_le_bytes(), class)
} }
pub fn remove(&self, key: &str) -> Result<()> {
let name = Self::name(key)?;
// Delete the variable from UEFI.
uefi::runtime::delete_variable(&name, &self.vendor)
.with_context(|| format!("unable to remove efi variable {}", key))
}
} }