From f82d24a206901a9597d85d6d2b1ce8687624df7f Mon Sep 17 00:00:00 2001 From: Alex Zenla Date: Tue, 28 Oct 2025 21:05:22 -0400 Subject: [PATCH 1/9] feat(integrations): implement initial bootloader interface touchpoints --- src/actions/chainload.rs | 5 ++ src/extractors/filesystem_device_match.rs | 91 ++++++++--------------- src/integrations.rs | 2 + src/integrations/bootloader_interface.rs | 43 +++++++++++ src/main.rs | 50 ++++++++++++- src/utils.rs | 50 ++++++++++++- 6 files changed, 178 insertions(+), 63 deletions(-) create mode 100644 src/integrations.rs create mode 100644 src/integrations/bootloader_interface.rs diff --git a/src/actions/chainload.rs b/src/actions/chainload.rs index b14015f..8223058 100644 --- a/src/actions/chainload.rs +++ b/src/actions/chainload.rs @@ -1,4 +1,5 @@ use crate::context::SproutContext; +use crate::integrations::bootloader_interface::BootloaderInterface; use crate::utils; use crate::utils::media_loader::MediaLoaderHandle; use crate::utils::media_loader::constants::linux::LINUX_EFI_INITRD_MEDIA_GUID; @@ -102,6 +103,10 @@ pub fn chainload(context: Rc, configuration: &ChainloadConfigurat initrd_handle = Some(handle); } + // Mark execution of an entry in the bootloader interface. + BootloaderInterface::mark_exec() + .context("unable to mark execution of boot entry in bootloader interface")?; + // Start the loaded image. // This call might return, or it may pass full control to another image that will never return. // Capture the result to ensure we can return an error if the image fails to start, but only diff --git a/src/extractors/filesystem_device_match.rs b/src/extractors/filesystem_device_match.rs index 6a6a75a..5bfb0be 100644 --- a/src/extractors/filesystem_device_match.rs +++ b/src/extractors/filesystem_device_match.rs @@ -9,9 +9,7 @@ use uefi::fs::{FileSystem, Path}; use uefi::proto::device_path::DevicePath; use uefi::proto::media::file::{File, FileSystemVolumeLabel}; use uefi::proto::media::fs::SimpleFileSystem; -use uefi::proto::media::partition::PartitionInfo; -use uefi::{CString16, Guid, Handle}; -use uefi_raw::Status; +use uefi::{CString16, Guid}; /// The filesystem device match extractor. /// This extractor finds a filesystem using some search criteria and returns @@ -41,48 +39,6 @@ pub struct FilesystemDeviceMatchExtractor { pub fallback: Option, } -/// Represents the partition UUIDs for a filesystem. -struct PartitionIds { - /// The UUID of the partition. - partition_uuid: Guid, - /// The type UUID of the partition. - type_uuid: Guid, -} - -/// Fetches the partition UUIDs for the specified filesystem handle. -fn fetch_partition_uuids(handle: Handle) -> Result> { - // Open the partition info protocol for this handle. - let partition_info = uefi::boot::open_protocol_exclusive::(handle); - - match partition_info { - Ok(partition_info) => { - // GPT partitions have a unique partition GUID. - // MBR does not. - if let Some(gpt) = partition_info.gpt_partition_entry() { - let uuid = gpt.unique_partition_guid; - let type_uuid = gpt.partition_type_guid; - Ok(Some(PartitionIds { - partition_uuid: uuid, - type_uuid: type_uuid.0, - })) - } else { - Ok(None) - } - } - - Err(error) => { - // If the filesystem does not have a partition, that is okay. - if error.status() == Status::NOT_FOUND || error.status() == Status::UNSUPPORTED { - Ok(None) - } else { - // We should still handle other errors gracefully. - Err(error).context("unable to open filesystem partition info")?; - unreachable!() - } - } - } -} - /// Extract a filesystem device path using the specified `context` and `extractor` configuration. pub fn extract( context: Rc, @@ -106,30 +62,49 @@ pub fn extract( // This defines whether a match has been found. let mut has_match = false; - // Extract the partition info for this filesystem. - // There is no guarantee that the filesystem has a partition. - let partition_info = - fetch_partition_uuids(handle).context("unable to fetch partition info")?; - // Check if the partition info matches partition uuid criteria. - if let Some(ref partition_info) = partition_info - && let Some(ref has_partition_uuid) = extractor.has_partition_uuid - { + if let Some(ref has_partition_uuid) = extractor.has_partition_uuid { + // Parse the partition uuid from the extractor. let parsed_uuid = Guid::from_str(has_partition_uuid) .map_err(|e| anyhow!("unable to parse has-partition-uuid: {}", e))?; - if partition_info.partition_uuid != parsed_uuid { + + // Fetch the root of the device. + let root = uefi::boot::open_protocol_exclusive::(handle) + .context("unable to fetch the device path of the filesystem")? + .deref() + .to_boxed(); + + // Fetch the partition uuid for this filesystem. + let partition_uuid = utils::partition_guid(&root, utils::PartitionGuidForm::Partition) + .context("unable to fetch the partition uuid of the filesystem")?; + + // Compare the partition uuid to the parsed uuid. + // If it does not match, continue to the next filesystem. + if partition_uuid != Some(parsed_uuid) { continue; } has_match = true; } // Check if the partition info matches partition type uuid criteria. - if let Some(ref partition_info) = partition_info - && let Some(ref has_partition_type_uuid) = extractor.has_partition_type_uuid - { + if let Some(ref has_partition_type_uuid) = extractor.has_partition_type_uuid { + // Parse the partition type uuid from the extractor. let parsed_uuid = Guid::from_str(has_partition_type_uuid) .map_err(|e| anyhow!("unable to parse has-partition-type-uuid: {}", e))?; - if partition_info.type_uuid != parsed_uuid { + + // Fetch the root of the device. + let root = uefi::boot::open_protocol_exclusive::(handle) + .context("unable to fetch the device path of the filesystem")? + .deref() + .to_boxed(); + + // Fetch the partition uuid for this filesystem. + let partition_type_uuid = + utils::partition_guid(&root, utils::PartitionGuidForm::Partition) + .context("unable to fetch the partition uuid of the filesystem")?; + // Compare the partition type uuid to the parsed uuid. + // If it does not match, continue to the next filesystem. + if partition_type_uuid != Some(parsed_uuid) { continue; } has_match = true; diff --git a/src/integrations.rs b/src/integrations.rs new file mode 100644 index 0000000..286bf58 --- /dev/null +++ b/src/integrations.rs @@ -0,0 +1,2 @@ +/// Implements support for the bootloader interface specification. +pub mod bootloader_interface; diff --git a/src/integrations/bootloader_interface.rs b/src/integrations/bootloader_interface.rs new file mode 100644 index 0000000..5d43b76 --- /dev/null +++ b/src/integrations/bootloader_interface.rs @@ -0,0 +1,43 @@ +use anyhow::Result; +use uefi::Guid; + +/// Bootloader Interface support. +pub struct BootloaderInterface; + +impl BootloaderInterface { + /// Tell the system that Sprout was initialized at the current time. + pub fn mark_init() -> Result<()> { + // TODO(azenla): Implement support for LoaderTimeInitUSec here. + Ok(()) + } + + /// Tell the system that Sprout is about to execute the boot entry. + pub fn mark_exec() -> Result<()> { + // TODO(azenla): Implement support for LoaderTimeExecUSec here. + Ok(()) + } + + /// Tell the system what the partition GUID of the ESP Sprout was booted from is. + pub fn set_partition_guid(_guid: &Guid) -> Result<()> { + // TODO(azenla): Implement support for LoaderDevicePartUUID here. + Ok(()) + } + + /// Tell the system what boot entries are available. + pub fn set_entries>(_entries: impl Iterator) -> Result<()> { + // TODO(azenla): Implement support for LoaderEntries here. + Ok(()) + } + + /// Tell the system what the default boot entry is. + pub fn set_default_entry(_entry: String) -> Result<()> { + // TODO(azenla): Implement support for LoaderEntryDefault here. + Ok(()) + } + + /// Tell the system what the selected boot entry is. + pub fn set_selected_entry(_entry: String) -> Result<()> { + // TODO(azenla): Implement support for LoaderEntrySelected here. + Ok(()) + } +} diff --git a/src/main.rs b/src/main.rs index 40caa9e..bbe8f8d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,9 +4,11 @@ use crate::config::RootConfiguration; use crate::context::{RootContext, SproutContext}; use crate::entries::BootableEntry; +use crate::integrations::bootloader_interface::BootloaderInterface; use crate::options::SproutOptions; use crate::options::parser::OptionsRepresentable; use crate::phases::phase; +use crate::utils::PartitionGuidForm; use anyhow::{Context, Result, bail}; use log::{error, info}; use std::collections::BTreeMap; @@ -41,6 +43,9 @@ pub mod generators; /// menu: Display a boot menu to select an entry to boot. pub mod menu; +/// integrations: Code that interacts with other systems. +pub mod integrations; + /// phases: Hooks into specific parts of the boot process. pub mod phases; @@ -55,6 +60,10 @@ pub mod utils; /// Run Sprout, returning an error if one occurs. fn run() -> Result<()> { + // Mark the initialization of Sprout in the bootloader interface. + BootloaderInterface::mark_init() + .context("unable to mark initialization in bootloader interface")?; + // Parse the options to the sprout executable. let options = SproutOptions::parse().context("unable to parse options")?; @@ -69,17 +78,31 @@ fn run() -> Result<()> { config::loader::load(&options)? }; - // Load the root context. + // Grab the sprout.efi loaded image path. // This is done in a block to ensure the release of the LoadedImageDevicePath protocol. - let mut root = { + let loaded_image_path = { let current_image_device_path_protocol = uefi::boot::open_protocol_exclusive::< LoadedImageDevicePath, >(uefi::boot::image_handle()) .context("unable to get loaded image device path")?; - let loaded_image_path = current_image_device_path_protocol.deref().to_boxed(); - RootContext::new(loaded_image_path, options) + current_image_device_path_protocol.deref().to_boxed() }; + // Grab the partition GUID of the ESP that sprout was loaded from. + let loaded_image_partition_guid = + utils::partition_guid(&loaded_image_path, PartitionGuidForm::Partition) + .context("unable to retrieve loaded image partition guid")?; + + // Set the partition GUID of the ESP that sprout was loaded from in the bootloader interface. + if let Some(loaded_image_partition_guid) = loaded_image_partition_guid { + // Tell the system about the partition GUID. + BootloaderInterface::set_partition_guid(&loaded_image_partition_guid) + .context("unable to set partition guid in bootloader interface")?; + } + + // Create the root context. + let mut root = RootContext::new(loaded_image_path, options); + // Insert the configuration actions into the root context. root.actions_mut().extend(config.actions.clone()); @@ -200,6 +223,21 @@ fn run() -> Result<()> { entry.mark_default(); } + // Iterate over all the entries and tell the bootloader interface what the entries are. + for entry in &entries { + // If the entry is the default entry, tell the bootloader interface it is the default. + if entry.is_default() { + // Tell the bootloader interface what the default entry is. + BootloaderInterface::set_default_entry(entry.name().to_string()) + .context("unable to set default entry in bootloader interface")?; + break; + } + } + + // Tell the bootloader interface what entries are available. + BootloaderInterface::set_entries(entries.iter().map(|entry| entry.name())) + .context("unable to set entries in bootloader interface")?; + // Execute the late phase. phase(context.clone(), &config.phases.late).context("unable to execute late phase")?; @@ -226,6 +264,10 @@ fn run() -> Result<()> { menu::select(menu_timeout, &entries).context("unable to select entry via boot menu")? }; + // Tell the bootloader interface what the selected entry is. + BootloaderInterface::set_selected_entry(entry.name().to_string()) + .context("unable to set selected entry in bootloader interface")?; + // Execute all the actions for the selected entry. for action in &entry.declaration().actions { let action = entry.context().stamp(action); diff --git a/src/utils.rs b/src/utils.rs index e988b8c..936a508 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -4,7 +4,9 @@ use uefi::fs::{FileSystem, Path}; use uefi::proto::device_path::text::{AllowShortcuts, DevicePathFromText, DisplayOnly}; use uefi::proto::device_path::{DevicePath, PoolDevicePath}; use uefi::proto::media::fs::SimpleFileSystem; -use uefi::{CString16, Handle}; +use uefi::proto::media::partition::PartitionInfo; +use uefi::{CString16, Guid, Handle}; +use uefi_raw::Status; /// Support code for the EFI framebuffer. pub mod framebuffer; @@ -181,3 +183,49 @@ pub fn combine_options>(options: impl Iterator) -> Strin pub fn unique_hash(input: &str) -> String { sha256::digest(input.as_bytes()) } + +/// Represents the type of partition GUID that can be retrieved. +#[derive(PartialEq, Eq)] +pub enum PartitionGuidForm { + Partition, + PartitionType, +} + +/// Retrieve the partition / partition type GUID of the device root `path`. +/// This only works on GPT partitions. If the root is not a GPT partition, None is returned. +pub fn partition_guid(path: &DevicePath, form: PartitionGuidForm) -> Result> { + // Clone the path so we can pass it to the UEFI stack. + let path = path.to_boxed(); + let result = uefi::boot::locate_device_path::(&mut &*path); + let handle = match result { + Ok(handle) => Ok(Some(handle)), + Err(error) => { + // If the error is NOT_FOUND or UNSUPPORTED, we can return None. + // These are non-fatal errors. + if error.status() == Status::NOT_FOUND || error.status() == Status::UNSUPPORTED { + Ok(None) + } else { + Err(error) + } + } + } + .context("unable to locate device path")?; + + // If we have the handle, we can try to open the partition info protocol. + if let Some(handle) = handle { + // Open the partition info protocol. + let partition_info = uefi::boot::open_protocol_exclusive::(handle) + .context("unable to open partition info protocol")?; + // Find the unique partition GUID. + // If this is not a GPT partition, this will produce None. + Ok(partition_info + .gpt_partition_entry() + .map(|entry| match form { + // Match the form of the partition GUID. + PartitionGuidForm::Partition => entry.unique_partition_guid, + PartitionGuidForm::PartitionType => entry.partition_type_guid.0, + })) + } else { + Ok(None) + } +} From e7d2438e5f8ec4bd783e5660aca1f17fff6bf17e Mon Sep 17 00:00:00 2001 From: Alex Zenla Date: Tue, 28 Oct 2025 23:23:12 -0400 Subject: [PATCH 2/9] feat(bls): basic support for boot loader interface --- src/entries.rs | 12 +++++ src/generators/bls.rs | 19 ++++++-- src/integrations/bootloader_interface.rs | 58 ++++++++++++++++++------ src/main.rs | 8 +++- 4 files changed, 78 insertions(+), 19 deletions(-) diff --git a/src/entries.rs b/src/entries.rs index d624c77..eed0ece 100644 --- a/src/entries.rs +++ b/src/entries.rs @@ -28,6 +28,7 @@ pub struct BootableEntry { context: Rc, declaration: EntryDeclaration, default: bool, + pin_name: bool, } impl BootableEntry { @@ -44,6 +45,7 @@ impl BootableEntry { context, declaration, default: false, + pin_name: false, } } @@ -72,6 +74,11 @@ impl BootableEntry { self.default } + /// Fetch whether the entry is pinned, which prevents prefixing. + pub fn is_pin_name(&self) -> bool { + self.pin_name + } + /// Swap out the context of the entry. pub fn swap_context(&mut self, context: Rc) { self.context = context; @@ -87,6 +94,11 @@ impl BootableEntry { self.default = true; } + /// Mark this entry as being pinned, which prevents prefixing. + pub fn mark_pin_name(&mut self) { + self.pin_name = true; + } + /// Prepend the name of the entry with `prefix`. pub fn prepend_name_prefix(&mut self, prefix: &str) { self.name.insert_str(0, prefix); diff --git a/src/generators/bls.rs b/src/generators/bls.rs index 9dc8d7d..4133427 100644 --- a/src/generators/bls.rs +++ b/src/generators/bls.rs @@ -83,13 +83,16 @@ pub fn generate(context: Rc, bls: &BlsConfiguration) -> Result, bls: &BlsConfiguration) -> Result Result<()> { // TODO(azenla): Implement support for LoaderTimeInitUSec here. @@ -18,26 +22,54 @@ impl BootloaderInterface { } /// Tell the system what the partition GUID of the ESP Sprout was booted from is. - pub fn set_partition_guid(_guid: &Guid) -> Result<()> { - // TODO(azenla): Implement support for LoaderDevicePartUUID here. - Ok(()) + pub fn set_partition_guid(guid: &Guid) -> Result<()> { + Self::set_cstr16("LoaderDevicePartUUID", &guid.to_string()) } /// Tell the system what boot entries are available. - pub fn set_entries>(_entries: impl Iterator) -> Result<()> { - // TODO(azenla): Implement support for LoaderEntries here. - Ok(()) + pub fn set_entries>(entries: impl Iterator) -> Result<()> { + // Entries are stored as a null-terminated list of CString16 strings back to back. + // Iterate over the entries and convert them to CString16 placing them into data. + let mut data = Vec::new(); + for entry in entries { + // Convert the entry to a CString16. + let entry = CString16::try_from(entry.as_ref()) + .context("unable to convert entry to CString16")?; + // Write the bytes (including the null terminator) into the data buffer. + data.extend_from_slice(entry.as_bytes()); + } + Self::set("LoaderEntries", &data) } /// Tell the system what the default boot entry is. - pub fn set_default_entry(_entry: String) -> Result<()> { - // TODO(azenla): Implement support for LoaderEntryDefault here. - Ok(()) + pub fn set_default_entry(entry: String) -> Result<()> { + Self::set_cstr16("LoaderEntryDefault", &entry) } /// Tell the system what the selected boot entry is. - pub fn set_selected_entry(_entry: String) -> Result<()> { - // TODO(azenla): Implement support for LoaderEntrySelected here. + pub fn set_selected_entry(entry: String) -> Result<()> { + Self::set_cstr16("LoaderEntrySelected", &entry) + } + + /// The [VariableAttributes] for bootloader interface variables. + fn attributes() -> VariableAttributes { + VariableAttributes::BOOTSERVICE_ACCESS | VariableAttributes::RUNTIME_ACCESS + } + + /// Set a bootloader interface variable specified by `key` to `value`. + fn set(key: &str, value: &[u8]) -> Result<()> { + let name = + CString16::try_from(key).context("unable to convert variable name to CString16")?; + uefi::runtime::set_variable(&name, &Self::VENDOR, Self::attributes(), value) + .with_context(|| format!("unable to set efi variable {}", key))?; Ok(()) } + + /// Set a bootloader interface variable specified by `key` to `value`, converting the value to + /// a [CString16]. + fn set_cstr16(key: &str, value: &str) -> Result<()> { + let value = + CString16::try_from(value).context("unable to convert variable value to CString16")?; + Self::set(key, value.as_bytes()) + } } diff --git a/src/main.rs b/src/main.rs index bbe8f8d..f936654 100644 --- a/src/main.rs +++ b/src/main.rs @@ -182,13 +182,17 @@ fn run() -> Result<()> { for (name, generator) in config.generators { let context = context.fork().freeze(); - // We will prefix all entries with [name]-. + // We will prefix all entries with [name]-, provided the name is not pinned. let prefix = format!("{}-", name); // Add all the entries generated by the generator to the entry list. // The generator specifies the context associated with the entry. for mut entry in generators::generate(context.clone(), &generator)? { - entry.prepend_name_prefix(&prefix); + // If the entry name is not pinned, prepend the name prefix. + if !entry.is_pin_name() { + entry.prepend_name_prefix(&prefix); + } + entries.push(entry); } } From 87d608366f6f1091f88ce3c0cd7b6b89295953ff Mon Sep 17 00:00:00 2001 From: Alex Zenla Date: Thu, 30 Oct 2025 02:36:14 -0400 Subject: [PATCH 3/9] feat(bootloader-interface): add support for loader boot times --- src/actions/chainload.rs | 2 +- src/context.rs | 19 +++++- src/integrations/bootloader_interface.rs | 11 ++-- src/main.rs | 10 ++- src/platform.rs | 2 + src/platform/timer.rs | 81 ++++++++++++++++++++++++ src/platform/timer/aarch64.rs | 33 ++++++++++ src/platform/timer/x86_64.rs | 66 +++++++++++++++++++ 8 files changed, 214 insertions(+), 10 deletions(-) create mode 100644 src/platform.rs create mode 100644 src/platform/timer.rs create mode 100644 src/platform/timer/aarch64.rs create mode 100644 src/platform/timer/x86_64.rs diff --git a/src/actions/chainload.rs b/src/actions/chainload.rs index 8223058..5341c4c 100644 --- a/src/actions/chainload.rs +++ b/src/actions/chainload.rs @@ -104,7 +104,7 @@ pub fn chainload(context: Rc, configuration: &ChainloadConfigurat } // Mark execution of an entry in the bootloader interface. - BootloaderInterface::mark_exec() + BootloaderInterface::mark_exec(context.root().timer()) .context("unable to mark execution of boot entry in bootloader interface")?; // Start the loaded image. diff --git a/src/context.rs b/src/context.rs index 22998bc..cc15ab2 100644 --- a/src/context.rs +++ b/src/context.rs @@ -1,5 +1,6 @@ use crate::actions::ActionDeclaration; use crate::options::SproutOptions; +use crate::platform::timer::PlatformTimer; use anyhow::anyhow; use anyhow::{Result, bail}; use std::cmp::Reverse; @@ -12,22 +13,29 @@ const CONTEXT_FINALIZE_ITERATION_LIMIT: usize = 100; /// Declares a root context for Sprout. /// This contains data that needs to be shared across Sprout. -#[derive(Default)] pub struct RootContext { /// The actions that are available in Sprout. actions: BTreeMap, /// The device path of the loaded Sprout image. loaded_image_path: Option>, + /// Platform timer started at the beginning of the boot process. + timer: PlatformTimer, /// The global options of Sprout. options: SproutOptions, } impl RootContext { /// Creates a new root context with the `loaded_image_device_path` which will be stored - /// in the context for easy access. - pub fn new(loaded_image_device_path: Box, options: SproutOptions) -> Self { + /// in the context for easy access. We also provide a `timer` which is used to measure elapsed + /// time for the bootloader. + pub fn new( + loaded_image_device_path: Box, + timer: PlatformTimer, + options: SproutOptions, + ) -> Self { Self { actions: BTreeMap::new(), + timer, loaded_image_path: Some(loaded_image_device_path), options, } @@ -43,6 +51,11 @@ impl RootContext { &mut self.actions } + /// Access the platform timer that is started at the beginning of the boot process. + pub fn timer(&self) -> &PlatformTimer { + &self.timer + } + /// Access the device path of the loaded Sprout image. pub fn loaded_image_path(&self) -> Result<&DevicePath> { self.loaded_image_path diff --git a/src/integrations/bootloader_interface.rs b/src/integrations/bootloader_interface.rs index 4053ec9..b291d70 100644 --- a/src/integrations/bootloader_interface.rs +++ b/src/integrations/bootloader_interface.rs @@ -1,3 +1,4 @@ +use crate::platform::timer::PlatformTimer; use anyhow::{Context, Result}; use uefi::{CString16, Guid, guid}; use uefi_raw::table::runtime::{VariableAttributes, VariableVendor}; @@ -11,14 +12,14 @@ impl BootloaderInterface { /// Tell the system that Sprout was initialized at the current time. pub fn mark_init() -> Result<()> { - // TODO(azenla): Implement support for LoaderTimeInitUSec here. - Ok(()) + Self::set_cstr16("LoaderTimeInitUSec", "0") } /// Tell the system that Sprout is about to execute the boot entry. - pub fn mark_exec() -> Result<()> { - // TODO(azenla): Implement support for LoaderTimeExecUSec here. - Ok(()) + pub fn mark_exec(timer: &PlatformTimer) -> Result<()> { + // Measure the elapsed time since the bootloader was started. + let elapsed = timer.elapsed(); + Self::set_cstr16("LoaderTimeExecUSec", &elapsed.as_micros().to_string()) } /// Tell the system what the partition GUID of the ESP Sprout was booted from is. diff --git a/src/main.rs b/src/main.rs index f936654..7a62242 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,6 @@ #![doc = include_str!("../README.md")] #![feature(uefi_std)] +extern crate core; use crate::config::RootConfiguration; use crate::context::{RootContext, SproutContext}; @@ -8,6 +9,7 @@ use crate::integrations::bootloader_interface::BootloaderInterface; use crate::options::SproutOptions; use crate::options::parser::OptionsRepresentable; use crate::phases::phase; +use crate::platform::timer::PlatformTimer; use crate::utils::PartitionGuidForm; use anyhow::{Context, Result, bail}; use log::{error, info}; @@ -40,6 +42,9 @@ pub mod extractors; /// generators: Runtime code that can generate entries with specific values. pub mod generators; +/// platform: Integration or support code for specific hardware platforms. +pub mod platform; + /// menu: Display a boot menu to select an entry to boot. pub mod menu; @@ -60,6 +65,9 @@ pub mod utils; /// Run Sprout, returning an error if one occurs. fn run() -> Result<()> { + // Start the platform timer. + let timer = PlatformTimer::start(); + // Mark the initialization of Sprout in the bootloader interface. BootloaderInterface::mark_init() .context("unable to mark initialization in bootloader interface")?; @@ -101,7 +109,7 @@ fn run() -> Result<()> { } // Create the root context. - let mut root = RootContext::new(loaded_image_path, options); + let mut root = RootContext::new(loaded_image_path, timer, options); // Insert the configuration actions into the root context. root.actions_mut().extend(config.actions.clone()); diff --git a/src/platform.rs b/src/platform.rs new file mode 100644 index 0000000..25b5b1f --- /dev/null +++ b/src/platform.rs @@ -0,0 +1,2 @@ +/// timer: Platform timer support code. +pub mod timer; diff --git a/src/platform/timer.rs b/src/platform/timer.rs new file mode 100644 index 0000000..80e151a --- /dev/null +++ b/src/platform/timer.rs @@ -0,0 +1,81 @@ +use std::time::Duration; + +/// Support for aarch64 timers. +#[cfg(target_arch = "aarch64")] +pub mod aarch64; + +/// Support for x86_64 timers. +#[cfg(target_arch = "x86_64")] +pub mod x86_64; + +/// The tick frequency of the platform. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum TickFrequency { + /// The platform provides the tick frequency. + Hardware(u64), + /// The tick frequency is measured internally. + Measured(u64, Duration), +} + +impl TickFrequency { + /// Acquire the tick frequency reported by the platform. + fn ticks(&self) -> u64 { + match self { + TickFrequency::Hardware(frequency) => *frequency, + TickFrequency::Measured(frequency, _) => *frequency, + } + } + + /// Calculate the nanoseconds represented by a tick. + fn nanos(&self) -> f64 { + 1.0e9_f64 / (self.ticks() as f64) + } + + /// Produce a duration from the provided elapsed `ticks` value. + fn duration(&self, ticks: u64) -> Duration { + let accuracy = self.nanos(); + let nanos = ticks as f64 * accuracy; + Duration::from_nanos(nanos as u64) + } +} + +/// Acquire the tick value reported by the platform. +fn arch_ticks() -> u64 { + #[cfg(target_arch = "aarch64")] + return aarch64::ticks(); + #[cfg(target_arch = "x86_64")] + return x86_64::ticks(); +} + +/// Acquire the tick frequency reported by the platform. +fn arch_frequency() -> TickFrequency { + #[cfg(target_arch = "aarch64")] + return aarch64::frequency(); + #[cfg(target_arch = "x86_64")] + return x86_64::frequency(); +} + +/// Platform timer that allows measurement of the elapsed time. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct PlatformTimer { + /// The start tick value. + start: u64, + /// The tick frequency of the platform. + frequency: TickFrequency, +} + +impl PlatformTimer { + /// Start a platform timer at the current instant. + pub fn start() -> Self { + Self { + start: arch_ticks(), + frequency: arch_frequency(), + } + } + + /// Measure the elapsed duration since the timer was started. + pub fn elapsed(&self) -> Duration { + let duration = arch_ticks() - self.start; + self.frequency.duration(duration) + } +} diff --git a/src/platform/timer/aarch64.rs b/src/platform/timer/aarch64.rs new file mode 100644 index 0000000..46a57ca --- /dev/null +++ b/src/platform/timer/aarch64.rs @@ -0,0 +1,33 @@ +use crate::platform::timer::TickFrequency; +use std::arch::asm; + +/// Reads the cntvct_el0 counter and returns the value. +pub fn ticks() -> u64 { + let counter: u64; + unsafe { + asm!("mrs x0, cntvct_el0", out("x0") counter); + } + counter +} + +/// We can use the actual ticks value as our start value. +pub fn start() -> u64 { + ticks() +} + +/// We can use the actual ticks value as our stop value. +pub fn stop() -> u64 { + ticks() +} + +/// Our frequency is provided by cntfrq_el0 on the platform. +pub fn frequency() -> TickFrequency { + let frequency: u64; + unsafe { + asm!( + "mrs x0, cntfrq_el0", + out("x0") frequency + ); + } + TickFrequency::Hardware(frequency) +} diff --git a/src/platform/timer/x86_64.rs b/src/platform/timer/x86_64.rs new file mode 100644 index 0000000..96eb38c --- /dev/null +++ b/src/platform/timer/x86_64.rs @@ -0,0 +1,66 @@ +use crate::platform::timer::TickFrequency; +use core::arch::asm; +use std::time::Duration; + +/// We will measure the frequency of the timer based on 1000 microseconds. +/// This will result in a call to BS->Stall(1000) in the end. +const MEASURE_FREQUENCY_DURATION: Duration = Duration::from_micros(1000); + +/// Read the number of ticks from the platform timer. +pub fn ticks() -> u64 { + let mut eax: u32; + let mut edx: u32; + + unsafe { + asm!("rdtsc", out("eax") eax, out("edx") edx); + } + + (edx as u64) << 32 | eax as u64 +} + +/// Read the starting number of ticks from the platform timer. +pub fn start() -> u64 { + let rax: u64; + unsafe { + asm!( + "mfence", + "lfence", + "rdtsc", + "shl rdx, 32", + "or rax, rdx", + out("rax") rax + ); + } + rax +} + +/// Read the ending number of ticks from the platform timer. +pub fn stop() -> u64 { + let rax: u64; + unsafe { + asm!( + "rdtsc", + "lfence", + "shl rdx, 32", + "or rax, rdx", + out("rax") rax + ); + } + rax +} + +/// Measure the frequency of the platform timer. +fn measure_frequency(duration: &Duration) -> u64 { + let start = start(); + uefi::boot::stall(*duration); + let stop = stop(); + let elapsed = (stop - start) as f64; + (elapsed / duration.as_secs_f64()) as u64 +} + +/// Acquire the platform timer frequency. +/// On x86_64, this is slightly expensive, so it should be done once. +pub fn frequency() -> TickFrequency { + let frequency = measure_frequency(&MEASURE_FREQUENCY_DURATION); + TickFrequency::Measured(frequency, MEASURE_FREQUENCY_DURATION) +} From a77be3c28270ede4ea7daac86b40aca933ea1f8b Mon Sep 17 00:00:00 2001 From: Alex Zenla Date: Thu, 30 Oct 2025 02:51:52 -0400 Subject: [PATCH 4/9] feat(bootloader-interface): measure time in firmware as well --- src/integrations/bootloader_interface.rs | 16 +++++++++++----- src/main.rs | 2 +- src/platform.rs | 2 +- src/platform/timer.rs | 7 ++++++- 4 files changed, 19 insertions(+), 8 deletions(-) diff --git a/src/integrations/bootloader_interface.rs b/src/integrations/bootloader_interface.rs index b291d70..0e6cf91 100644 --- a/src/integrations/bootloader_interface.rs +++ b/src/integrations/bootloader_interface.rs @@ -11,15 +11,21 @@ impl BootloaderInterface { const VENDOR: VariableVendor = VariableVendor(guid!("4a67b082-0a4c-41cf-b6c7-440b29bb8c4f")); /// Tell the system that Sprout was initialized at the current time. - pub fn mark_init() -> Result<()> { - Self::set_cstr16("LoaderTimeInitUSec", "0") + pub fn mark_init(timer: &PlatformTimer) -> Result<()> { + Self::mark_time("LoaderTimeInitUSec", timer) } /// Tell the system that Sprout is about to execute the boot entry. pub fn mark_exec(timer: &PlatformTimer) -> Result<()> { - // Measure the elapsed time since the bootloader was started. - let elapsed = timer.elapsed(); - Self::set_cstr16("LoaderTimeExecUSec", &elapsed.as_micros().to_string()) + Self::mark_time("LoaderTimeExecUSec", timer) + } + + /// Tell the system about the current time as measured by the platform timer. + /// Sets the variable specified by `key` to the number of microseconds. + fn mark_time(key: &str, timer: &PlatformTimer) -> Result<()> { + // Measure the elapsed time since the hardware timer was started. + let elapsed = timer.elapsed_since_lifetime(); + Self::set_cstr16(key, &elapsed.as_micros().to_string()) } /// Tell the system what the partition GUID of the ESP Sprout was booted from is. diff --git a/src/main.rs b/src/main.rs index 7a62242..8385dd5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -69,7 +69,7 @@ fn run() -> Result<()> { let timer = PlatformTimer::start(); // Mark the initialization of Sprout in the bootloader interface. - BootloaderInterface::mark_init() + BootloaderInterface::mark_init(&timer) .context("unable to mark initialization in bootloader interface")?; // Parse the options to the sprout executable. diff --git a/src/platform.rs b/src/platform.rs index 25b5b1f..66d17b4 100644 --- a/src/platform.rs +++ b/src/platform.rs @@ -1,2 +1,2 @@ -/// timer: Platform timer support code. +/// timer: Platform timer support. pub mod timer; diff --git a/src/platform/timer.rs b/src/platform/timer.rs index 80e151a..9a2d9be 100644 --- a/src/platform/timer.rs +++ b/src/platform/timer.rs @@ -73,8 +73,13 @@ impl PlatformTimer { } } + /// Measure the elapsed duration since the hardware started ticking upwards. + pub fn elapsed_since_lifetime(&self) -> Duration { + self.frequency.duration(arch_ticks()) + } + /// Measure the elapsed duration since the timer was started. - pub fn elapsed(&self) -> Duration { + pub fn elapsed_since_start(&self) -> Duration { let duration = arch_ticks() - self.start; self.frequency.duration(duration) } From 37ab0406bbc646d7941445aaf5c2d35eb5ab26d1 Mon Sep 17 00:00:00 2001 From: Alex Zenla Date: Thu, 30 Oct 2025 11:47:35 -0400 Subject: [PATCH 5/9] feat(bootloader-interface): implement support for UEFI firmware information --- src/integrations/bootloader_interface.rs | 16 ++++++++++++++++ src/main.rs | 4 ++++ 2 files changed, 20 insertions(+) diff --git a/src/integrations/bootloader_interface.rs b/src/integrations/bootloader_interface.rs index 0e6cf91..18f71af 100644 --- a/src/integrations/bootloader_interface.rs +++ b/src/integrations/bootloader_interface.rs @@ -58,6 +58,22 @@ impl BootloaderInterface { Self::set_cstr16("LoaderEntrySelected", &entry) } + /// Tell the system about the UEFI firmware we are running on. + pub fn set_firmware_info() -> Result<()> { + // Format the firmware information string into something human-readable. + let firmware_info = format!( + "{} {}.{:02}", + uefi::system::firmware_vendor(), + uefi::system::firmware_revision() >> 16, + uefi::system::firmware_revision() & 0xFFFFF, + ); + Self::set_cstr16("LoaderFirmwareInfo", &firmware_info)?; + + // Format the firmware revision into something human-readable. + let firmware_type = format!("UEFI {:02}", uefi::system::firmware_revision()); + Self::set_cstr16("LoaderFirmwareType", &firmware_type) + } + /// The [VariableAttributes] for bootloader interface variables. fn attributes() -> VariableAttributes { VariableAttributes::BOOTSERVICE_ACCESS | VariableAttributes::RUNTIME_ACCESS diff --git a/src/main.rs b/src/main.rs index 8385dd5..fb9616a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -72,6 +72,10 @@ fn run() -> Result<()> { BootloaderInterface::mark_init(&timer) .context("unable to mark initialization in bootloader interface")?; + // Tell the bootloader interface what firmware we are running on. + BootloaderInterface::set_firmware_info() + .context("unable to set firmware info in bootloader interface")?; + // Parse the options to the sprout executable. let options = SproutOptions::parse().context("unable to parse options")?; From d2bef03437675d608e180697acacc8cb32f174cd Mon Sep 17 00:00:00 2001 From: Alex Zenla Date: Thu, 30 Oct 2025 12:30:08 -0400 Subject: [PATCH 6/9] fix(platform/timer): add back note of platform timer reference --- src/platform/timer.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/platform/timer.rs b/src/platform/timer.rs index 9a2d9be..e546e9c 100644 --- a/src/platform/timer.rs +++ b/src/platform/timer.rs @@ -1,3 +1,6 @@ +// Referenced https://github.com/sheroz/tick_counter (MIT license) as a baseline. +// Architecturally modified to support UEFI and remove x86 (32-bit) support. + use std::time::Duration; /// Support for aarch64 timers. From cff55322fc59863fe7ccc54af34d4a7e285a1d0a Mon Sep 17 00:00:00 2001 From: Alex Zenla Date: Thu, 30 Oct 2025 12:44:07 -0400 Subject: [PATCH 7/9] feat(bootloader-interface): implement support for LoaderImageIdentifier --- src/integrations/bootloader_interface.rs | 28 ++++++++++++++++++------ src/main.rs | 4 ++++ 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/src/integrations/bootloader_interface.rs b/src/integrations/bootloader_interface.rs index 18f71af..82351cb 100644 --- a/src/integrations/bootloader_interface.rs +++ b/src/integrations/bootloader_interface.rs @@ -1,5 +1,7 @@ use crate::platform::timer::PlatformTimer; +use crate::utils::device_path_subpath; use anyhow::{Context, Result}; +use uefi::proto::device_path::DevicePath; use uefi::{CString16, Guid, guid}; use uefi_raw::table::runtime::{VariableAttributes, VariableVendor}; @@ -28,6 +30,12 @@ impl BootloaderInterface { Self::set_cstr16(key, &elapsed.as_micros().to_string()) } + /// Tell the system the relative path to the partition root of the current bootloader. + pub fn set_loader_path(path: &DevicePath) -> Result<()> { + let subpath = device_path_subpath(path).context("unable to get loader path subpath")?; + Self::set_cstr16("LoaderImageIdentifier", &subpath) + } + /// Tell the system what the partition GUID of the ESP Sprout was booted from is. pub fn set_partition_guid(guid: &Guid) -> Result<()> { Self::set_cstr16("LoaderDevicePartUUID", &guid.to_string()) @@ -39,11 +47,14 @@ impl BootloaderInterface { // Iterate over the entries and convert them to CString16 placing them into data. let mut data = Vec::new(); for entry in entries { - // Convert the entry to a CString16. - let entry = CString16::try_from(entry.as_ref()) - .context("unable to convert entry to CString16")?; + // Convert the entry to CString16 little endian. + let encoded = entry + .as_ref() + .encode_utf16() + .flat_map(|c| c.to_le_bytes()) + .collect::>(); // Write the bytes (including the null terminator) into the data buffer. - data.extend_from_slice(entry.as_bytes()); + data.extend_from_slice(&encoded); } Self::set("LoaderEntries", &data) } @@ -91,8 +102,11 @@ impl BootloaderInterface { /// Set a bootloader interface variable specified by `key` to `value`, converting the value to /// a [CString16]. fn set_cstr16(key: &str, value: &str) -> Result<()> { - let value = - CString16::try_from(value).context("unable to convert variable value to CString16")?; - Self::set(key, value.as_bytes()) + // Encode the value as a CString16 little endian. + let encoded = value + .encode_utf16() + .flat_map(|c| c.to_le_bytes()) + .collect::>(); + Self::set(key, &encoded) } } diff --git a/src/main.rs b/src/main.rs index fb9616a..7a657f2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -112,6 +112,10 @@ fn run() -> Result<()> { .context("unable to set partition guid in bootloader interface")?; } + // Tell the bootloader interface what the loaded image path is. + BootloaderInterface::set_loader_path(&loaded_image_path) + .context("unable to set loader path in bootloader interface")?; + // Create the root context. let mut root = RootContext::new(loaded_image_path, timer, options); From cc90199d6189be958683c096ec8968b1dd6b9f00 Mon Sep 17 00:00:00 2001 From: Alex Zenla Date: Thu, 30 Oct 2025 12:50:36 -0400 Subject: [PATCH 8/9] feat(bootloader-interface): identify ourselves as sprout --- src/integrations/bootloader_interface.rs | 8 ++++++++ src/main.rs | 4 ++++ 2 files changed, 12 insertions(+) diff --git a/src/integrations/bootloader_interface.rs b/src/integrations/bootloader_interface.rs index 82351cb..2581f19 100644 --- a/src/integrations/bootloader_interface.rs +++ b/src/integrations/bootloader_interface.rs @@ -5,6 +5,9 @@ use uefi::proto::device_path::DevicePath; use uefi::{CString16, Guid, guid}; use uefi_raw::table::runtime::{VariableAttributes, VariableVendor}; +/// The name of the bootloader to tell the system. +const LOADER_NAME: &str = "Sprout"; + /// Bootloader Interface support. pub struct BootloaderInterface; @@ -30,6 +33,11 @@ impl BootloaderInterface { Self::set_cstr16(key, &elapsed.as_micros().to_string()) } + /// Tell the system what loader is being used. + pub fn set_loader_info() -> Result<()> { + Self::set_cstr16("LoaderInfo", LOADER_NAME) + } + /// Tell the system the relative path to the partition root of the current bootloader. pub fn set_loader_path(path: &DevicePath) -> Result<()> { let subpath = device_path_subpath(path).context("unable to get loader path subpath")?; diff --git a/src/main.rs b/src/main.rs index 7a657f2..15579c5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -76,6 +76,10 @@ fn run() -> Result<()> { BootloaderInterface::set_firmware_info() .context("unable to set firmware info in bootloader interface")?; + // Tell the bootloader interface what loader is being used. + BootloaderInterface::set_loader_info() + .context("unable to set loader info in bootloader interface")?; + // Parse the options to the sprout executable. let options = SproutOptions::parse().context("unable to parse options")?; From 9d3a022e084eaec8875a22a55ff7ed6d7a6ff3bb Mon Sep 17 00:00:00 2001 From: Alex Zenla Date: Thu, 30 Oct 2025 13:27:58 -0400 Subject: [PATCH 9/9] feat(bootloader-interface): add support for marking when the menu is being display --- src/integrations/bootloader_interface.rs | 5 +++++ src/main.rs | 3 ++- src/menu.rs | 12 +++++++++++- 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/integrations/bootloader_interface.rs b/src/integrations/bootloader_interface.rs index 2581f19..d1fd77a 100644 --- a/src/integrations/bootloader_interface.rs +++ b/src/integrations/bootloader_interface.rs @@ -25,6 +25,11 @@ impl BootloaderInterface { Self::mark_time("LoaderTimeExecUSec", timer) } + /// Tell the system that Sprout is about to display the menu. + pub fn mark_menu(timer: &PlatformTimer) -> Result<()> { + Self::mark_time("LoaderTimeMenuUsec", timer) + } + /// Tell the system about the current time as measured by the platform timer. /// Sets the variable specified by `key` to the number of microseconds. fn mark_time(key: &str, timer: &PlatformTimer) -> Result<()> { diff --git a/src/main.rs b/src/main.rs index 15579c5..13d030b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -285,7 +285,8 @@ fn run() -> Result<()> { .context(format!("unable to find entry: {force_boot_entry}"))? } else { // Delegate to the menu to select an entry to boot. - menu::select(menu_timeout, &entries).context("unable to select entry via boot menu")? + menu::select(&timer, menu_timeout, &entries) + .context("unable to select entry via boot menu")? }; // Tell the bootloader interface what the selected entry is. diff --git a/src/menu.rs b/src/menu.rs index baa7ea0..a735bb1 100644 --- a/src/menu.rs +++ b/src/menu.rs @@ -1,4 +1,6 @@ use crate::entries::BootableEntry; +use crate::integrations::bootloader_interface::BootloaderInterface; +use crate::platform::timer::PlatformTimer; use anyhow::{Context, Result, bail}; use log::info; use std::time::Duration; @@ -162,7 +164,15 @@ fn select_with_input<'a>( /// Shows a boot menu to select a bootable entry to boot. /// The actual work is done internally in [select_with_input] which is called /// within the context of the standard input device. -pub fn select(timeout: Duration, entries: &[BootableEntry]) -> Result<&BootableEntry> { +pub fn select<'live>( + timer: &'live PlatformTimer, + timeout: Duration, + entries: &'live [BootableEntry], +) -> Result<&'live BootableEntry> { + // Notify the bootloader interface that we are about to display the menu. + BootloaderInterface::mark_menu(timer) + .context("unable to mark menu display in bootloader interface")?; + // Acquire the standard input device and run the boot menu. uefi::system::with_stdin(move |input| select_with_input(input, timeout, entries)) }