diff --git a/Cargo.toml b/Cargo.toml index 105d86c99..5febbd121 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,6 +42,7 @@ patina_lzma_rs = { version = "0.3.1", default-features = false } patina_macro = { version = "21.0.0", path = "sdk/patina_macro" } patina_mm = { version = "21.0.0", path = "components/patina_mm" } patina_mtrr = { version = "^1.1.4" } +patina_boot = { version = "19.0.5", path = "components/patina_boot" } patina_paging = { version = "11.0.2" } patina_performance = { version = "21.0.0", path = "components/patina_performance" } patina_smbios = { version = "21.0.0", path = "components/patina_smbios" } diff --git a/components/patina_boot/Cargo.toml b/components/patina_boot/Cargo.toml new file mode 100644 index 000000000..14ab85e11 --- /dev/null +++ b/components/patina_boot/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "patina_boot" +resolver = "2" +version.workspace = true +repository.workspace = true +license.workspace = true +edition.workspace = true +description = "Boot orchestration components for Patina firmware." + +[lints] +workspace = true + +[dependencies] +log = { workspace = true } +patina = { workspace = true, features = ["unstable-device-path"] } +patina_macro = { workspace = true } +r-efi = { workspace = true } +spin = { workspace = true } +zerocopy = { workspace = true } +zerocopy-derive = { workspace = true } + +[dev-dependencies] +patina = { path = "../../sdk/patina", features = ["mockall", "unstable-device-path"] } +mockall = { workspace = true } + +[features] +default = [] +std = [] diff --git a/components/patina_boot/README.md b/components/patina_boot/README.md new file mode 100644 index 000000000..f2439b741 --- /dev/null +++ b/components/patina_boot/README.md @@ -0,0 +1,35 @@ +# Patina Boot + +Boot orchestration component for Patina-based firmware implementing UEFI boot manager functionality. + +## Components + +- **BootDispatcher**: Installs the BDS architectural protocol and delegates to a `BootOrchestrator` implementation. +- **BootOrchestrator**: Trait for custom boot flows. Platforms implement this to define boot behavior. +- **SimpleBootManager**: Default `BootOrchestrator` for platforms with straightforward boot topologies. + +## Usage + +```rust +use patina_boot::{BootDispatcher, SimpleBootManager, config::BootConfig}; + +// Minimal boot: +let orchestrator = SimpleBootManager::new( + BootConfig::new(nvme_esp_path()) + .with_device(nvme_recovery_path()), +); +add.component(BootDispatcher::new(orchestrator)); + +// Custom orchestrator: +add.component(BootDispatcher::new(MyCustomOrchestrator::new())); +``` + +## Helper Functions + +For custom boot flows, use the helper functions in the `helpers` module: + +- `connect_all()` - Connect all controllers for device enumeration +- `signal_bds_phase_entry()` - Signal EndOfDxe event +- `signal_ready_to_boot()` - Signal ReadyToBoot event +- `discover_console_devices()` - Populate console variables +- `boot_from_device_path()` - Load and start a boot image diff --git a/components/patina_boot/src/boot_dispatcher.rs b/components/patina_boot/src/boot_dispatcher.rs new file mode 100644 index 000000000..0eb74bf65 --- /dev/null +++ b/components/patina_boot/src/boot_dispatcher.rs @@ -0,0 +1,183 @@ +//! Boot dispatcher component. +//! +//! [`BootDispatcher`] is the Patina component that installs the BDS architectural +//! protocol and delegates to a [`BootOrchestrator`] +//! implementation when invoked by the DXE core. +//! +//! ## License +//! +//! Copyright (c) Microsoft Corporation. +//! +//! SPDX-License-Identifier: Apache-2.0 +//! +extern crate alloc; + +use alloc::boxed::Box; +use core::ffi::c_void; + +use patina::{ + boot_services::{BootServices, StandardBootServices}, + component::{ + component, + params::Handle, + service::{Service, dxe_dispatch::DxeDispatch}, + }, + error::{EfiError, Result}, + pi::protocols::bds, + runtime_services::StandardRuntimeServices, +}; +use spin::Once; + +use crate::boot_orchestrator::BootOrchestrator; + +/// Context stored in a static for the BDS protocol callback to access. +struct BdsContext { + orchestrator: Box, + dxe_dispatch: &'static dyn DxeDispatch, + boot_services: StandardBootServices, + runtime_services: StandardRuntimeServices, + image_handle: r_efi::efi::Handle, +} + +// SAFETY: BdsContext is only accessed from the BDS entry point which runs on the +// BSP (Bootstrap Processor) at TPL_APPLICATION. UEFI is single-threaded at this point. +unsafe impl Send for BdsContext {} +// SAFETY: BdsContext is stored in a spin::Once and only read after initialization. +// Access is single-threaded (BSP at TPL_APPLICATION during BDS phase). +unsafe impl Sync for BdsContext {} + +/// Static storage for the BDS context. Initialized once during component dispatch, +/// consumed once when the DXE core invokes the BDS protocol. +static BDS_CONTEXT: Once = Once::new(); + +/// Boot dispatcher component. +/// +/// This is the single Patina component for driving boot orchestration. It: +/// - Accepts a [`BootOrchestrator`] implementation via [`BootDispatcher::new()`] +/// - Installs the BDS architectural protocol during component dispatch +/// - Consumes the [`DxeDispatch`] service via dependency injection +/// - When the DXE core invokes BDS: delegates to `orchestrator.execute()` +/// +/// ## Usage +/// +/// ```rust,ignore +/// use patina_boot::{BootDispatcher, SimpleBootManager, config::BootConfig}; +/// +/// // Minimal boot: +/// add.component(BootDispatcher::new(SimpleBootManager::new( +/// BootConfig::new(nvme_esp_path()) +/// .with_device(nvme_recovery_path()), +/// ))); +/// +/// // Custom orchestrator: +/// add.component(BootDispatcher::new(MyCustomOrchestrator::new())); +/// ``` +pub struct BootDispatcher { + orchestrator: Box, +} + +impl BootDispatcher { + /// Create a new `BootDispatcher` with the given orchestrator. + /// + /// The orchestrator is boxed internally — callers pass any type that + /// implements [`BootOrchestrator`]. + pub fn new(orchestrator: impl BootOrchestrator) -> Self { + Self { orchestrator: Box::new(orchestrator) } + } +} + +#[component] +impl BootDispatcher { + /// Entry point: stores context and installs the BDS architectural protocol. + /// + /// The actual boot flow does not execute here. It executes later when the + /// DXE core calls `bds_entry_point` after all architectural protocols are + /// satisfied and all DXE drivers have been dispatched. + #[coverage(off)] // Component integration — tested via integration tests + fn entry_point( + self, + boot_services: StandardBootServices, + runtime_services: StandardRuntimeServices, + dxe_dispatch: Service, + image_handle: Option, + ) -> Result<()> { + let handle = image_handle.ok_or_else(|| { + log::error!("Handle not provided — required for LoadImage parent handle"); + EfiError::InvalidParameter + })?; + + // Store the orchestrator and services for the BDS callback + BDS_CONTEXT.call_once(|| BdsContext { + orchestrator: self.orchestrator, + dxe_dispatch: *dxe_dispatch, + boot_services: boot_services.clone(), + runtime_services: runtime_services.clone(), + image_handle: *handle, + }); + + // Install the BDS architectural protocol + let protocol = Box::leak(Box::new(bds::Protocol { entry: bds_entry_point })); + + // SAFETY: protocol is a valid, leaked BDS protocol struct with a valid entry function pointer. + // Using unchecked variant because bds::Protocol does not implement ProtocolInterface. + unsafe { + boot_services.as_ref().install_protocol_interface_unchecked( + None, + &bds::PROTOCOL_GUID, + protocol as *mut _ as *mut c_void, + ) + } + .map_err(EfiError::from)?; + + Ok(()) + } +} + +/// BDS architectural protocol entry point. +/// +/// Called by the DXE core after all architectural protocols are installed and +/// all DXE drivers have been dispatched. Retrieves the stored context and +/// delegates to the orchestrator. +#[coverage(off)] // Extern "efiapi" callback — tested via integration tests +extern "efiapi" fn bds_entry_point(_this: *mut bds::Protocol) { + let Some(context) = BDS_CONTEXT.get() else { + panic!("BDS context not initialized — BootDispatcher entry_point was not called"); + }; + + let Err(e) = context.orchestrator.execute( + &context.boot_services, + &context.runtime_services, + context.dxe_dispatch, + context.image_handle, + ); + panic!("BootOrchestrator::execute() failed: {e:?}"); +} + +#[cfg(test)] +mod tests { + use super::*; + use patina::{ + boot_services::StandardBootServices, component::service::dxe_dispatch::DxeDispatch, + runtime_services::StandardRuntimeServices, + }; + use r_efi::efi; + + struct MockOrchestrator; + + impl BootOrchestrator for MockOrchestrator { + fn execute( + &self, + _boot_services: &StandardBootServices, + _runtime_services: &StandardRuntimeServices, + _dxe_dispatch: &dyn DxeDispatch, + _image_handle: efi::Handle, + ) -> core::result::Result { + Err(patina::error::EfiError::NotFound) + } + } + + #[test] + fn test_new_boot_dispatcher() { + let _dispatcher = BootDispatcher::new(MockOrchestrator); + } +} diff --git a/components/patina_boot/src/boot_orchestrator.rs b/components/patina_boot/src/boot_orchestrator.rs new file mode 100644 index 000000000..4132e216b --- /dev/null +++ b/components/patina_boot/src/boot_orchestrator.rs @@ -0,0 +1,75 @@ +//! Boot orchestrator trait definition. +//! +//! Defines the [`BootOrchestrator`] trait that platforms implement to customize +//! boot behavior. The [`BootDispatcher`](crate::BootDispatcher) component holds +//! a `Box` and delegates to it when the DXE core invokes +//! the BDS architectural protocol. +//! +//! ## License +//! +//! Copyright (c) Microsoft Corporation. +//! +//! SPDX-License-Identifier: Apache-2.0 +//! +use patina::{ + boot_services::StandardBootServices, component::service::dxe_dispatch::DxeDispatch, error::EfiError, + runtime_services::StandardRuntimeServices, +}; +use r_efi::efi; + +/// Trait for boot orchestration. +/// +/// Platforms implement this trait to define custom boot flows. The implementation +/// is passed to [`BootDispatcher::new()`](crate::BootDispatcher::new) and invoked +/// when the DXE core calls the BDS architectural protocol entry point. +/// +/// ## Built-in Implementation +/// +/// [`SimpleBootManager`](crate::SimpleBootManager) provides a default implementation +/// for platforms with straightforward boot topologies (primary/secondary devices, +/// optional hotkey). +/// +/// ## Custom Implementation +/// +/// ```rust,ignore +/// use patina_boot::BootOrchestrator; +/// +/// struct MyCustomBoot { /* ... */ } +/// +/// impl BootOrchestrator for MyCustomBoot { +/// fn execute( +/// &self, +/// boot_services: &StandardBootServices, +/// runtime_services: &StandardRuntimeServices, +/// dxe_services: &dyn DxeDispatch, +/// image_handle: efi::Handle, +/// ) -> Result { +/// // Custom boot flow... +/// // Return Err if all boot options are exhausted +/// Err(EfiError::NotFound) +/// } +/// } +/// ``` +pub trait BootOrchestrator: Send + Sync + 'static { + /// Execute the boot flow. + /// + /// Called by [`BootDispatcher`](crate::BootDispatcher) when the DXE core invokes + /// the BDS architectural protocol. This method should: + /// + /// 1. Enumerate devices (e.g., `connect_all()`) + /// 2. Signal BDS phase events (EndOfDxe, ReadyToBoot) + /// 3. Attempt to boot from configured device paths + /// 4. Handle boot failures + /// + /// A successful boot transfers control to the boot image and never returns. + /// If all boot options are exhausted, the implementation returns + /// `Err(EfiError)`. The `Ok` variant is uninhabitable (`!`), enforcing at + /// the type level that this method can only "succeed" by not returning. + fn execute( + &self, + boot_services: &StandardBootServices, + runtime_services: &StandardRuntimeServices, + dxe_services: &dyn DxeDispatch, + image_handle: efi::Handle, + ) -> Result; +} diff --git a/components/patina_boot/src/config.rs b/components/patina_boot/src/config.rs new file mode 100644 index 000000000..928a18be2 --- /dev/null +++ b/components/patina_boot/src/config.rs @@ -0,0 +1,206 @@ +//! Boot configuration types. +//! +//! [`BootConfig`] provides the platform boot configuration used by +//! [`BootOrchestrator`](crate::BootOrchestrator) implementations. +//! +//! ## License +//! +//! Copyright (c) Microsoft Corporation. +//! +//! SPDX-License-Identifier: Apache-2.0 +//! +extern crate alloc; + +use alloc::{boxed::Box, vec::Vec}; +use patina::device_path::paths::DevicePathBuf; + +/// Boot options provided by the platform. +/// +/// Platforms configure boot behavior by providing this configuration to the +/// [`SimpleBootManager`](crate::SimpleBootManager). +/// +/// ## Example +/// +/// ```rust,ignore +/// use patina_boot::config::BootConfig; +/// +/// let config = BootConfig::new(nvme_device_path) +/// .with_device(usb_device_path) +/// .with_hotkey(0x16) // F12 (UEFI scan code) +/// .with_hotkey_device(usb_device_path) // Boot from USB when F12 pressed +/// .with_failure_handler(|| show_error_screen()); +/// ``` +pub struct BootConfig { + /// Boot device paths in priority order. + devices: Vec, + /// Optional hotkey for boot override (e.g., F12 for boot menu). + hotkey: Option, + /// Alternate boot device paths used when hotkey is detected. + hotkey_devices: Vec, + /// Handler called when all boot options fail. + failure_handler: Option>, +} + +impl BootConfig { + /// Create a new boot configuration with an initial boot device. + /// + /// At least one boot device is required. Additional devices can be added + /// with [`with_device`](Self::with_device). + pub fn new(device: DevicePathBuf) -> Self { + Self { devices: alloc::vec![device], hotkey: None, hotkey_devices: Vec::new(), failure_handler: None } + } + + /// Add a boot device path. + /// + /// Device paths are tried in the order they are added. + pub fn with_device(mut self, device: DevicePathBuf) -> Self { + self.devices.push(device); + self + } + + /// Add a hotkey scancode for boot override. + /// + /// When this hotkey is detected during boot, the orchestrator will use + /// the alternate boot options configured via [`with_hotkey_device`](Self::with_hotkey_device) + /// instead of the primary boot devices. + /// + /// Note: Hotkey detection reads and consumes all pending keystrokes from the + /// keyboard buffer. Any keys pressed before detection will not be available + /// to subsequent code. + pub fn with_hotkey(mut self, scancode: u16) -> Self { + self.hotkey = Some(scancode); + self + } + + /// Add an alternate boot device path used when the hotkey is detected. + /// + /// Hotkey devices are tried in the order they are added, but only when + /// the configured hotkey is detected during boot. + pub fn with_hotkey_device(mut self, device: DevicePathBuf) -> Self { + self.hotkey_devices.push(device); + self + } + + /// Add a failure handler called when all boot options fail. + pub fn with_failure_handler(mut self, handler: F) -> Self + where + F: Fn() + Send + Sync + 'static, + { + self.failure_handler = Some(Box::new(handler)); + self + } + + /// Get the hotkey scancode, if configured. + pub fn hotkey(&self) -> Option { + self.hotkey + } + + /// Returns an iterator over all configured boot device paths. + pub fn devices(&self) -> impl Iterator { + self.devices.iter() + } + + /// Returns an iterator over alternate boot device paths used when hotkey is detected. + pub fn hotkey_devices(&self) -> impl Iterator { + self.hotkey_devices.iter() + } + + /// Call the failure handler if configured. + /// + /// This is called when all boot options have been exhausted. + pub fn handle_failure(&self) { + if let Some(handler) = &self.failure_handler { + handler(); + } + } +} + +#[cfg(test)] +mod tests { + extern crate alloc; + extern crate std; + + use super::*; + use alloc::sync::Arc; + use core::sync::atomic::{AtomicBool, Ordering}; + use patina::device_path::node_defs::EndEntire; + + fn create_test_device_path() -> DevicePathBuf { + DevicePathBuf::from_device_path_node_iter(core::iter::once(EndEntire)) + } + + #[test] + fn test_new_requires_device() { + let config = BootConfig::new(create_test_device_path()); + assert_eq!(config.devices().count(), 1); + assert!(config.hotkey().is_none()); + assert_eq!(config.hotkey_devices().count(), 0); + } + + #[test] + fn test_with_additional_devices() { + let config = BootConfig::new(create_test_device_path()) + .with_device(create_test_device_path()) + .with_device(create_test_device_path()); + assert_eq!(config.devices().count(), 3); + } + + #[test] + fn test_with_hotkey() { + let config = BootConfig::new(create_test_device_path()).with_hotkey(0x16); // F12 + assert_eq!(config.hotkey(), Some(0x16)); + } + + #[test] + fn test_with_hotkey_device() { + let config = BootConfig::new(create_test_device_path()).with_hotkey_device(create_test_device_path()); + assert_eq!(config.hotkey_devices().count(), 1); + } + + #[test] + fn test_with_multiple_hotkey_devices() { + let config = BootConfig::new(create_test_device_path()) + .with_hotkey_device(create_test_device_path()) + .with_hotkey_device(create_test_device_path()); + assert_eq!(config.hotkey_devices().count(), 2); + } + + #[test] + fn test_hotkey_with_hotkey_devices() { + let config = BootConfig::new(create_test_device_path()) + .with_hotkey(0x16) // F12 + .with_hotkey_device(create_test_device_path()); + + assert_eq!(config.devices().count(), 1); + assert_eq!(config.hotkey(), Some(0x16)); + assert_eq!(config.hotkey_devices().count(), 1); + } + + #[test] + fn test_failure_handler_called() { + let called = Arc::new(AtomicBool::new(false)); + let called_clone = called.clone(); + + let config = BootConfig::new(create_test_device_path()).with_failure_handler(move || { + called_clone.store(true, Ordering::SeqCst); + }); + + assert!(!called.load(Ordering::SeqCst)); + config.handle_failure(); + assert!(called.load(Ordering::SeqCst)); + } + + #[test] + fn test_failure_handler_not_configured() { + let config = BootConfig::new(create_test_device_path()); + // Should not panic when no handler is configured + config.handle_failure(); + } + + #[test] + fn test_devices_iterator_order() { + let config = BootConfig::new(create_test_device_path()).with_device(create_test_device_path()); + let devices: Vec<_> = config.devices().collect(); + assert_eq!(devices.len(), 2); + } +} diff --git a/components/patina_boot/src/helpers.rs b/components/patina_boot/src/helpers.rs new file mode 100644 index 000000000..1538f96f0 --- /dev/null +++ b/components/patina_boot/src/helpers.rs @@ -0,0 +1,1528 @@ +//! Helper functions for boot orchestration. +//! +//! This module provides helper functions for platforms implementing custom boot flows. +//! The [`SimpleBootManager`](crate::SimpleBootManager) uses these internally, and +//! platforms can use them directly for custom orchestration. +//! +//! ## License +//! +//! Copyright (c) Microsoft Corporation. +//! +//! SPDX-License-Identifier: Apache-2.0 +//! +extern crate alloc; + +use alloc::vec::Vec; +use core::ptr; + +use patina::{ + boot_services::{BootServices, event::EventType, protocol_handler::HandleSearchType, tpl::Tpl}, + device_path::{ + node_defs::{DevicePathType, HardDrive, MediaSubType}, + paths::{DevicePath, DevicePathBuf}, + }, + error::{EfiError, Result}, + guids::{EFI_GLOBAL_VARIABLE, EVENT_GROUP_END_OF_DXE}, + runtime_services::RuntimeServices, +}; +use r_efi::{ + efi, + protocols::simple_text_input, + system::{EVENT_GROUP_READY_TO_BOOT, VARIABLE_BOOTSERVICE_ACCESS, VARIABLE_NON_VOLATILE, VARIABLE_RUNTIME_ACCESS}, +}; + +/// Watchdog timeout in seconds per UEFI Specification Section 3.1.2. +const WATCHDOG_TIMEOUT_SECONDS: usize = 300; // 5 minutes + +/// Check if a hotkey was pressed during boot. +/// +/// Reads any pending keystrokes from all SimpleTextInput protocol instances +/// and returns `true` if any key matches the specified scancode. +/// +/// This is a non-blocking check that consumes any buffered keystrokes. +/// +/// # Arguments +/// +/// * `boot_services` - Boot services interface +/// * `hotkey_scancode` - The scancode to check for (e.g., 0x16 for F12) +/// +/// # Returns +/// +/// Returns `true` if the hotkey was detected, `false` otherwise. +pub fn detect_hotkey(boot_services: &B, hotkey_scancode: u16) -> bool { + // Locate all SimpleTextInput handles + let handles = + match boot_services.locate_handle_buffer(HandleSearchType::ByProtocol(&simple_text_input::PROTOCOL_GUID)) { + Ok(handles) => handles, + Err(_) => return false, + }; + + // SAFETY: Handles are valid from locate_handle_buffer, protocol_ptr is valid from handle_protocol + unsafe { detect_hotkey_from_handles(boot_services, &handles, hotkey_scancode) } +} + +/// Inner hotkey detection loop over handles. +/// +/// This function is separated from `detect_hotkey` because it uses raw protocol +/// function pointers that cannot be unit tested with mocks. Integration tests +/// verify this code path on real hardware/emulators. +/// +/// # Safety +/// +/// - `handles` must contain valid handles obtained from `locate_handle_buffer` +/// - Each handle must support the `SimpleTextInput` protocol for `handle_protocol` to succeed +unsafe fn detect_hotkey_from_handles( + boot_services: &B, + handles: &[efi::Handle], + hotkey_scancode: u16, +) -> bool { + for &handle in handles.iter() { + // Get the protocol interface for this handle + // SAFETY: handle is valid per function contract (from locate_handle_buffer) + let protocol_ptr = match unsafe { boot_services.handle_protocol::(handle) } { + Ok(ptr) => ptr, + Err(_) => continue, + }; + + // Read any pending keystrokes (non-blocking) + // The protocol will return NOT_READY if no key is available + loop { + let mut key = simple_text_input::InputKey::default(); + let status = (protocol_ptr.read_key_stroke)(protocol_ptr, &mut key); + + if status == efi::Status::SUCCESS { + if key.scan_code == hotkey_scancode { + return true; + } + // Key didn't match, continue reading to drain buffer + } else { + // NOT_READY or error - no more keys in buffer + break; + } + } + } + + false +} + +/// Load and start a boot image with UEFI spec compliance. +/// +/// Enables a 5-minute watchdog timer before `StartImage()` per UEFI Specification +/// Section 3.1.2. Disables watchdog when boot returns control. +/// +/// # Arguments +/// +/// * `boot_services` - Boot services interface +/// * `parent_handle` - Parent image handle for the loaded image (typically the calling image's handle) +/// * `device_path` - Device path to the boot image +/// +/// # Returns +/// +/// Returns `Ok(())` if the boot image was successfully started (which typically +/// means it returned control). Returns an error if loading or starting fails. +pub fn boot_from_device_path( + boot_services: &B, + parent_handle: efi::Handle, + device_path: &DevicePathBuf, +) -> Result<()> { + // Expand partial device paths to full paths + let full_path = if is_partial_device_path(device_path.as_ref()) { + expand_device_path(boot_services, device_path.as_ref())? + } else { + device_path.clone() + }; + + // Load the image + let device_path_ptr = full_path.as_ref() as *const _ as *mut efi::protocols::device_path::Protocol; + let image_handle = match boot_services.load_image(true, parent_handle, device_path_ptr, None) { + Ok(handle) => handle, + Err(status) => { + log::error!("LoadImage failed with status: {:?}", status); + return Err(EfiError::from(status)); + } + }; + + // Enable 5-minute watchdog timer per UEFI spec Section 3.1.2 + boot_services.set_watchdog_timer(WATCHDOG_TIMEOUT_SECONDS).map_err(EfiError::from)?; + + // Start the image + let result = boot_services.start_image(image_handle); + + // Disable watchdog timer when boot option returns control + let _ = boot_services.set_watchdog_timer(0); + + match result { + Ok(()) => Ok(()), + Err((status, _exit_data)) => Err(EfiError::from(status)), + } +} + +/// Connect all controllers recursively for device enumeration. +/// +/// Connects all handles in the system recursively until the device topology +/// stabilizes (no new handles are created). +/// +/// # Arguments +/// +/// * `boot_services` - Boot services interface +/// +/// # Returns +/// +/// Returns `Ok(())` when device topology enumeration is complete. +/// +pub fn connect_all(boot_services: &B) -> Result<()> { + // Loop until the number of handles stabilizes, indicating device topology is complete. + // This is needed because connecting a PCI bus creates new handles for PCI devices, + // which then need to be connected to bind drivers like NVMe, which creates namespace + // handles, etc. + const MAX_ITERATIONS: usize = 10; + let mut prev_handle_count = 0; + + for _iteration in 0..MAX_ITERATIONS { + // Get all handles in the system + let handles = boot_services.locate_handle_buffer(HandleSearchType::AllHandle).map_err(EfiError::from)?; + let current_handle_count = handles.len(); + + // Connect each handle recursively + for &handle in handles.iter() { + // SAFETY: Empty driver handle list and null device path are valid per UEFI spec + let _ = unsafe { boot_services.connect_controller(handle, Vec::new(), ptr::null_mut(), true) }; + } + + // Check if handle count has stabilized + if current_handle_count == prev_handle_count { + break; + } + + prev_handle_count = current_handle_count; + } + + Ok(()) +} + +/// Signal EndOfDxe event for platforms implementing custom orchestration. +/// +/// Signals `gEfiEndOfDxeEventGroupGuid` to notify security components that +/// DXE phase initialization is complete. Security components (e.g., SMM/MM) +/// register for this event and perform lockdown. +/// +/// # Arguments +/// +/// * `boot_services` - Boot services interface +pub fn signal_bds_phase_entry(boot_services: &B) -> Result<()> { + // Create and signal EndOfDxe event + // SAFETY: Null context is valid for signal-only events + let event = unsafe { + boot_services.create_event_ex_unchecked::<()>( + EventType::NOTIFY_SIGNAL, + Tpl::CALLBACK, + signal_event_noop, + ptr::null_mut(), + &EVENT_GROUP_END_OF_DXE, + ) + } + .map_err(EfiError::from)?; + + let signal_result = boot_services.signal_event(event); + // Always close the event, even if signal failed + let close_result = boot_services.close_event(event); + + signal_result.map_err(EfiError::from)?; + close_result.map_err(EfiError::from)?; + + Ok(()) +} + +/// Signal ReadyToBoot event for platforms implementing custom orchestration. +/// +/// Signals `gEfiEventReadyToBootGuid` immediately before attempting the first +/// boot option. This event notifies drivers that boot is imminent. +/// +/// # Arguments +/// +/// * `boot_services` - Boot services interface +pub fn signal_ready_to_boot(boot_services: &B) -> Result<()> { + // Create and signal ReadyToBoot event + // SAFETY: Null context is valid for signal-only events + let event = unsafe { + boot_services.create_event_ex_unchecked::<()>( + EventType::NOTIFY_SIGNAL, + Tpl::CALLBACK, + signal_event_noop, + ptr::null_mut(), + &EVENT_GROUP_READY_TO_BOOT, + ) + } + .map_err(EfiError::from)?; + + let signal_result = boot_services.signal_event(event); + // Always close the event, even if signal failed + let close_result = boot_services.close_event(event); + + signal_result.map_err(EfiError::from)?; + close_result.map_err(EfiError::from)?; + + Ok(()) +} + +/// Discover console devices and write ConIn, ConOut, and ErrOut UEFI variables. +/// +/// Enumerates all handles supporting console-related protocols (SimpleTextInput, +/// SimpleTextOutput, GraphicsOutput) and writes multi-instance device paths to the +/// corresponding UEFI global variables. These variables allow UEFI applications and +/// OS loaders to discover available console devices. +/// +/// Individual variable failures are non-fatal — the function logs a warning and +/// continues with the remaining variables. +/// +/// # Arguments +/// +/// * `boot_services` - Boot services interface for handle enumeration +/// * `runtime_services` - Runtime services interface for writing UEFI variables +pub fn discover_console_devices( + boot_services: &B, + runtime_services: &R, +) -> Result<()> { + let attrs = VARIABLE_NON_VOLATILE | VARIABLE_BOOTSERVICE_ACCESS | VARIABLE_RUNTIME_ACCESS; + + // UTF-16 null-terminated variable names + let con_in_name: &[u16] = &[b'C' as u16, b'o' as u16, b'n' as u16, b'I' as u16, b'n' as u16, 0]; + let con_out_name: &[u16] = &[b'C' as u16, b'o' as u16, b'n' as u16, b'O' as u16, b'u' as u16, b't' as u16, 0]; + let err_out_name: &[u16] = &[b'E' as u16, b'r' as u16, b'r' as u16, b'O' as u16, b'u' as u16, b't' as u16, 0]; + + let console_vars: &[(&str, &[u16], &[&'static efi::Guid])] = &[ + ("ConIn", con_in_name, &[&simple_text_input::PROTOCOL_GUID]), + ( + "ConOut", + con_out_name, + &[&efi::protocols::simple_text_output::PROTOCOL_GUID, &efi::protocols::graphics_output::PROTOCOL_GUID], + ), + ("ErrOut", err_out_name, &[&efi::protocols::simple_text_output::PROTOCOL_GUID]), + ]; + + for &(label, name, guids) in console_vars { + let device_path = build_multi_instance_device_path(boot_services, guids); + + if let Some(dp) = device_path { + let bytes = dp.as_ref().as_bytes().to_vec(); + + if let Err(e) = runtime_services.set_variable(name, &EFI_GLOBAL_VARIABLE, attrs, &bytes) { + log::error!("{label}: failed to set variable: {e:?}"); + } + } + } + + Ok(()) +} + +/// Build a multi-instance device path from all handles supporting the given protocols. +/// +/// For each protocol GUID, locates all handles via `locate_handle_buffer` and extracts +/// their device paths, combining them into a single multi-instance device path separated +/// by `EndInstance` nodes. +/// +fn build_multi_instance_device_path( + boot_services: &B, + protocol_guids: &[&'static efi::Guid], +) -> Option { + let mut result: Option = None; + let mut seen_handles: Vec = Vec::new(); + + for &guid in protocol_guids { + let handles = match boot_services.locate_handle_buffer(HandleSearchType::ByProtocol(guid)) { + Ok(handles) => handles, + Err(_) => continue, + }; + + for &handle in handles.iter() { + if seen_handles.contains(&handle) { + continue; + } + seen_handles.push(handle); + // SAFETY: handle is valid from locate_handle_buffer, requesting device path protocol. + let dp_ptr = match unsafe { boot_services.handle_protocol::(handle) } + { + Ok(ptr) => ptr, + Err(_) => continue, + }; + + // SAFETY: The device path pointer comes from a valid protocol interface. + let device_path = match unsafe { DevicePath::try_from_ptr(dp_ptr as *const _ as *const u8) } { + Ok(dp) => dp, + Err(_) => continue, + }; + + match &mut result { + Some(multi) => multi.append_device_path_instances(device_path), + None => result = Some(DevicePathBuf::from(device_path)), + } + } + } + + result +} + +/// No-op event callback for signal-only events. +#[coverage(off)] // Extern callback - tested via integration tests +extern "efiapi" fn signal_event_noop(_event: *mut core::ffi::c_void, _context: *mut ()) {} + +/// Returns true if the device path is a partial (short-form) device path. +/// +/// Full device paths start with Hardware (type 1) or ACPI (type 2) root nodes, +/// representing the complete path from system root to device. +/// +/// Partial device paths start with other node types (e.g., Media type 4 for HD nodes, +/// Messaging type 3 for NVMe without root) and must be expanded by matching against +/// the current device topology before they can be used for booting. +/// +/// # Arguments +/// +/// * `device_path` - The device path to check +/// +/// # Returns +/// +/// `true` if the device path is partial (does not start with Hardware or ACPI node), +/// `false` if it's a full device path or empty. +pub fn is_partial_device_path(device_path: &DevicePath) -> bool { + let Some(first_node) = device_path.iter().next() else { + return false; + }; + + // Full paths start with Hardware (1) or ACPI (2) nodes. + // Media FV/FvFile paths are also complete — LoadImage resolves them directly. + // Partial paths start with Media HardDrive (4/1), Messaging (3), or other nodes. + let node_type = first_node.header.r#type; + let node_subtype = first_node.header.sub_type; + + if node_type == DevicePathType::Media as u8 + && (node_subtype == MediaSubType::PiwgFirmwareFile as u8 + || node_subtype == MediaSubType::PiwgFirmwareVolume as u8) + { + return false; + } + + node_type != DevicePathType::Hardware as u8 + && node_type != DevicePathType::Acpi as u8 + && node_type != DevicePathType::End as u8 +} + +/// Expands a partial device path to a full device path by matching against device topology. +/// +/// This function takes a partial (short-form) device path and finds the corresponding +/// full device path by enumerating all device handles and matching against the partial +/// path's identifying characteristics (e.g., partition GUID for HardDrive nodes). +/// +/// If the input is already a full device path (starts with Hardware or ACPI node), +/// it is returned unchanged. +/// +/// # Arguments +/// +/// * `boot_services` - Boot services for handle enumeration +/// * `partial_path` - The device path to expand (may be full or partial) +/// +/// # Returns +/// +/// * `Ok(DevicePathBuf)` - The expanded full device path, or the original if already full +/// * `Err(EfiError::InvalidParameter)` - If the partial path is empty +/// * `Err(EfiError::NotFound)` - If no matching device was found in the topology +/// +/// # Supported Partial Path Types +/// +/// Currently supports: +/// - **HardDrive (Media type 4, subtype 1)**: Matches by partition signature and signature type +/// +/// Future enhancements may add support for: +/// - FilePath-only paths (require filesystem enumeration) +/// - Messaging node paths without root +pub fn expand_device_path(boot_services: &B, partial_path: &DevicePath) -> Result { + // Return unchanged if already a full path + if !is_partial_device_path(partial_path) { + return Ok(partial_path.into()); + } + + // Parse the HardDrive node from the partial path to extract the partition signature. + let target_sig = partial_path.iter().find_map(|node| { + let hd = HardDrive::try_from_node(&node)?; + Some((hd.partition_signature.to_vec(), hd.signature_type)) + }); + + let (target_sig, target_sig_type) = match target_sig { + Some(s) => s, + None => { + log::error!("expand_device_path: no HardDrive node found in partial path"); + return Err(EfiError::InvalidParameter); + } + }; + + // Collect remaining nodes after the HardDrive node in the partial path. + // Typically this is the FilePath node (e.g., \EFI\Boot\BOOTX64.efi). + let remaining_nodes: Vec<_> = { + let mut past_hd = false; + partial_path + .iter() + .filter(move |node| { + if past_hd && node.header.r#type != DevicePathType::End as u8 { + return true; + } + if HardDrive::try_from_node(node).is_some() { + past_hd = true; + } + false + }) + .collect() + }; + + // Enumerate all handles with DevicePath protocol + let handles = boot_services + .locate_handle_buffer(HandleSearchType::ByProtocol(&efi::protocols::device_path::PROTOCOL_GUID)) + .map_err(EfiError::from)?; + + // Search for a handle whose device path contains a matching HardDrive node + for &handle in handles.iter() { + // SAFETY: handle is valid from locate_handle_buffer, requesting device path protocol. + let dp_ptr = match unsafe { boot_services.handle_protocol::(handle) } { + Ok(ptr) => ptr, + Err(_) => continue, + }; + + // SAFETY: The device path pointer comes from a valid protocol interface. + let handle_path = match unsafe { DevicePath::try_from_ptr(dp_ptr as *const _ as *const u8) } { + Ok(path) => path, + Err(_) => continue, + }; + + // Walk the handle's device path looking for a matching HardDrive node. + // Collect nodes up to and including the HD node so we truncate any nodes + // beyond HD (e.g., filesystem handles may extend past HD with FilePath nodes + // that would conflict with remaining_nodes from the partial path). + let mut prefix_nodes = Vec::new(); + let mut found = false; + for node in handle_path.iter() { + if node.header.r#type == DevicePathType::End as u8 { + break; + } + let is_match = HardDrive::try_from_node(&node).is_some_and(|hd| { + hd.partition_signature == target_sig.as_slice() && hd.signature_type == target_sig_type + }); + prefix_nodes.push(node); + if is_match { + found = true; + break; + } + } + + if found { + let mut result = DevicePathBuf::from_device_path_node_iter(prefix_nodes.into_iter()); + let remaining_dp = DevicePathBuf::from_device_path_node_iter(remaining_nodes.into_iter()); + result.append_device_path(&remaining_dp); + return Ok(result); + } + } + + log::error!("expand_device_path: no matching partition found in device topology"); + Err(EfiError::NotFound) +} + +const LOAD_OPTION_ACTIVE: u32 = 0x00000001; + +use zerocopy::FromBytes; +use zerocopy_derive::*; + +#[derive(FromBytes, KnownLayout, Immutable)] +#[repr(C, packed)] +struct LoadOptionHeader { + attributes: zerocopy::little_endian::U32, + file_path_list_length: zerocopy::little_endian::U16, +} + +/// Discover boot options from UEFI `BootOrder` and `Boot####` variables. +/// +/// Reads the `BootOrder` variable to determine boot attempt order, then reads +/// each corresponding `Boot####` variable and parses the `EFI_LOAD_OPTION` +/// structure to extract device paths. Only active boot options are returned. +/// +/// Returns a [`BootConfig`](crate::config::BootConfig) populated with the discovered device paths, or +/// an error if `BootOrder` cannot be read. +pub fn discover_boot_options(runtime_services: &R) -> Result { + let namespace = EFI_GLOBAL_VARIABLE.into_inner(); + + let boot_order_name: &[u16] = &[ + 'B' as u16, 'o' as u16, 'o' as u16, 't' as u16, 'O' as u16, 'r' as u16, 'd' as u16, 'e' as u16, 'r' as u16, 0, + ]; + + let (boot_order_bytes, _attributes): (Vec, u32) = + runtime_services.get_variable(boot_order_name, &namespace, None)?; + + if boot_order_bytes.len() % 2 != 0 || boot_order_bytes.is_empty() { + log::error!("discover_boot_options: invalid BootOrder variable length"); + return Err(EfiError::NotFound); + } + + let boot_order: Vec = + boot_order_bytes.chunks_exact(2).map(|chunk| u16::from_le_bytes([chunk[0], chunk[1]])).collect(); + + let mut device_paths: Vec = Vec::new(); + + for option_number in &boot_order { + let var_name = boot_option_variable_name(*option_number); + + let load_option_bytes = match runtime_services.get_variable::>(&var_name, &namespace, None) { + Ok((bytes, _)) => bytes, + Err(e) => { + log::warn!("discover_boot_options: failed to read Boot{:04X}: {:?}", option_number, e); + continue; + } + }; + + if let Some(device_path_buf) = parse_load_option(&load_option_bytes) { + device_paths.push(device_path_buf); + } + } + + let mut iter = device_paths.into_iter(); + let first = iter.next().ok_or(EfiError::NotFound)?; + let config = iter.fold(super::config::BootConfig::new(first), |config, dp| config.with_device(dp)); + + Ok(config) +} + +/// Build a null-terminated UTF-16 variable name for `Boot####`. +fn boot_option_variable_name(option_number: u16) -> Vec { + let mut name = alloc::format!("Boot{:04X}", option_number).encode_utf16().collect::>(); + name.push(0); + name +} + +/// Parse an `EFI_LOAD_OPTION` structure and return the device path if active. +/// +/// EFI_LOAD_OPTION layout: +/// u32 Attributes +/// u16 FilePathListLength +/// [u16] Description (null-terminated UTF-16) +/// [u8] FilePathList (device path, FilePathListLength bytes) +/// [u8] OptionalData (remaining bytes, ignored) +fn parse_load_option(data: &[u8]) -> Option { + let (header, rest) = LoadOptionHeader::read_from_prefix(data).ok()?; + + if header.attributes.get() & LOAD_OPTION_ACTIVE == 0 { + return None; + } + + let file_path_list_length = header.file_path_list_length.get() as usize; + + // Skip past the null-terminated UTF-16 description string + let mut offset = 0; + loop { + if offset + 1 >= rest.len() { + return None; + } + let ch = u16::from_le_bytes([rest[offset], rest[offset + 1]]); + offset += 2; + if ch == 0 { + break; + } + } + + let file_path_end = offset + file_path_list_length; + if file_path_end > rest.len() { + return None; + } + + let file_path_bytes = &rest[offset..file_path_end]; + + // SAFETY: file_path_bytes points to a valid device path from the EFI_LOAD_OPTION. + let device_path = unsafe { DevicePath::try_from_ptr(file_path_bytes.as_ptr()) }; + match device_path { + Ok(dp) => Some(DevicePathBuf::from(dp)), + Err(e) => { + log::warn!("discover_boot_options: invalid device path in load option: {}", e); + None + } + } +} + +#[cfg(test)] +mod tests { + extern crate alloc; + extern crate std; + + use alloc::boxed::Box; + + use super::*; + use core::sync::atomic::{AtomicUsize, Ordering}; + use patina::{ + boot_services::{MockBootServices, boxed::BootServicesBox}, + device_path::node_defs::{Acpi, EndEntire, HardDrive}, + }; + + fn create_test_device_path() -> DevicePathBuf { + // Create a full device path (starts with ACPI node) so it won't trigger partial path expansion + DevicePathBuf::from_device_path_node_iter([Acpi::new_pci_root(0)].into_iter()) + } + + fn dummy_parent_handle() -> efi::Handle { + std::ptr::dangling_mut::() + } + + #[test] + fn test_boot_from_device_path_success() { + let device_path = create_test_device_path(); + let mut mock = MockBootServices::new(); + + // Expect load_image to succeed + mock.expect_load_image().returning(|_, _, _, _| Ok(core::ptr::null_mut())); + + // Expect watchdog to be set to 5 minutes + mock.expect_set_watchdog_timer().withf(|timeout| *timeout == WATCHDOG_TIMEOUT_SECONDS).returning(|_| Ok(())); + + // Expect start_image to succeed (return Ok) + mock.expect_start_image().returning(|_| Ok(())); + + // Expect watchdog to be disabled after boot returns + mock.expect_set_watchdog_timer().withf(|timeout| *timeout == 0).returning(|_| Ok(())); + + let result = boot_from_device_path(&mock, dummy_parent_handle(), &device_path); + assert!(result.is_ok()); + } + + #[test] + fn test_boot_from_device_path_load_failure() { + let device_path = create_test_device_path(); + let mut mock = MockBootServices::new(); + + // Expect load_image to fail + mock.expect_load_image().returning(|_, _, _, _| Err(efi::Status::NOT_FOUND)); + + let result = boot_from_device_path(&mock, dummy_parent_handle(), &device_path); + assert!(result.is_err()); + } + + #[test] + fn test_boot_from_device_path_start_failure() { + let device_path = create_test_device_path(); + let mut mock = MockBootServices::new(); + + // Expect load_image to succeed + mock.expect_load_image().returning(|_, _, _, _| Ok(core::ptr::null_mut())); + + // Expect watchdog to be set + mock.expect_set_watchdog_timer().returning(|_| Ok(())); + + // Expect start_image to fail + mock.expect_start_image().returning(|_| Err((efi::Status::LOAD_ERROR, None))); + + // Expect watchdog to be disabled even on failure + mock.expect_set_watchdog_timer().returning(|_| Ok(())); + + let result = boot_from_device_path(&mock, dummy_parent_handle(), &device_path); + assert!(result.is_err()); + } + + #[test] + fn test_boot_from_device_path_watchdog_disabled_on_failure() { + let device_path = create_test_device_path(); + let mut mock = MockBootServices::new(); + + static WATCHDOG_DISABLE_CALLED: AtomicUsize = AtomicUsize::new(0); + + mock.expect_load_image().returning(|_, _, _, _| Ok(core::ptr::null_mut())); + + mock.expect_set_watchdog_timer().returning(|timeout| { + if timeout == 0 { + WATCHDOG_DISABLE_CALLED.fetch_add(1, Ordering::SeqCst); + } + Ok(()) + }); + + mock.expect_start_image().returning(|_| Err((efi::Status::ABORTED, None))); + + let _ = boot_from_device_path(&mock, dummy_parent_handle(), &device_path); + + // Verify watchdog was disabled (timeout=0 was called) + assert!(WATCHDOG_DISABLE_CALLED.load(Ordering::SeqCst) >= 1); + } + + #[test] + fn test_signal_bds_phase_entry_signals_end_of_dxe() { + let mut mock = MockBootServices::new(); + + // Expect event creation with proper type annotation + mock.expect_create_event_ex_unchecked::<()>().returning(|_, _, _, _, _| Ok(core::ptr::null_mut())); + + // Expect event to be signaled + mock.expect_signal_event().returning(|_| Ok(())); + + // Expect event to be closed + mock.expect_close_event().returning(|_| Ok(())); + + let result = signal_bds_phase_entry(&mock); + assert!(result.is_ok()); + } + + #[test] + fn test_signal_ready_to_boot() { + let mut mock = MockBootServices::new(); + + mock.expect_create_event_ex_unchecked::<()>().returning(|_, _, _, _, _| Ok(core::ptr::null_mut())); + mock.expect_signal_event().returning(|_| Ok(())); + mock.expect_close_event().returning(|_| Ok(())); + + let result = signal_ready_to_boot(&mock); + assert!(result.is_ok()); + } + + #[test] + fn test_connect_all_locate_failure() { + let mut mock = MockBootServices::new(); + + // locate_handle_buffer fails on first call + mock.expect_locate_handle_buffer().returning(|_| Err(efi::Status::NOT_FOUND)); + + let result = connect_all(&mock); + assert!(result.is_err()); + } + + #[test] + fn test_discover_console_devices_handles_missing_protocols() { + use patina::runtime_services::MockRuntimeServices; + + let mut boot_mock = MockBootServices::new(); + let runtime_mock = MockRuntimeServices::new(); + + // Protocols not found - returns error but function should still succeed + boot_mock.expect_locate_handle_buffer().returning(|_| Err(efi::Status::NOT_FOUND)); + + // Function should still succeed even with no console devices + let result = discover_console_devices(&boot_mock, &runtime_mock); + assert!(result.is_ok()); + } + + #[test] + fn test_signal_bds_phase_entry_create_event_failure() { + let mut mock = MockBootServices::new(); + + // Event creation fails + mock.expect_create_event_ex_unchecked::<()>().returning(|_, _, _, _, _| Err(efi::Status::OUT_OF_RESOURCES)); + + let result = signal_bds_phase_entry(&mock); + assert!(result.is_err()); + } + + #[test] + fn test_signal_bds_phase_entry_signal_failure() { + let mut mock = MockBootServices::new(); + + mock.expect_create_event_ex_unchecked::<()>().returning(|_, _, _, _, _| Ok(core::ptr::null_mut())); + + // Signal fails + mock.expect_signal_event().returning(|_| Err(efi::Status::INVALID_PARAMETER)); + + // close_event is always called, even on signal failure + mock.expect_close_event().returning(|_| Ok(())); + + let result = signal_bds_phase_entry(&mock); + assert!(result.is_err()); + } + + #[test] + fn test_signal_bds_phase_entry_close_event_failure() { + let mut mock = MockBootServices::new(); + + mock.expect_create_event_ex_unchecked::<()>().returning(|_, _, _, _, _| Ok(core::ptr::null_mut())); + mock.expect_signal_event().returning(|_| Ok(())); + + // Close fails + mock.expect_close_event().returning(|_| Err(efi::Status::INVALID_PARAMETER)); + + let result = signal_bds_phase_entry(&mock); + assert!(result.is_err()); + } + + #[test] + fn test_signal_ready_to_boot_create_event_failure() { + let mut mock = MockBootServices::new(); + + mock.expect_create_event_ex_unchecked::<()>().returning(|_, _, _, _, _| Err(efi::Status::OUT_OF_RESOURCES)); + + let result = signal_ready_to_boot(&mock); + assert!(result.is_err()); + } + + #[test] + fn test_boot_from_device_path_watchdog_set_failure() { + let device_path = create_test_device_path(); + let mut mock = MockBootServices::new(); + + mock.expect_load_image().returning(|_, _, _, _| Ok(core::ptr::null_mut())); + + // Watchdog set fails + mock.expect_set_watchdog_timer().returning(|_| Err(efi::Status::DEVICE_ERROR)); + + let result = boot_from_device_path(&mock, dummy_parent_handle(), &device_path); + assert!(result.is_err()); + } + + #[test] + fn test_detect_hotkey_no_input_handles() { + let mut mock = MockBootServices::new(); + + // No SimpleTextInput handles found + mock.expect_locate_handle_buffer().returning(|_| Err(efi::Status::NOT_FOUND)); + + let result = detect_hotkey(&mock, 0x16); // F12 + assert!(!result); + } + + // Tests for partial device path expansion + + use patina::device_path::node_defs::Pci; + + /// Helper to build a partial device path starting with HD node. + fn build_partial_hd_path(guid: [u8; 16]) -> DevicePathBuf { + DevicePathBuf::from_device_path_node_iter([HardDrive::new_gpt(1, 2048, 1000000, guid)].into_iter()) + } + + /// Helper to build a full device path starting with ACPI root. + fn build_full_path_with_hd(guid: [u8; 16]) -> DevicePathBuf { + let mut path = DevicePathBuf::from_device_path_node_iter([Acpi::new_pci_root(0)].into_iter()); + let pci_path = DevicePathBuf::from_device_path_node_iter([Pci { function: 0, device: 0x1D }].into_iter()); + path.append_device_path(&pci_path); + let hd_path = + DevicePathBuf::from_device_path_node_iter([HardDrive::new_gpt(1, 2048, 1000000, guid)].into_iter()); + path.append_device_path(&hd_path); + path + } + + #[test] + fn test_is_partial_with_hd_node() { + let partial = build_partial_hd_path([0xAA; 16]); + assert!(is_partial_device_path(&partial)); + } + + #[test] + fn test_is_partial_with_full_path_acpi() { + let full = build_full_path_with_hd([0xAA; 16]); + assert!(!is_partial_device_path(&full)); + } + + #[test] + fn test_is_partial_empty_path() { + let empty = DevicePathBuf::from_device_path_node_iter([EndEntire].into_iter()); + // EndEntire is type 0x7F (End) - an end-only path is not a meaningful partial path + assert!(!is_partial_device_path(&empty)); + } + + #[test] + fn test_expand_already_full_returns_unchanged() { + let full = build_full_path_with_hd([0xAA; 16]); + + let mock = MockBootServices::new(); + // No mock setup needed since full paths return early + + let result = expand_device_path(&mock, &full); + assert!(result.is_ok()); + assert_eq!(result.unwrap(), full); + } + + #[test] + fn test_expand_partial_path_locate_fails() { + let partial = build_partial_hd_path([0xBB; 16]); + + let mut mock = MockBootServices::new(); + + // locate_handle_buffer fails — no device path handles available + mock.expect_locate_handle_buffer().returning(|_| Err(efi::Status::NOT_FOUND)); + + let result = expand_device_path(&mock, &partial); + assert!(result.is_err(), "expand_device_path should fail when no handles found"); + } + + /// Helper: leaked MockBootServices whose only job is to accept free_pool calls + /// when a BootServicesBox is dropped. + fn leaked_boot_services_for_box() -> &'static MockBootServices { + Box::leak(Box::new({ + let mut m = MockBootServices::new(); + m.expect_free_pool().returning(|_| Ok(())); + m + })) + } + + /// Helper: build a BootServicesBox<[Handle]> for use in test mock returns. + /// + /// Handle storage is intentionally leaked; the mock's free_pool is a no-op. + fn mock_handle_buffer( + handle_addrs: &[usize], + boot_services: &'static MockBootServices, + ) -> BootServicesBox<'static, [efi::Handle], MockBootServices> { + let handles: Vec = handle_addrs.iter().map(|&a| a as efi::Handle).collect(); + let leaked = handles.leak(); + // SAFETY: leaked is a valid pointer+length from Vec::leak. + unsafe { BootServicesBox::from_raw_parts_mut(leaked.as_mut_ptr(), leaked.len(), boot_services) } + } + + #[test] + fn test_build_multi_instance_device_path_single_protocol() { + let dp = DevicePathBuf::from_device_path_node_iter([Acpi::new_pci_root(0)].into_iter()); + let dp_addr = dp.as_ref() as *const DevicePath as *const u8 as usize; + let handle_addr: usize = 1; + let box_mock = leaked_boot_services_for_box(); + + let mut mock = MockBootServices::new(); + + mock.expect_locate_handle_buffer().returning(move |_| Ok(mock_handle_buffer(&[handle_addr], box_mock))); + + // SAFETY: Test code — returning a pointer to a valid DevicePathBuf kept alive by the test. + unsafe { + mock.expect_handle_protocol::() + .returning(move |_| Ok((dp_addr as *mut efi::protocols::device_path::Protocol).as_mut().unwrap())); + } + + let result = build_multi_instance_device_path(&mock, &[&simple_text_input::PROTOCOL_GUID]); + assert!(result.is_some()); + + let multi = result.unwrap(); + assert_eq!(multi.as_ref().as_bytes(), dp.as_ref().as_bytes()); + } + + #[test] + fn test_build_multi_instance_device_path_deduplicates_handles() { + let dp = DevicePathBuf::from_device_path_node_iter([Acpi::new_pci_root(0)].into_iter()); + let dp_addr = dp.as_ref() as *const DevicePath as *const u8 as usize; + + // Same handle for both protocols — should appear only once + let handle_addr: usize = 1; + let box_mock = leaked_boot_services_for_box(); + + let mut mock = MockBootServices::new(); + + mock.expect_locate_handle_buffer().returning(move |_| Ok(mock_handle_buffer(&[handle_addr], box_mock))); + + // SAFETY: Test code — returning pointer to valid DevicePathBuf. + unsafe { + mock.expect_handle_protocol::() + .times(1) // Called only once despite two GUIDs — handle is deduplicated + .returning(move |_| Ok((dp_addr as *mut efi::protocols::device_path::Protocol).as_mut().unwrap())); + } + + let result = build_multi_instance_device_path( + &mock, + &[&efi::protocols::simple_text_output::PROTOCOL_GUID, &efi::protocols::graphics_output::PROTOCOL_GUID], + ); + assert!(result.is_some()); + } + + #[test] + fn test_build_multi_instance_device_path_handle_protocol_failure() { + let handle_addr: usize = 1; + let box_mock = leaked_boot_services_for_box(); + + let mut mock = MockBootServices::new(); + + mock.expect_locate_handle_buffer().returning(move |_| Ok(mock_handle_buffer(&[handle_addr], box_mock))); + + // handle_protocol fails — handle has no device path + mock.expect_handle_protocol::() + .returning(|_| Err(efi::Status::UNSUPPORTED)); + + let result = build_multi_instance_device_path(&mock, &[&simple_text_input::PROTOCOL_GUID]); + assert!(result.is_none()); + } + + #[test] + fn test_discover_console_devices_sets_variables() { + use patina::runtime_services::MockRuntimeServices; + + let dp = DevicePathBuf::from_device_path_node_iter([Acpi::new_pci_root(0)].into_iter()); + let dp_addr = dp.as_ref() as *const DevicePath as *const u8 as usize; + let handle_addr: usize = 1; + let box_mock = leaked_boot_services_for_box(); + + let mut boot_mock = MockBootServices::new(); + let mut runtime_mock = MockRuntimeServices::new(); + + boot_mock.expect_locate_handle_buffer().returning(move |_| Ok(mock_handle_buffer(&[handle_addr], box_mock))); + + // SAFETY: Test code — returning pointer to valid DevicePathBuf. + unsafe { + boot_mock + .expect_handle_protocol::() + .returning(move |_| Ok((dp_addr as *mut efi::protocols::device_path::Protocol).as_mut().unwrap())); + } + + runtime_mock + .expect_set_variable::>() + .times(3) // ConIn, ConOut, ErrOut + .returning(|_, _, _, _| Ok(())); + + let result = discover_console_devices(&boot_mock, &runtime_mock); + assert!(result.is_ok()); + } + + #[test] + fn test_discover_console_devices_set_variable_failure_is_non_fatal() { + use patina::runtime_services::MockRuntimeServices; + + let dp = DevicePathBuf::from_device_path_node_iter([Acpi::new_pci_root(0)].into_iter()); + let dp_addr = dp.as_ref() as *const DevicePath as *const u8 as usize; + let handle_addr: usize = 1; + let box_mock = leaked_boot_services_for_box(); + + let mut boot_mock = MockBootServices::new(); + let mut runtime_mock = MockRuntimeServices::new(); + + boot_mock.expect_locate_handle_buffer().returning(move |_| Ok(mock_handle_buffer(&[handle_addr], box_mock))); + + // SAFETY: Test code — returning pointer to valid DevicePathBuf. + unsafe { + boot_mock + .expect_handle_protocol::() + .returning(move |_| Ok((dp_addr as *mut efi::protocols::device_path::Protocol).as_mut().unwrap())); + } + + // set_variable fails — should still return Ok + runtime_mock.expect_set_variable::>().returning(|_, _, _, _| Err(efi::Status::OUT_OF_RESOURCES)); + + let result = discover_console_devices(&boot_mock, &runtime_mock); + assert!(result.is_ok(), "set_variable failure should be non-fatal"); + } + + // Tests for connect_all + + #[test] + fn test_connect_all_stabilizes_after_two_iterations() { + let box_mock = leaked_boot_services_for_box(); + let call_count = std::sync::Arc::new(AtomicUsize::new(0)); + let call_count_clone = call_count.clone(); + + let mut mock = MockBootServices::new(); + + // First call returns 1 handle, second returns 2 (new device discovered), + // third returns 2 again (stabilized). + mock.expect_locate_handle_buffer().returning(move |_| { + let n = call_count_clone.fetch_add(1, Ordering::SeqCst); + let count = if n == 0 { 1 } else { 2 }; + let addrs: Vec = (1..=count).collect(); + Ok(mock_handle_buffer(&addrs, box_mock)) + }); + + mock.expect_connect_controller().returning(|_, _, _, _| Ok(())); + + let result = connect_all(&mock); + assert!(result.is_ok()); + // 3 iterations: 1 handle, 2 handles, 2 handles (stabilized) + assert_eq!(call_count.load(Ordering::SeqCst), 3); + } + + // Tests for detect_hotkey_from_handles + + /// Test extern "efiapi" read_key_stroke that returns F12 then NOT_READY. + extern "efiapi" fn mock_read_key_stroke_f12( + _this: *mut simple_text_input::Protocol, + key: *mut simple_text_input::InputKey, + ) -> efi::Status { + static CALL_COUNT: AtomicUsize = AtomicUsize::new(0); + let n = CALL_COUNT.fetch_add(1, Ordering::SeqCst); + if n == 0 { + // SAFETY: key is a valid pointer provided by the caller. + unsafe { + (*key).scan_code = 0x16; // F12 + (*key).unicode_char = 0; + } + efi::Status::SUCCESS + } else { + efi::Status::NOT_READY + } + } + + /// Test extern "efiapi" read_key_stroke that always returns NOT_READY. + extern "efiapi" fn mock_read_key_stroke_empty( + _this: *mut simple_text_input::Protocol, + _key: *mut simple_text_input::InputKey, + ) -> efi::Status { + efi::Status::NOT_READY + } + + /// Test extern "efiapi" reset (unused, required for struct completeness). + extern "efiapi" fn mock_reset(_this: *mut simple_text_input::Protocol, _extended: efi::Boolean) -> efi::Status { + efi::Status::SUCCESS + } + + #[test] + fn test_detect_hotkey_from_handles_finds_matching_key() { + let mut protocol = simple_text_input::Protocol { + reset: mock_reset, + read_key_stroke: mock_read_key_stroke_f12, + wait_for_key: ptr::null_mut(), + }; + let protocol_addr = &mut protocol as *mut _ as usize; + + let mut mock = MockBootServices::new(); + + // SAFETY: Test code — returning pointer to a valid Protocol kept alive by the test. + unsafe { + mock.expect_handle_protocol::() + .returning(move |_| Ok((protocol_addr as *mut simple_text_input::Protocol).as_mut().unwrap())); + } + + let handle: efi::Handle = 1usize as efi::Handle; + // SAFETY: handle is a test value, mock returns a valid protocol. + let result = unsafe { detect_hotkey_from_handles(&mock, &[handle], 0x16) }; + assert!(result, "F12 hotkey should be detected"); + } + + #[test] + fn test_detect_hotkey_from_handles_no_keys_buffered() { + let mut protocol = simple_text_input::Protocol { + reset: mock_reset, + read_key_stroke: mock_read_key_stroke_empty, + wait_for_key: ptr::null_mut(), + }; + let protocol_addr = &mut protocol as *mut _ as usize; + + let mut mock = MockBootServices::new(); + + // SAFETY: Test code — returning pointer to a valid Protocol kept alive by the test. + unsafe { + mock.expect_handle_protocol::() + .returning(move |_| Ok((protocol_addr as *mut simple_text_input::Protocol).as_mut().unwrap())); + } + + let handle: efi::Handle = 1usize as efi::Handle; + // SAFETY: handle is a test value, mock returns a valid protocol. + let result = unsafe { detect_hotkey_from_handles(&mock, &[handle], 0x16) }; + assert!(!result, "No keys in buffer should return false"); + } + + #[test] + fn test_detect_hotkey_from_handles_protocol_failure() { + let mut mock = MockBootServices::new(); + + // handle_protocol fails — no SimpleTextInput on this handle + mock.expect_handle_protocol::().returning(|_| Err(efi::Status::UNSUPPORTED)); + + let handle: efi::Handle = 1usize as efi::Handle; + // SAFETY: handle is a test value, mock returns Err. + let result = unsafe { detect_hotkey_from_handles(&mock, &[handle], 0x16) }; + assert!(!result, "Protocol failure should return false"); + } + + // Tests for expand_device_path success path + + #[test] + fn test_expand_partial_path_matches_partition() { + let guid = [0xAA; 16]; + let partial = build_partial_hd_path(guid); + let full = build_full_path_with_hd(guid); + let full_addr = full.as_ref() as *const DevicePath as *const u8 as usize; + + let handle_addr: usize = 1; + let box_mock = leaked_boot_services_for_box(); + + let mut mock = MockBootServices::new(); + + mock.expect_locate_handle_buffer().returning(move |_| Ok(mock_handle_buffer(&[handle_addr], box_mock))); + + // SAFETY: Test code — returning pointer to valid DevicePathBuf. + unsafe { + mock.expect_handle_protocol::() + .returning(move |_| Ok((full_addr as *mut efi::protocols::device_path::Protocol).as_mut().unwrap())); + } + + let result = expand_device_path(&mock, &partial); + assert!(result.is_ok(), "Should find matching partition"); + + // The expanded path should start with ACPI root (from the full path) + let expanded = result.unwrap(); + assert!(!is_partial_device_path(&expanded), "Expanded path should be a full path"); + } + + #[test] + fn test_expand_partial_path_no_matching_partition() { + let partial = build_partial_hd_path([0xAA; 16]); + // Full path has a different GUID — no match + let full = build_full_path_with_hd([0xBB; 16]); + let full_addr = full.as_ref() as *const DevicePath as *const u8 as usize; + + let handle_addr: usize = 1; + let box_mock = leaked_boot_services_for_box(); + + let mut mock = MockBootServices::new(); + + mock.expect_locate_handle_buffer().returning(move |_| Ok(mock_handle_buffer(&[handle_addr], box_mock))); + + // SAFETY: Test code — returning pointer to valid DevicePathBuf. + unsafe { + mock.expect_handle_protocol::() + .returning(move |_| Ok((full_addr as *mut efi::protocols::device_path::Protocol).as_mut().unwrap())); + } + + let result = expand_device_path(&mock, &partial); + assert!(result.is_err(), "Should fail when no partition matches"); + } + + #[test] + /// Verify that expansion truncates the handle's device path at the matched node, + /// discarding any trailing nodes. This prevents duplication when a handle's path + /// extends past the node we match on (e.g., filesystem handles that include + /// FilePath nodes after HD). The same principle applies to any future partial + /// path types — we must only use the prefix up to the matched node. + fn test_expand_partial_path_truncates_at_matched_node() { + use patina::device_path::node_defs::FilePath; + + let guid = [0xAA; 16]; + // Partial path: HD()/FilePath(\EFI\Boot\BOOTX64.efi) + let mut partial = + DevicePathBuf::from_device_path_node_iter([HardDrive::new_gpt(1, 2048, 1000000, guid)].into_iter()); + let fp = DevicePathBuf::from_device_path_node_iter([FilePath::new("\\EFI\\Boot\\BOOTX64.efi")].into_iter()); + partial.append_device_path(&fp); + + // Handle has nodes beyond the matched node — simulates a handle whose device + // path extends past the point we match on (e.g., a filesystem handle). + // ACPI/PCI/HD()/FilePath(\some\other\path) + let mut handle_dp = build_full_path_with_hd(guid); + let extra_fp = DevicePathBuf::from_device_path_node_iter([FilePath::new("\\some\\other\\path")].into_iter()); + handle_dp.append_device_path(&extra_fp); + + let handle_dp_addr = handle_dp.as_ref() as *const DevicePath as *const u8 as usize; + let handle_addr: usize = 1; + let box_mock = leaked_boot_services_for_box(); + + let mut mock = MockBootServices::new(); + + mock.expect_locate_handle_buffer().returning(move |_| Ok(mock_handle_buffer(&[handle_addr], box_mock))); + + // SAFETY: Test code — returning pointer to valid DevicePathBuf. + unsafe { + mock.expect_handle_protocol::().returning(move |_| { + Ok((handle_dp_addr as *mut efi::protocols::device_path::Protocol).as_mut().unwrap()) + }); + } + + let result = expand_device_path(&mock, &partial); + assert!(result.is_ok()); + + let expanded = result.unwrap(); + let node_count = expanded.as_ref().node_count(); + assert_eq!(node_count, 5, "Should have prefix(3) + remaining(1) + End, got {node_count} nodes"); + } + + #[test] + fn test_expand_partial_path_handle_protocol_failure() { + let partial = build_partial_hd_path([0xAA; 16]); + + let handle_addr: usize = 1; + let box_mock = leaked_boot_services_for_box(); + + let mut mock = MockBootServices::new(); + + mock.expect_locate_handle_buffer().returning(move |_| Ok(mock_handle_buffer(&[handle_addr], box_mock))); + + // handle_protocol fails + mock.expect_handle_protocol::() + .returning(|_| Err(efi::Status::UNSUPPORTED)); + + let result = expand_device_path(&mock, &partial); + assert!(result.is_err(), "Should fail when handle_protocol fails"); + } + + // Tests for discover_boot_options / parse_load_option / boot_option_variable_name + + fn build_load_option(attributes: u32, description: &str, device_path: &DevicePathBuf) -> Vec { + let mut data = Vec::new(); + data.extend_from_slice(&attributes.to_le_bytes()); + let dp_bytes = device_path.as_ref().as_bytes(); + data.extend_from_slice(&(dp_bytes.len() as u16).to_le_bytes()); + for c in description.encode_utf16() { + data.extend_from_slice(&c.to_le_bytes()); + } + data.extend_from_slice(&0u16.to_le_bytes()); // null terminator + data.extend_from_slice(dp_bytes); + data + } + + fn build_boot_order(option_numbers: &[u16]) -> Vec { + option_numbers.iter().flat_map(|n| n.to_le_bytes()).collect() + } + + #[test] + fn test_boot_option_variable_name() { + let name = boot_option_variable_name(0x0001); + let expected: Vec = "Boot0001\0".encode_utf16().collect(); + assert_eq!(name, expected); + } + + #[test] + fn test_boot_option_variable_name_hex() { + let name = boot_option_variable_name(0x00AB); + let expected: Vec = "Boot00AB\0".encode_utf16().collect(); + assert_eq!(name, expected); + } + + #[test] + fn test_parse_load_option_active() { + let dp = create_test_device_path(); + let data = build_load_option(LOAD_OPTION_ACTIVE, "Test", &dp); + let result = parse_load_option(&data); + assert!(result.is_some()); + } + + #[test] + fn test_parse_load_option_inactive() { + let dp = create_test_device_path(); + let data = build_load_option(0, "Test", &dp); + let result = parse_load_option(&data); + assert!(result.is_none()); + } + + #[test] + fn test_parse_load_option_too_short() { + let result = parse_load_option(&[0; 4]); + assert!(result.is_none()); + } + + #[test] + fn test_parse_load_option_truncated_description() { + // Active attributes + file path length but no null terminator for description + let mut data = Vec::new(); + data.extend_from_slice(&LOAD_OPTION_ACTIVE.to_le_bytes()); + data.extend_from_slice(&0u16.to_le_bytes()); // file path length + data.extend_from_slice(&[0x41, 0x00]); // 'A' in UTF-16 but no null terminator + let result = parse_load_option(&data); + assert!(result.is_none()); + } + + #[test] + fn test_discover_boot_options_single_option() { + use patina::runtime_services::MockRuntimeServices; + + let dp = create_test_device_path(); + let load_option = build_load_option(LOAD_OPTION_ACTIVE, "Windows", &dp); + let boot_order = build_boot_order(&[0x0001]); + + let mut runtime_mock = MockRuntimeServices::new(); + + runtime_mock.expect_get_variable::>().returning(move |name, _, _| { + if name[0] == 'B' as u16 && name[4] == 'O' as u16 { + Ok((boot_order.clone(), 0)) + } else { + Ok((load_option.clone(), 0)) + } + }); + + let result = discover_boot_options(&runtime_mock); + assert!(result.is_ok()); + assert_eq!(result.unwrap().devices().count(), 1); + } + + #[test] + fn test_discover_boot_options_multiple_options() { + use patina::runtime_services::MockRuntimeServices; + + let dp = create_test_device_path(); + let load_option = build_load_option(LOAD_OPTION_ACTIVE, "Option", &dp); + let boot_order = build_boot_order(&[0x0001, 0x0002, 0x0003]); + + let mut runtime_mock = MockRuntimeServices::new(); + + runtime_mock.expect_get_variable::>().returning(move |name, _, _| { + if name[0] == 'B' as u16 && name[4] == 'O' as u16 { + Ok((boot_order.clone(), 0)) + } else { + Ok((load_option.clone(), 0)) + } + }); + + let result = discover_boot_options(&runtime_mock); + assert!(result.is_ok()); + assert_eq!(result.unwrap().devices().count(), 3); + } + + #[test] + fn test_discover_boot_options_skips_inactive() { + use patina::runtime_services::MockRuntimeServices; + + let dp = create_test_device_path(); + let active = build_load_option(LOAD_OPTION_ACTIVE, "Active", &dp); + let inactive = build_load_option(0, "Inactive", &dp); + let boot_order = build_boot_order(&[0x0001, 0x0002]); + + let call_count = std::sync::Arc::new(AtomicUsize::new(0)); + let call_count_clone = call_count.clone(); + + let mut runtime_mock = MockRuntimeServices::new(); + + runtime_mock.expect_get_variable::>().returning(move |name, _, _| { + if name[0] == 'B' as u16 && name[4] == 'O' as u16 { + Ok((boot_order.clone(), 0)) + } else { + let n = call_count_clone.fetch_add(1, Ordering::SeqCst); + if n == 0 { Ok((active.clone(), 0)) } else { Ok((inactive.clone(), 0)) } + } + }); + + let result = discover_boot_options(&runtime_mock); + assert!(result.is_ok()); + assert_eq!(result.unwrap().devices().count(), 1); + } + + #[test] + fn test_discover_boot_options_boot_order_not_found() { + use patina::runtime_services::MockRuntimeServices; + + let mut runtime_mock = MockRuntimeServices::new(); + + runtime_mock.expect_get_variable::>().returning(|_, _, _| Err(efi::Status::NOT_FOUND)); + + let result = discover_boot_options(&runtime_mock); + assert!(result.is_err()); + } + + #[test] + fn test_discover_boot_options_all_inactive() { + use patina::runtime_services::MockRuntimeServices; + + let dp = create_test_device_path(); + let inactive = build_load_option(0, "Inactive", &dp); + let boot_order = build_boot_order(&[0x0001]); + + let mut runtime_mock = MockRuntimeServices::new(); + + runtime_mock.expect_get_variable::>().returning(move |name, _, _| { + if name[0] == 'B' as u16 && name[4] == 'O' as u16 { + Ok((boot_order.clone(), 0)) + } else { + Ok((inactive.clone(), 0)) + } + }); + + let result = discover_boot_options(&runtime_mock); + assert!(result.is_err()); + } + + #[test] + fn test_discover_boot_options_skips_unreadable_option() { + use patina::runtime_services::MockRuntimeServices; + + let dp = create_test_device_path(); + let active = build_load_option(LOAD_OPTION_ACTIVE, "Good", &dp); + let boot_order = build_boot_order(&[0x0001, 0x0002]); + + let call_count = std::sync::Arc::new(AtomicUsize::new(0)); + let call_count_clone = call_count.clone(); + + let mut runtime_mock = MockRuntimeServices::new(); + + runtime_mock.expect_get_variable::>().returning(move |name, _, _| { + if name[0] == 'B' as u16 && name[4] == 'O' as u16 { + Ok((boot_order.clone(), 0)) + } else { + let n = call_count_clone.fetch_add(1, Ordering::SeqCst); + if n == 0 { Err(efi::Status::NOT_FOUND) } else { Ok((active.clone(), 0)) } + } + }); + + let result = discover_boot_options(&runtime_mock); + assert!(result.is_ok()); + assert_eq!(result.unwrap().devices().count(), 1); + } +} diff --git a/components/patina_boot/src/lib.rs b/components/patina_boot/src/lib.rs new file mode 100644 index 000000000..685828fcd --- /dev/null +++ b/components/patina_boot/src/lib.rs @@ -0,0 +1,41 @@ +//! Boot Orchestration Components +//! +//! This crate provides boot orchestration for Patina firmware, implementing +//! UEFI Specification 2.11 Chapter 3 (Boot Manager) and PI Specification BDS phase requirements. +//! +//! ## Architecture +//! +//! - [`BootOrchestrator`]: A trait defining the boot flow interface. Platforms implement this +//! trait to customize boot behavior. +//! - [`BootDispatcher`]: The Patina component that installs the BDS architectural protocol and +//! delegates to a `BootOrchestrator` implementation when invoked by the DXE core. +//! - [`SimpleBootManager`]: A default `BootOrchestrator` implementation for platforms with +//! straightforward boot topologies. +//! +//! ## Configuration +//! +//! - [`config::BootConfig`]: Boot configuration for `BootOrchestrator` implementations +//! +//! ## Helper Functions +//! +//! The [`helpers`] module provides helper functions for platforms implementing custom boot flows. +//! +//! ## License +//! +//! Copyright (c) Microsoft Corporation. +//! +//! SPDX-License-Identifier: Apache-2.0 +//! +#![cfg_attr(not(feature = "std"), no_std)] +#![feature(coverage_attribute)] +#![feature(never_type)] + +pub mod boot_dispatcher; +pub mod boot_orchestrator; +pub mod config; +pub mod helpers; +pub mod orchestrators; + +pub use boot_dispatcher::BootDispatcher; +pub use boot_orchestrator::BootOrchestrator; +pub use orchestrators::SimpleBootManager; diff --git a/components/patina_boot/src/orchestrators.rs b/components/patina_boot/src/orchestrators.rs new file mode 100644 index 000000000..a5771c2d8 --- /dev/null +++ b/components/patina_boot/src/orchestrators.rs @@ -0,0 +1,12 @@ +//! Built-in [`BootOrchestrator`](crate::BootOrchestrator) implementations. +//! +//! ## License +//! +//! Copyright (c) Microsoft Corporation. +//! +//! SPDX-License-Identifier: Apache-2.0 +//! + +mod simple_boot_manager; + +pub use simple_boot_manager::SimpleBootManager; diff --git a/components/patina_boot/src/orchestrators/simple_boot_manager.rs b/components/patina_boot/src/orchestrators/simple_boot_manager.rs new file mode 100644 index 000000000..16ea46b94 --- /dev/null +++ b/components/patina_boot/src/orchestrators/simple_boot_manager.rs @@ -0,0 +1,327 @@ +//! Simple boot manager implementation. +//! +//! [`SimpleBootManager`] implements the [`BootOrchestrator`](crate::BootOrchestrator) +//! trait for platforms with straightforward boot topologies. It supports: +//! +//! - Flexible multi-device boot via [`BootConfig`](crate::config::BootConfig) +//! - Optional hotkey detection for alternate boot paths +//! - Configurable failure handler +//! +//! ## License +//! +//! Copyright (c) Microsoft Corporation. +//! +//! SPDX-License-Identifier: Apache-2.0 +//! +extern crate alloc; + +use alloc::vec::Vec; + +use patina::{ + boot_services::StandardBootServices, device_path::paths::DevicePathBuf, error::EfiError, + runtime_services::StandardRuntimeServices, +}; +use r_efi::efi; + +use patina::component::service::dxe_dispatch::DxeDispatch; + +use patina::boot_services::BootServices; + +use crate::{boot_orchestrator::BootOrchestrator, config::BootConfig, helpers}; + +/// Interleave controller connection with DXE driver dispatch. +/// +/// Alternates between connecting all controllers and dispatching newly loaded +/// drivers (e.g., PCI option ROMs) until both the device topology stabilizes +/// and no new drivers are dispatched. +/// +/// This ensures that drivers loaded from firmware volumes during device +/// enumeration (such as PCI option ROM drivers) are dispatched before +/// continuing enumeration, allowing those drivers to bind to newly +/// discovered controllers. +fn interleave_connect_and_dispatch( + boot_services: &B, + dxe_services: &D, +) -> patina::error::Result<()> { + const MAX_ROUNDS: usize = 10; + + for _round in 0..MAX_ROUNDS { + helpers::connect_all(boot_services)?; + if !dxe_services.dispatch()? { + return Ok(()); + } + } + + debug_assert!(false, "connect-dispatch interleaving did not converge after {MAX_ROUNDS} rounds"); + + Ok(()) +} + +/// Simple boot manager implementing [`BootOrchestrator`]. +/// +/// Provides a default boot flow suitable for platforms with straightforward +/// boot topologies. +/// +/// ## Boot Flow +/// +/// 1. Interleave controller connection with DXE driver dispatch for device enumeration +/// 2. Signal EndOfDxe (security lockdown) +/// 3. Discover console devices +/// 4. Detect hotkey (if configured); select alternate devices if pressed +/// 5. Signal ReadyToBoot +/// 6. Iterate boot devices, attempt `LoadImage()`/`StartImage()` for each +/// 7. Call failure handler if all options exhausted +pub struct SimpleBootManager { + config: BootConfig, +} + +impl SimpleBootManager { + /// Create a `SimpleBootManager` from a boot configuration. + /// + /// ## Example + /// + /// ```rust,ignore + /// use patina_boot::{SimpleBootManager, config::BootConfig}; + /// + /// let manager = SimpleBootManager::new( + /// BootConfig::new(nvme_esp_path()) + /// .with_device(nvme_recovery_path()) + /// .with_hotkey(0x16) + /// .with_hotkey_device(usb_device_path()) + /// .with_failure_handler(|| show_error_screen("Boot failed")), + /// ); + /// ``` + pub fn new(config: BootConfig) -> Self { + Self { config } + } +} + +// Expose config for test assertions +#[cfg(test)] +impl SimpleBootManager { + pub(crate) fn config(&self) -> &BootConfig { + &self.config + } +} + +impl BootOrchestrator for SimpleBootManager { + #[coverage(off)] // Integration point — delegates to helper functions which are individually tested + fn execute( + &self, + boot_services: &StandardBootServices, + runtime_services: &StandardRuntimeServices, + dxe_dispatch: &dyn DxeDispatch, + image_handle: efi::Handle, + ) -> Result { + if let Err(e) = interleave_connect_and_dispatch(boot_services, dxe_dispatch) { + log::error!("interleave_connect_and_dispatch failed: {:?}", e); + } + + if let Err(e) = helpers::signal_bds_phase_entry(boot_services) { + log::error!("signal_bds_phase_entry failed: {:?}", e); + } + + if let Err(e) = helpers::discover_console_devices(boot_services, runtime_services) { + log::error!("discover_console_devices failed: {:?}", e); + } + + // Check for hotkey press after devices are connected and consoles discovered + let use_hotkey_devices = + if let Some(hotkey) = self.config.hotkey() { helpers::detect_hotkey(boot_services, hotkey) } else { false }; + + if let Err(e) = helpers::signal_ready_to_boot(boot_services) { + log::error!("signal_ready_to_boot failed: {:?}", e); + } + + // Select boot devices based on hotkey detection + let boot_devices: Vec<&DevicePathBuf> = if use_hotkey_devices { + log::info!("Using alternate boot options (hotkey detected)"); + self.config.hotkey_devices().collect() + } else { + self.config.devices().collect() + }; + + for device_path in boot_devices { + match helpers::boot_from_device_path(boot_services, image_handle, device_path) { + Ok(()) => { + // Boot image returned control (e.g., EFI application exited). + // Continue to try next boot option. + log::warn!("Boot option returned, trying next..."); + } + Err(_) => { + log::warn!("Boot option failed, trying next..."); + } + } + } + + self.config.handle_failure(); + log::error!("All boot options exhausted"); + Err(EfiError::NotFound) + } +} + +#[cfg(test)] +mod tests { + extern crate alloc; + + use super::*; + use alloc::{boxed::Box, sync::Arc}; + use core::sync::atomic::{AtomicBool, Ordering}; + use patina::{ + boot_services::{MockBootServices, boxed::BootServicesBox}, + device_path::{node_defs::EndEntire, paths::DevicePathBuf}, + }; + + fn test_device_path() -> DevicePathBuf { + DevicePathBuf::from_device_path_node_iter(core::iter::once(EndEntire)) + } + + // Tests for interleave_connect_and_dispatch + + struct MockDxeDispatcher { + results: spin::Mutex>>, + } + + impl MockDxeDispatcher { + fn new(results: &[patina::error::Result]) -> Self { + Self { results: spin::Mutex::new(results.iter().cloned().collect()) } + } + } + + impl DxeDispatch for MockDxeDispatcher { + fn dispatch(&self) -> patina::error::Result { + self.results.lock().pop_front().expect("MockDxeDispatcher: unexpected dispatch call") + } + } + + fn leaked_boot_services_for_box() -> &'static MockBootServices { + Box::leak(Box::new({ + let mut m = MockBootServices::new(); + m.expect_free_pool().returning(|_| Ok(())); + m + })) + } + + fn mock_handle_buffer( + handle_addrs: &[usize], + boot_services: &'static MockBootServices, + ) -> BootServicesBox<'static, [efi::Handle], MockBootServices> { + let handles: Vec = handle_addrs.iter().map(|&a| a as efi::Handle).collect(); + let leaked = handles.leak(); + // SAFETY: leaked is a valid pointer+length from Vec::leak. + unsafe { BootServicesBox::from_raw_parts_mut(leaked.as_mut_ptr(), leaked.len(), boot_services) } + } + + #[test] + fn test_interleave_single_round_no_drivers_dispatched() { + let box_mock = leaked_boot_services_for_box(); + let mut boot_mock = MockBootServices::new(); + + boot_mock.expect_locate_handle_buffer().returning(move |_| Ok(mock_handle_buffer(&[1], box_mock))); + boot_mock.expect_connect_controller().returning(|_, _, _, _| Ok(())); + + let dxe_mock = MockDxeDispatcher::new(&[Ok(false)]); + + let result = interleave_connect_and_dispatch(&boot_mock, &dxe_mock); + assert!(result.is_ok()); + } + + #[test] + fn test_interleave_multiple_rounds() { + let box_mock = leaked_boot_services_for_box(); + let mut boot_mock = MockBootServices::new(); + + boot_mock.expect_locate_handle_buffer().returning(move |_| Ok(mock_handle_buffer(&[1], box_mock))); + boot_mock.expect_connect_controller().returning(|_, _, _, _| Ok(())); + + let dxe_mock = MockDxeDispatcher::new(&[Ok(true), Ok(false)]); + + let result = interleave_connect_and_dispatch(&boot_mock, &dxe_mock); + assert!(result.is_ok()); + } + + #[test] + fn test_interleave_connect_failure_propagates() { + let mut boot_mock = MockBootServices::new(); + + boot_mock.expect_locate_handle_buffer().returning(|_| Err(efi::Status::NOT_FOUND)); + + let dxe_mock = MockDxeDispatcher::new(&[]); + + let result = interleave_connect_and_dispatch(&boot_mock, &dxe_mock); + assert!(result.is_err()); + } + + #[test] + fn test_interleave_dispatch_failure_propagates() { + let box_mock = leaked_boot_services_for_box(); + let mut boot_mock = MockBootServices::new(); + + boot_mock.expect_locate_handle_buffer().returning(move |_| Ok(mock_handle_buffer(&[1], box_mock))); + boot_mock.expect_connect_controller().returning(|_, _, _, _| Ok(())); + + let dxe_mock = MockDxeDispatcher::new(&[Err(EfiError::DeviceError)]); + + let result = interleave_connect_and_dispatch(&boot_mock, &dxe_mock); + assert!(result.is_err()); + } + + #[test] + fn test_interleave_stops_at_max_rounds() { + let box_mock = leaked_boot_services_for_box(); + let mut boot_mock = MockBootServices::new(); + + boot_mock.expect_locate_handle_buffer().returning(move |_| Ok(mock_handle_buffer(&[1], box_mock))); + boot_mock.expect_connect_controller().returning(|_, _, _, _| Ok(())); + + let dxe_mock = MockDxeDispatcher::new(&[Ok(true); 10]); + + let result = interleave_connect_and_dispatch(&boot_mock, &dxe_mock); + assert!(result.is_ok()); + } + + #[test] + fn test_new() { + let config = BootConfig::new(test_device_path()).with_hotkey(0x16).with_hotkey_device(test_device_path()); + let manager = SimpleBootManager::new(config); + assert_eq!(manager.config().hotkey(), Some(0x16)); + assert_eq!(manager.config().devices().count(), 1); + assert_eq!(manager.config().hotkey_devices().count(), 1); + } + + #[test] + fn test_with_hotkey() { + let config = BootConfig::new(test_device_path()) + .with_device(test_device_path()) + .with_hotkey(0x16) + .with_hotkey_device(test_device_path()); + let manager = SimpleBootManager::new(config); + assert_eq!(manager.config().hotkey(), Some(0x16)); + assert_eq!(manager.config().devices().count(), 2); + assert_eq!(manager.config().hotkey_devices().count(), 1); + } + + #[test] + fn test_without_hotkey() { + let config = BootConfig::new(test_device_path()).with_device(test_device_path()); + let manager = SimpleBootManager::new(config); + assert!(manager.config().hotkey().is_none()); + assert_eq!(manager.config().devices().count(), 2); + assert_eq!(manager.config().hotkey_devices().count(), 0); + } + + #[test] + fn test_with_failure_handler() { + let called = Arc::new(AtomicBool::new(false)); + let called_clone = called.clone(); + + let config = BootConfig::new(test_device_path()).with_failure_handler(move || { + called_clone.store(true, Ordering::SeqCst); + }); + let manager = SimpleBootManager::new(config); + + assert!(!called.load(Ordering::SeqCst)); + manager.config().handle_failure(); + assert!(called.load(Ordering::SeqCst)); + } +} diff --git a/cspell.yml b/cspell.yml index f53d4014a..2b85d5678 100644 --- a/cspell.yml +++ b/cspell.yml @@ -41,6 +41,7 @@ words: - armasm - autocfg - bitvec + - bootaa - bootx - bucketize - bumpalo diff --git a/patina_dxe_core/src/component_dispatcher.rs b/patina_dxe_core/src/component_dispatcher.rs index 4da367f02..0f379e031 100644 --- a/patina_dxe_core/src/component_dispatcher.rs +++ b/patina_dxe_core/src/component_dispatcher.rs @@ -123,7 +123,7 @@ impl ComponentDispatcher { /// Creates a new locked ComponentDispatcher. /// /// Uses TPL_APPLICATION so that component entry points can use boot services - /// that are restricted at higher TPL levels. + /// that are restricted at higher TPL levels (per UEFI spec Section 7.1). #[inline(always)] pub(crate) const fn new_locked() -> TplMutex { TplMutex::new(efi::TPL_APPLICATION, Self::new(), "ComponentDispatcher") @@ -180,7 +180,6 @@ impl ComponentDispatcher { /// Sets the core Image Handle in storage. #[coverage(off)] - #[inline(always)] pub(crate) fn set_image_handle(&mut self, handle: efi::Handle) { self.storage.set_image_handle(handle); } diff --git a/patina_dxe_core/src/lib.rs b/patina_dxe_core/src/lib.rs index 848060b04..c9de0deb8 100644 --- a/patina_dxe_core/src/lib.rs +++ b/patina_dxe_core/src/lib.rs @@ -514,42 +514,49 @@ impl Core

{ // Instantiate system table. systemtables::init_system_table(); - let mut st_guard = systemtables::SYSTEM_TABLE.lock(); - let st = st_guard.as_mut().expect("System Table not initialized!"); - - allocator::install_memory_services(st); - gcd::init_paging(self.hob_list()); - events::init_events_support(st); - protocols::init_protocol_support(st); - misc_boot_services::init_misc_boot_services_support(st); - config_tables::init_config_tables_support(st); - runtime::init_runtime_support(); - self.pi_dispatcher.init(self.hob_list(), st); - self.install_dxe_services_table(st); - driver_services::init_driver_services(st); - - memory_attributes_protocol::install_memory_attributes_protocol(); - - // re-checksum the system tables after above initialization. - st.checksum_all(); - - // Install HobList configuration table - config_tables::core_install_configuration_table(patina::guids::HOB_LIST.into_inner(), physical_hob_list, st) + // Extract boot/runtime services pointers while holding SYSTEM_TABLE lock (TPL_NOTIFY). + // We must release this lock before accessing component_dispatcher (TPL_APPLICATION) + // to avoid TPL violation - you cannot lower TPL while holding a higher TPL lock. + let (boot_services_ptr, runtime_services_ptr) = { + let mut st = systemtables::SYSTEM_TABLE.lock(); + let st = st.as_mut().expect("System Table not initialized!"); + + allocator::install_memory_services(st); + gcd::init_paging(self.hob_list()); + events::init_events_support(st); + protocols::init_protocol_support(st); + misc_boot_services::init_misc_boot_services_support(st); + config_tables::init_config_tables_support(st); + runtime::init_runtime_support(); + self.pi_dispatcher.init(self.hob_list(), st); + self.install_dxe_services_table(st); + driver_services::init_driver_services(st); + + memory_attributes_protocol::install_memory_attributes_protocol(); + + // re-checksum the system tables after above initialization. + st.checksum_all(); + + // Install HobList configuration table + config_tables::core_install_configuration_table( + patina::guids::HOB_LIST.into_inner(), + physical_hob_list, + st, + ) .expect("Unable to create configuration table due to invalid table entry."); - // Install Memory Type Info configuration table. - allocator::install_memory_type_info_table(st).expect("Unable to create Memory Type Info Table"); + // Install Memory Type Info configuration table. + allocator::install_memory_type_info_table(st).expect("Unable to create Memory Type Info Table"); - memory_attributes_table::init_memory_attributes_table_support(); + memory_attributes_table::init_memory_attributes_table_support(); - // The component dispatcher has a TPL_APPLICATION TPLMutex, so we need to drop the TPL_NOTIFY st_guard before - // attempting to unlock the component dispatcher to prevent TPL inversion - let boot_services = StandardBootServices::new(st.boot_services().as_mut_ptr()); - let runtime_services = StandardRuntimeServices::new(st.runtime_services().as_mut_ptr()); - drop(st_guard); + // Extract pointers before releasing the lock + (st.boot_services().as_mut_ptr(), st.runtime_services().as_mut_ptr()) + }; // SYSTEM_TABLE lock released here - self.component_dispatcher.lock().set_boot_services(boot_services); - self.component_dispatcher.lock().set_runtime_services(runtime_services); + // Now safe to access component_dispatcher at TPL_APPLICATION + self.component_dispatcher.lock().set_boot_services(StandardBootServices::new(boot_services_ptr)); + self.component_dispatcher.lock().set_runtime_services(StandardRuntimeServices::new(runtime_services_ptr)); self.component_dispatcher.lock().set_image_handle(protocol_db::DXE_CORE_HANDLE); Ok(()) diff --git a/sdk/patina/src/component/struct_component.rs b/sdk/patina/src/component/struct_component.rs index 537806625..473fd6f51 100644 --- a/sdk/patina/src/component/struct_component.rs +++ b/sdk/patina/src/component/struct_component.rs @@ -188,14 +188,21 @@ mod tests { fn test_component_run_handling_works_as_expected() { let mut storage = crate::component::storage::Storage::new(); + // Test component with Config - requires locked configs let mut test_struct = TestStructSuccess { x: 5 }.into_component(); test_struct.initialize(&mut storage); + storage.lock_configs(); // Lock configs so Config is accessible assert!(test_struct.run(&mut storage).is_ok_and(|res| res)); let mut test_enum = TestEnumSuccess::A.into_component(); test_enum.initialize(&mut storage); assert!(test_enum.run(&mut storage).is_ok_and(|res| res)); + // Unlock configs for the next test + let config_id = storage.register_config::(); + storage.unlock_config(config_id); + + // Test component with ConfigMut - configs are now locked, so it should fail let mut test_struct = TestStructNotDispatched { x: 5 }.into_component(); test_struct.initialize(&mut storage); storage.lock_configs(); // Lock it so the ConfigMut can't be accessed diff --git a/sdk/patina/src/device_path/node_defs.rs b/sdk/patina/src/device_path/node_defs.rs index 3d3d3a0be..e9e8f96c4 100644 --- a/sdk/patina/src/device_path/node_defs.rs +++ b/sdk/patina/src/device_path/node_defs.rs @@ -659,6 +659,16 @@ impl HardDrive { /// Signature type: MBR 32-bit signature pub const SIGNATURE_TYPE_MBR: u8 = 0x01; + /// Try to parse a `HardDrive` from a raw device path node. + /// + /// Returns `None` if the node type/subtype doesn't match or the data is malformed. + pub fn try_from_node(node: &parse_node::UnknownDevicePathNode<'_>) -> Option { + if !Self::is_type(node.header.r#type, node.header.sub_type) { + return None; + } + node.data.pread_with(0, scroll::LE).ok() + } + /// Create a new HardDrive device path node for a GPT partition. /// /// # Arguments diff --git a/sdk/patina/src/device_path/paths.rs b/sdk/patina/src/device_path/paths.rs index 3ec984b26..e5ee37fe1 100644 --- a/sdk/patina/src/device_path/paths.rs +++ b/sdk/patina/src/device_path/paths.rs @@ -214,6 +214,11 @@ impl DevicePath { self.buffer.len() } + /// Return the raw byte representation of the device path. + pub fn as_bytes(&self) -> &[u8] { + &self.buffer + } + /// Return the number of nodes in the device path. pub fn node_count(&self) -> usize { self.iter().count() diff --git a/sdk/patina/src/guids.rs b/sdk/patina/src/guids.rs index ed4ed92dc..fac07170f 100644 --- a/sdk/patina/src/guids.rs +++ b/sdk/patina/src/guids.rs @@ -259,6 +259,19 @@ pub const SMM_COMMUNICATION_PROTOCOL: crate::BinaryGuid = /// ``` pub const ZERO: crate::BinaryGuid = crate::BinaryGuid::from_string("00000000-0000-0000-0000-000000000000"); +/// EFI Global Variable GUID +/// +/// The namespace GUID for UEFI-defined global variables such as `ConIn`, `ConOut`, +/// `ErrOut`, `Boot####`, `BootOrder`, etc. See UEFI Specification Table 3-1. +/// +/// (`8BE4DF61-93CA-11D2-AA0D-00E098032B8C`) +/// ``` +/// # use patina::{Guid, guids::EFI_GLOBAL_VARIABLE}; +/// # assert_eq!("8BE4DF61-93CA-11D2-AA0D-00E098032B8C", format!("{:?}", Guid::from_ref(&EFI_GLOBAL_VARIABLE))); +/// ``` +pub const EFI_GLOBAL_VARIABLE: crate::BinaryGuid = + crate::BinaryGuid::from_string("8BE4DF61-93CA-11D2-AA0D-00E098032B8C"); + /// EFI_HOB_MEMORY_ALLOC_STACK_GUID /// /// Describes the memory stack that is produced by the HOB producer phase and upon which all post