diff --git a/src/integrations/bootloader_interface.rs b/src/integrations/bootloader_interface.rs index 5c7078e..e6a422d 100644 --- a/src/integrations/bootloader_interface.rs +++ b/src/integrations/bootloader_interface.rs @@ -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> { + // 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::() + .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 { + // 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) + } } diff --git a/src/main.rs b/src/main.rs index ecd0c81..1859b72 100644 --- a/src/main.rs +++ b/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. diff --git a/src/utils.rs b/src/utils.rs index 66703cc..ab9bde4 100644 --- a/src/utils.rs +++ b/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> { } } } + +/// Convert a byte slice into a CString16. +pub fn utf16_bytes_to_cstring16(bytes: &[u8]) -> Result { + // 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") +} diff --git a/src/utils/variables.rs b/src/utils/variables.rs index 3f48955..22615a5 100644 --- a/src/utils/variables.rs +++ b/src/utils/variables.rs @@ -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> { + 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 { 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)) + } }