mirror of
https://github.com/edera-dev/sprout.git
synced 2025-12-19 17:10:17 +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.
|
/// 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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
40
src/main.rs
40
src/main.rs
@@ -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.
|
||||||
|
|||||||
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 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")
|
||||||
|
}
|
||||||
|
|||||||
@@ -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))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user