mirror of
https://github.com/edera-dev/sprout.git
synced 2025-12-19 15:40:16 +00:00
feat(bootloader-interface): add support for LoaderConfigTimeout and LoaderConfigTimeoutOneShot
This commit is contained in:
@@ -13,6 +13,20 @@ mod bitflags;
|
||||
/// The name of the bootloader to tell the system.
|
||||
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.
|
||||
pub struct BootloaderInterface;
|
||||
|
||||
@@ -28,6 +42,9 @@ impl BootloaderInterface {
|
||||
| LoaderFeatures::LoadDriver
|
||||
| LoaderFeatures::Tpm2ActivePcrBanks
|
||||
| LoaderFeatures::RetainShim
|
||||
| LoaderFeatures::ConfigTimeout
|
||||
| LoaderFeatures::ConfigTimeoutOneShot
|
||||
| LoaderFeatures::MenuDisable
|
||||
}
|
||||
|
||||
/// Tell the system that Sprout was initialized at the current time.
|
||||
@@ -185,4 +202,86 @@ impl BootloaderInterface {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
40
src/main.rs
40
src/main.rs
@@ -1,6 +1,5 @@
|
||||
#![doc = include_str!("../README.md")]
|
||||
#![feature(uefi_std)]
|
||||
extern crate core;
|
||||
|
||||
/// The delay to wait for when an error occurs in Sprout.
|
||||
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::context::{RootContext, SproutContext};
|
||||
use crate::entries::BootableEntry;
|
||||
use crate::integrations::bootloader_interface::BootloaderInterface;
|
||||
use crate::integrations::bootloader_interface::{BootloaderInterface, BootloaderInterfaceTimeout};
|
||||
use crate::options::SproutOptions;
|
||||
use crate::options::parser::OptionsRepresentable;
|
||||
use crate::phases::phase;
|
||||
@@ -288,18 +287,51 @@ fn run() -> Result<()> {
|
||||
// Execute the 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.
|
||||
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.
|
||||
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.
|
||||
// We prefer the options over the configuration to allow for overriding.
|
||||
let menu_timeout = context
|
||||
let mut menu_timeout = context
|
||||
.root()
|
||||
.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);
|
||||
|
||||
// Use the forced boot entry if possible, otherwise pick the first entry using a boot menu.
|
||||
|
||||
18
src/utils.rs
18
src/utils.rs
@@ -1,4 +1,4 @@
|
||||
use anyhow::{Context, Result};
|
||||
use anyhow::{Context, Result, bail};
|
||||
use std::ops::Deref;
|
||||
use uefi::boot::SearchType;
|
||||
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")
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
use crate::utils;
|
||||
use anyhow::{Context, Result};
|
||||
use log::warn;
|
||||
use uefi::{CString16, guid};
|
||||
use uefi_raw::Status;
|
||||
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")
|
||||
}
|
||||
|
||||
/// 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`.
|
||||
pub fn get_bool(&self, key: &str) -> Result<bool> {
|
||||
let name = Self::name(key)?;
|
||||
@@ -104,4 +141,12 @@ impl VariableController {
|
||||
pub fn set_u64le(&self, key: &str, value: u64, class: VariableClass) -> Result<()> {
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user