diff --git a/components/patina_acpi/src/acpi_table.rs b/components/patina_acpi/src/acpi_table.rs index 503cb8255..4aeee5259 100644 --- a/components/patina_acpi/src/acpi_table.rs +++ b/components/patina_acpi/src/acpi_table.rs @@ -20,6 +20,7 @@ use patina::{ component::service::{ Service, memory::{AllocationOptions, MemoryManager, PageAllocationStrategy}, + uefi_services::config_table::ConfigTable, }, uefi::memory::EfiMemoryType, uefi_size_to_pages, @@ -212,6 +213,10 @@ pub struct AcpiRsdp { pub(crate) reserved: [u8; 3], } +impl ConfigTable for AcpiRsdp { + const TABLE_GUID: patina::BinaryGuid = signature::ACPI_TABLE_GUID; +} + /// Represents the XSDT for ACPI 2.0+. /// The XSDT has a standard header followed by 64-bit addresses of installed tables. /// The `length` field of the header tells us the number of trailing bytes representing table entries. diff --git a/components/patina_samples/src/component.rs b/components/patina_samples/src/component.rs index 951ff24d2..89e398722 100644 --- a/components/patina_samples/src/component.rs +++ b/components/patina_samples/src/component.rs @@ -10,3 +10,4 @@ //! SPDX-License-Identifier: Apache-2.0 //! pub mod hello_world; +pub mod uefi_services; diff --git a/components/patina_samples/src/component/uefi_services.rs b/components/patina_samples/src/component/uefi_services.rs new file mode 100644 index 000000000..9cf38c5e7 --- /dev/null +++ b/components/patina_samples/src/component/uefi_services.rs @@ -0,0 +1,32 @@ +//! UEFI Services Sample Components +//! +//! This module collects sample components that demonstrate the Patina UEFI Services from +//! [`patina::component::service::uefi_services`]. Each sample is a small, self-contained +//! component: +//! +//! - [`overview`] - A quick tour of timer, event, and protocol usage in one component. +//! - [`configuration_table`] - Installs a vendor configuration table and reads it back. +//! - [`driver_connect`] - Discovers controllers by protocol and connects drivers to them. +//! - [`end_of_dxe_protocol_consumer`] - Defers protocol consumption to End-of-DXE. +//! - [`protocol_consumer`] - Shows different ways to consume a protocol. +//! - [`protocol_publisher`] - Demonstrates one component publishing a protocol and another consuming it. +//! - [`timers`] - Drives work from one-shot and periodic timers using Rust closures. +//! - [`tpl_critical_section`] - Serializes access to shared state with TPL. +//! +//! ## License +//! +//! Copyright (c) Microsoft Corporation. +//! +//! SPDX-License-Identifier: Apache-2.0 +//! + +pub mod configuration_table; +pub mod driver_connect; +pub mod end_of_dxe_protocol_consumer; +pub mod overview; +pub mod protocol_consumer; +pub mod protocol_publisher; +pub mod timers; +pub mod tpl_critical_section; + +pub use overview::UefiServicesSample; diff --git a/components/patina_samples/src/component/uefi_services/configuration_table.rs b/components/patina_samples/src/component/uefi_services/configuration_table.rs new file mode 100644 index 000000000..0502967e9 --- /dev/null +++ b/components/patina_samples/src/component/uefi_services/configuration_table.rs @@ -0,0 +1,161 @@ +//! Configuration Table Sample Component +//! +//! This component demonstrates [`ConfigurationTableServices`] by installing a vendor configuration +//! table under a GUID and reading it back. Configuration tables are how firmware publishes system-wide, +//! pointer-addressable tables such as ACPI RSDP. An OS or later component finds them by GUID in the UEFI +//! system table. +//! +//! [`SampleVendorTable`] implements [`ConfigTable`], which binds it to a GUID at the type level. This +//! lets the component install and retrieve it through [`ConfigurationTableServicesExt::install`] and +//! [`ConfigurationTableServicesExt::get`]. Neither method takes a raw pointer or directly requires the +//! GUID, so they're relatively straightforward and simple to use. `get` is not `unsafe`, because the +//! service verifies the installed type before casting the pointer. +//! +//! [`ConfigTable`] only supports tables whose size is known at compile time, but that type can be a +//! self-describing header whose own field covers trailing data laid out after it in the same allocation. +//! See [`SampleDynamicHeader`] below, for an example. Tables whose header type isn't owned by the installing +//! code at all must fall back to using [`ConfigurationTableServices::install_table`] with a raw +//! [`ConfigTablePtr`](patina::component::service::uefi_services::config_table::ConfigTablePtr) instead. +//! +//! ## License +//! +//! Copyright (c) Microsoft Corporation. +//! +//! SPDX-License-Identifier: Apache-2.0 +//! + +use patina::{ + BinaryGuid, + component::{ + component, + service::{ + Service, + uefi_services::config_table::{ConfigTable, ConfigurationTableServices, ConfigurationTableServicesExt}, + }, + }, + error::Result, +}; + +/// A sample vendor table laid out the way a typical firmware table would be as `#[repr(C)]`, with a +/// signature and version so consumers can validate it. +#[repr(C)] +pub struct SampleVendorTable { + /// Four-character signature identifying the table (`b"PTNA"`). + pub signature: [u8; 4], + /// Table format version. + pub version: u32, + /// Number of vendor-defined entries that follow in the real table. + pub entry_count: u32, +} + +impl ConfigTable for SampleVendorTable { + /// GUID under which the table is installed in the system table. + const TABLE_GUID: BinaryGuid = BinaryGuid::from_string("0fedcba9-8765-4321-fedc-ba9876543210"); +} + +/// The table instance. It must be `'static` as the system table stores a pointer to it that outlives +/// this component's entry point. +static VENDOR_TABLE: SampleVendorTable = SampleVendorTable { signature: *b"PTNA", version: 1, entry_count: 0 }; + +/// Installs [`SampleVendorTable`] into the system configuration table, then reads it back to +/// confirm the table was installed correctly. +#[derive(Default)] +pub struct ConfigurationTableSample; + +#[component] +impl ConfigurationTableSample { + /// Creates a new instance of the component. + pub fn new() -> Self { + Self + } + + fn entry_point(self, config: Service) -> Result<()> { + // Two methods are available to install configuration tables: + // 1. `install` - As shown below. This will return `ConfigTableError::AlreadyExists` instead of + // silently replacing an existing table, if a table is already installed under + // `SampleVendorTable::TABLE_GUID`. + // 2. `install_or_replace` - Replaces any existing table under the same GUID. This is useful + // for tables that may be updated or re-published during boot. + config.install(&VENDOR_TABLE)?; + log::info!("Installed SampleVendorTable v{}", VENDOR_TABLE.version); + + // Read it back. Note that `unsafe` is not needed as the service only returns a table here if it was + // installed as `SampleVendorTable`, which is what `install` did above. + if let Some(table) = config.get::() { + log::info!( + "Read back signature {:?}, {} entries", + core::str::from_utf8(&table.signature).unwrap_or("????"), + table.entry_count + ); + } + + Ok(()) + } +} + +const ENTRY_COUNT: u32 = 3; + +/// A sample vendor table with a self-describing header where `total_len` covers this header plus the +/// `entries` that follow it in memory. +#[repr(C)] +pub struct SampleDynamicHeader { + /// Four-character signature identifying the table (`b"PTNB"`). + pub signature: [u8; 4], + /// Total size of the table, in bytes, including this header and the trailing entries. + pub total_len: u32, + /// Number of trailing `u32` entries. + pub entry_count: u32, +} + +impl ConfigTable for SampleDynamicHeader { + /// GUID under which the table is installed in the system table. + const TABLE_GUID: BinaryGuid = BinaryGuid::from_string("1a2b3c4d-5e6f-4788-99aa-bbccddeeff00"); + + fn table_len(&self) -> usize { + self.total_len as usize + } +} + +/// The header and its trailing entries are laid out contiguously so `SampleDynamicHeader::table_len` +/// describes the whole allocation starting at `header`'s address. +#[repr(C)] +struct SampleDynamicTable { + header: SampleDynamicHeader, + entries: [u32; ENTRY_COUNT as usize], +} + +static DYNAMIC_TABLE: SampleDynamicTable = SampleDynamicTable { + header: SampleDynamicHeader { + signature: *b"PTNB", + total_len: core::mem::size_of::() as u32, + entry_count: ENTRY_COUNT, + }, + entries: [10, 20, 30], +}; + +/// Installs [`SampleDynamicHeader`] into the system configuration table. This pattern can be used for a +/// self-describing header type, with trailing data laid out after it in the same allocation. +#[derive(Default)] +pub struct DynamicConfigurationTableSample; + +#[component] +impl DynamicConfigurationTableSample { + /// Creates a new instance of the component. + pub fn new() -> Self { + Self + } + + fn entry_point(self, config: Service) -> Result<()> { + config.install(&DYNAMIC_TABLE.header)?; + log::info!("Installed SampleDynamicHeader with {} entries", DYNAMIC_TABLE.header.entry_count); + + // SAFETY: `table_len` is implemented to report `DYNAMIC_TABLE`'s allocated size (header plus entries), + // matching what was installed above. + if let Some(bytes) = unsafe { config.get_bytes::() } { + let entries = bytes.get(core::mem::size_of::()..).unwrap_or(&[]); + log::info!("Read back {} bytes, including the trailing entries: {:?}", bytes.len(), entries); + } + + Ok(()) + } +} diff --git a/components/patina_samples/src/component/uefi_services/driver_connect.rs b/components/patina_samples/src/component/uefi_services/driver_connect.rs new file mode 100644 index 000000000..f98f604f9 --- /dev/null +++ b/components/patina_samples/src/component/uefi_services/driver_connect.rs @@ -0,0 +1,61 @@ +//! Driver Connect Sample Component +//! +//! This component demonstrates combining [`ProtocolServices`] with [`DriverServices`]. It connects +//! a driver to each Block I/O controller as the protocol is installed, rather than waiting for a +//! fixed point in boot. Connecting drivers is commonly used to bring up a device stack (for example, +//! binding a disk driver onto every Block I/O controller). +//! +//! ## License +//! +//! Copyright (c) Microsoft Corporation. +//! +//! SPDX-License-Identifier: Apache-2.0 +//! + +use patina::standard::efi::protocols::block_io::Protocol as BlockIo; +use patina::{ + component::{ + component, + service::{ + Service, + uefi_services::{ + driver::DriverServices, + protocol::{ProtocolServices, ProtocolServicesExt, Tpl}, + }, + }, + }, + error::Result, +}; + +/// Connects a driver to each Block I/O controller as it is installed. +#[derive(Default)] +pub struct DriverConnectSample; + +#[component] +impl DriverConnectSample { + /// Creates a new instance of the component. + pub fn new() -> Self { + Self + } + + fn entry_point(self, protocols: Service, drivers: Service) -> Result<()> { + // Runs for every handle that already exposes Block I/O and for every future install. + protocols.on_protocol_installed::(Tpl::Callback, move |controller| { + log::info!("Block I/O controller installed: {controller:?}"); + + if let Ok(controllers) = protocols.locate_handles_for::() { + log::info!("There are currently {} Block I/O controllers installed", controllers.len()); + } + + // `recursive = true` also connects any child controllers the driver produces, bringing + // up the device tree. A failure to connect one controller should not stop future + // notifications, so log and continue. + log::info!("Calling connect_controller for {controller:?}"); + if let Err(err) = drivers.connect_controller(controller, true) { + log::warn!("Failed to connect {controller:?}: {err:?}"); + } + })?; + + Ok(()) + } +} diff --git a/components/patina_samples/src/component/uefi_services/end_of_dxe_protocol_consumer.rs b/components/patina_samples/src/component/uefi_services/end_of_dxe_protocol_consumer.rs new file mode 100644 index 000000000..c17ecbd77 --- /dev/null +++ b/components/patina_samples/src/component/uefi_services/end_of_dxe_protocol_consumer.rs @@ -0,0 +1,67 @@ +//! End-of-DXE Protocol Consumer Sample Component +//! +//! This component demonstrates deferring protocol consumption to an event group with +//! [`EventServicesExt`], rather than locating the protocol during normal component dispatch. It +//! registers a callback for [`END_OF_DXE_EVENT_GROUP_GUID`], the event group signaled once at the +//! end of the DXE phase, before BDS. When the callback runs, it locates the [`SampleVendorProtocol`] +//! published by the protocol publisher sample and logs the status returned through its interface. +//! +//! This pattern suits a component that must consume a protocol that may not be published yet when +//! it dispatches, without taking an explicit dispatch dependency on the publisher. +//! +//! ## License +//! +//! Copyright (c) Microsoft Corporation. +//! +//! SPDX-License-Identifier: Apache-2.0 +//! + +use patina::{ + component::{ + component, + service::{ + Service, + uefi_services::{ + event::{EventServices, EventServicesExt, Tpl}, + protocol::{ProtocolServices, ProtocolServicesExt}, + }, + }, + }, + error::Result, + pi::event::END_OF_DXE_EVENT_GROUP_GUID, +}; + +use super::protocol_publisher::SampleVendorProtocol; + +/// Registers an End-of-DXE callback that locates [`SampleVendorProtocol`] and logs its status. +#[derive(Default)] +pub struct EndOfDxeProtocolConsumerSample; + +#[component] +impl EndOfDxeProtocolConsumerSample { + /// Creates a new instance of the component. + pub fn new() -> Self { + Self + } + + fn entry_point(self, events: Service, protocols: Service) -> Result<()> { + // This example checks for a protocol at End of DXE. The callback fires once every + // event that shares the group (including this one) is signaled. move is used to + // capture the protocols service by value for use in the closure. This allows the + // closure to take ownership of the protocols service so it can be used when the + // callback runs in the future (after this entry_point function returns). + events.on_event_group(END_OF_DXE_EVENT_GROUP_GUID, Tpl::Callback, move || { + match protocols.locate_protocol::() { + Ok(protocol) => { + let status = (protocol.get_status)(); + log::info!("End of DXE: sample_get_status returned {status:#x}"); + } + Err(_) => log::debug!("End of DXE: SampleVendorProtocol not published"), + } + })?; + + log::info!("Registered End-of-DXE callback for SampleVendorProtocol"); + + Ok(()) + } +} diff --git a/components/patina_samples/src/component/uefi_services/overview.rs b/components/patina_samples/src/component/uefi_services/overview.rs new file mode 100644 index 000000000..0249dd65c --- /dev/null +++ b/components/patina_samples/src/component/uefi_services/overview.rs @@ -0,0 +1,86 @@ +//! UEFI Services Overview Sample Component +//! +//! This component demonstrates using the Patina UEFI Services from +//! [`patina::component::service::uefi_services`] in some common patterns. +//! +//! For samples of how to use individual services, see the other sibling modules: +//! +//! - [`super::configuration_table`] - Installing and reading a vendor configuration table. +//! - [`super::driver_connect`] - Locating controllers and connecting drivers to them. +//! - [`super::end_of_dxe_protocol_consumer`] - Deferring protocol consumption to the End-of-DXE event group. +//! - [`super::protocol_consumer`] - Different approaches to consume a protocol. +//! - [`super::protocol_publisher`] - Publishing a protocol for other components to consume. +//! - [`super::timers`] - One-shot and periodic timers used with Rust closures. +//! - [`super::tpl_critical_section`] - Guarding shared state with a raised task priority level. +//! +//! ## License +//! +//! Copyright (c) Microsoft Corporation. +//! +//! SPDX-License-Identifier: Apache-2.0 +//! + +use alloc::boxed::Box; +use core::sync::atomic::{AtomicU32, Ordering}; +use core::time::Duration; + +use patina::standard::efi::protocols::graphics_output::Protocol as GraphicsOutput; +use patina::{ + component::{ + component, + service::{ + Service, + uefi_services::{ + protocol::{ProtocolServices, ProtocolServicesExt}, + timer_event::{TimerEventServices, TimerType, Tpl}, + timing::TimingServices, + }, + }, + }, + error::Result, +}; + +/// Counts how many times the sample timer has fired. Shared with the timer's notification closure. +static TICK_COUNT: AtomicU32 = AtomicU32::new(0); + +/// A sample component that consumes the timing, event, and protocol UEFI services. +#[derive(Default)] +pub struct UefiServicesSample; + +#[component] +impl UefiServicesSample { + /// Creates a new instance of the component. + pub fn new() -> Self { + Self + } + + fn entry_point( + self, + timing: Service, + timer_events: Service, + protocols: Service, + ) -> Result<()> { + // Using the timing service to stall for one millisecond. + timing.stall(Duration::from_millis(1))?; + + // Create a periodic timer whose Rust closure runs on each tick. The closure is + // owned by the event and dropped when the event is closed. + let timer = timer_events.create_timer_event( + Tpl::Callback, + Box::new(|| { + TICK_COUNT.fetch_add(1, Ordering::Relaxed); + }), + )?; + // Fire every 10 milliseconds. + timer_events.set_timer(timer, TimerType::Periodic(Duration::from_millis(10)))?; + + // Locate a protocol in safe code. The returned value is a reference bound to the + // protocol's interface type. The component never handles a raw pointer or GUID. + match protocols.locate_protocol::() { + Ok(_gop) => log::info!("Graphics Output Protocol is available"), + Err(_) => log::debug!("Graphics Output Protocol not present at this time"), + } + + Ok(()) + } +} diff --git a/components/patina_samples/src/component/uefi_services/protocol_consumer.rs b/components/patina_samples/src/component/uefi_services/protocol_consumer.rs new file mode 100644 index 000000000..be3df1800 --- /dev/null +++ b/components/patina_samples/src/component/uefi_services/protocol_consumer.rs @@ -0,0 +1,95 @@ +//! Protocol Consumer Sample Component +//! +//! Depending on how a protocol needs to be used, there are different approaches to consuming it. +//! This sample shows the four access styles that [`ProtocolServicesExt`] provides and explains +//! when each is useful. It consumes the [`SampleVendorProtocol`] published by the protocol +//! publisher sample. +//! +//! The approaches below are sorted from shortest-lived to longest-lived. Try to use the shortest +//! lived approach +//! +//! 1. `with_protocol` runs a closure with the interface. Use it for a single, immediate use. +//! 2. `open_protocol` returns a guard that dereferences to the interface for a block. Use it when +//! several statements in one scope need the interface. +//! 3. `locate_token` returns a token that stores only a handle. Use it to keep a reference for a +//! longer period of time such as throughout boot. Call `resolve` to re-validate on each use. +//! 4. `on_protocol_installed` runs a callback for every present and future install. Use it when +//! code needs to run when the protocol is published. +//! +//! ## License +//! +//! Copyright (c) Microsoft Corporation. +//! +//! SPDX-License-Identifier: Apache-2.0 +//! + +use patina::{ + component::{ + component, + service::{ + Service, + uefi_services::protocol::{ProtocolServices, ProtocolServicesExt, Tpl}, + }, + }, + error::Result, +}; + +use super::protocol_publisher::SampleVendorProtocol; + +/// Demonstrates the four ways to consume a protocol over its lifetime. +#[derive(Default)] +pub struct ProtocolConsumerSample; + +#[component] +impl ProtocolConsumerSample { + /// Creates a new instance of the component. + pub fn new() -> Self { + Self + } + + fn entry_point(self, protocols: Service) -> Result<()> { + // Option 1: with_protocol. Best for a single immediate use like calling a function on + // the protocol. The closure borrows the interface and returns a plain value, so nothing + // outlives the call. + match protocols.with_protocol::(|protocol| (protocol.get_status)()) { + Ok(status) => log::info!("with_protocol() read status is {status:#x}"), + Err(_) => log::debug!("SampleVendorProtocol is not present"), + } + + // Option 2 and 3 act on a specific handle, so first find one that has the protocol. + if let Some(handle) = protocols.locate_handles_for::()?.into_iter().next() { + // Option 2: open_protocol. Best when a block needs the interface across several + // statements. The guard dereferences to the interface and releases access at the end + // of the block. + { + let protocol = protocols.open_protocol::(handle)?; + log::info!("open_protocol() revision is {:#x}", protocol.revision); + log::info!("open_protocol() status is {:#x}", (protocol.get_status)()); + } + + // Option 3: locate_token then resolve. Best for using a protocol instance over time. The + // token holds only a handle and doesn't dangle. resolve re-validates and returns None if the + // interface has been uninstalled since the token was created. A real component could + // store the token and resolve it as needed. + let token = protocols.locate_token::()?; + match protocols.resolve(&token) { + Some(protocol) => log::info!("Protocol token resolved, status is {:#x}", (protocol.get_status)()), + None => log::debug!("Token no longer valid"), + } + } + + // Option 4: on_protocol_installed. Best when another component publishes the protocol later. + // The callback runs for handles already present and for every future install, until the + // registration is cancelled. + let registration = protocols.on_protocol_installed::(Tpl::Callback, |handle| { + log::info!("SampleVendorProtocol notification: Protocol is installed on {handle:?}"); + })?; + + // The callback stays active only while the registration is alive. Dropping it does not cancel + // it. This sample cancels immediately to demonstrate that call. To cancel it later, the + // NotifyRegistration value needs to be stored somewhere to pass to cancel in the future. + protocols.cancel(registration)?; + + Ok(()) + } +} diff --git a/components/patina_samples/src/component/uefi_services/protocol_publisher.rs b/components/patina_samples/src/component/uefi_services/protocol_publisher.rs new file mode 100644 index 000000000..744f14837 --- /dev/null +++ b/components/patina_samples/src/component/uefi_services/protocol_publisher.rs @@ -0,0 +1,104 @@ +//! Protocol Publisher Sample Component +//! +//! These demonstrate the producer side of [`ProtocolServices`]. This component publishes a +//! protocol interface, and another consumes it. +//! +//! This is the pattern to use when a component must expose functionality to code that is not part +//! of the Patina component model (for example, a UEFI driver written in C), or when interoperating +//! with the wider UEFI protocol database. Otherwise, producing a Patina component service should be +//! preferred. +//! +//! The interface is a plain `#[repr(C)]` struct bound to a GUID via [`ProtocolInterface`]. Neither +//! component ever handles a raw pointer or GUID directly. [`ProtocolServicesExt`] methods derive +//! the GUID from the interface type. +//! +//! ## License +//! +//! Copyright (c) Microsoft Corporation. +//! +//! SPDX-License-Identifier: Apache-2.0 +//! + +use patina::{ + BinaryGuid, + component::{ + component, + service::{ + Service, + uefi_services::protocol::{ProtocolServices, ProtocolServicesExt}, + }, + }, + error::Result, + protocol::ProtocolInterface, +}; + +/// A sample vendor protocol exposing a single `revision` field and a function pointer. +/// +/// UEFI protocols must be a `#[repr(C)]` struct of data with `extern "efiapi"` function pointers. +/// The layout must match what consumers expect, which is encoded by [`ProtocolInterface`]. +#[repr(C)] +pub struct SampleVendorProtocol { + /// Interface revision, so consumers can detect the layout they are talking to. + pub revision: u64, + /// Returns a vendor-defined status value. + pub get_status: extern "efiapi" fn() -> u64, +} + +// SAFETY: `SampleVendorProtocol` is `#[repr(C)]` and this GUID is used consistently for both the +// the install (publisher) and locate (consumer) paths in this sample, so the GUID correctly indicates +// the layout of the protocol binary interface. +unsafe impl ProtocolInterface for SampleVendorProtocol { + const PROTOCOL_GUID: BinaryGuid = BinaryGuid::from_string("a1b2c3d4-e5f6-4789-abcd-ef0123456789"); +} + +/// The `get_status` implementation backing the published interface. +extern "efiapi" fn sample_get_status() -> u64 { + 0x1234_5678 +} + +/// The interface instance. It must live for `'static` because the protocol database stores a +/// pointer to it for the lifetime of boot services. +static SAMPLE_PROTOCOL: SampleVendorProtocol = + SampleVendorProtocol { revision: 0x0001_0000, get_status: sample_get_status }; + +/// Publishes [`SampleVendorProtocol`] on a new handle so other components (or drivers) can find it. +#[derive(Default)] +pub struct ProtocolPublisherSample; + +#[component] +impl ProtocolPublisherSample { + /// Creates a new instance of the component. + pub fn new() -> Self { + Self + } + + fn entry_point(self, protocols: Service) -> Result<()> { + // Passing `None` for the handle asks the core to create a fresh handle for the interface. + let handle = protocols.install_protocol::(None, &SAMPLE_PROTOCOL)?; + log::info!("published SampleVendorProtocol on handle {handle:?}"); + Ok(()) + } +} + +/// Consumes [`SampleVendorProtocol`] by locating it and calling through its interface. +#[derive(Default)] +pub struct ProtocolConsumerSample; + +#[component] +impl ProtocolConsumerSample { + /// Creates a new instance of the component. + pub fn new() -> Self { + Self + } + + fn entry_point(self, protocols: Service) -> Result<()> { + match protocols.locate_protocol::() { + Ok(protocol) => { + let status = (protocol.get_status)(); + log::info!("SampleVendorProtocol rev {:#x} returned status {status:#x}", protocol.revision); + } + Err(_) => log::debug!("SampleVendorProtocol is not published yet"), + } + Ok(()) + } +} diff --git a/components/patina_samples/src/component/uefi_services/timers.rs b/components/patina_samples/src/component/uefi_services/timers.rs new file mode 100644 index 000000000..7c7f396c2 --- /dev/null +++ b/components/patina_samples/src/component/uefi_services/timers.rs @@ -0,0 +1,90 @@ +//! Timer and Event Sample Component +//! +//! This component demonstrates [`TimerEventServicesExt`] timers. [`TimerEventServices`] is only +//! registered by the DXE Core once the Timer Architectural Protocol, the protocol backing the +//! `SetTimer()` boot service, is installed, so this component is simply not dispatched until +//! timers can be used. It shows a **one-shot** timer that fires once after a delay and a +//! **periodic** timer that fires repeatedly. Notifications are ordinary Rust closures. The +//! closure is owned by the event and dropped when the event is closed. +//! +//! Because a timer closure runs asynchronously at a raised task priority level, it communicates +//! with the rest of the component through `'static` atomics rather than captured borrows. +//! +//! ## License +//! +//! Copyright (c) Microsoft Corporation. +//! +//! SPDX-License-Identifier: Apache-2.0 +//! + +use core::sync::atomic::{AtomicBool, AtomicU32, Ordering}; +use core::time::Duration; + +use patina::{ + component::{ + component, + service::{ + Service, + uefi_services::{ + event::EventServices, + timer_event::{TimerEventServices, TimerEventServicesExt, TimerType, Tpl}, + timing::TimingServices, + }, + }, + }, + error::Result, +}; + +/// Set to `true` by the one-shot timer's closure when it fires. +static ONE_SHOT_FIRED: AtomicBool = AtomicBool::new(false); +/// Incremented by the periodic timer's closure on every tick. +static PERIODIC_TICKS: AtomicU32 = AtomicU32::new(0); + +/// Arms a one-shot timer and a periodic timer. Dispatched only once the Timer Architectural +/// Protocol is installed. +#[derive(Default)] +pub struct TimerSample; + +#[component] +impl TimerSample { + /// Creates a new instance of the component. + pub fn new() -> Self { + Self + } + + fn entry_point( + self, + timer_events: Service, + timing: Service, + events: Service, + ) -> Result<()> { + // Fire once, 50 ms from now. `TimerType::Relative` schedules a single fire. + let one_shot = timer_events.on_timer_event(Tpl::Callback, || { + ONE_SHOT_FIRED.store(true, Ordering::Relaxed); + log::info!("Logged from the one-shot timer event"); + })?; + timer_events.set_timer(one_shot, TimerType::Relative(Duration::from_millis(5)))?; + + // Fire every 10 ms until cancelled. `TimerType::Periodic` re-arms automatically. + let periodic = timer_events.on_timer_event(Tpl::Callback, || { + PERIODIC_TICKS.fetch_add(1, Ordering::Relaxed); + log::info!("Logged from the periodic timer event"); + })?; + timer_events.set_timer(periodic, TimerType::Periodic(Duration::from_millis(10)))?; + + log::info!("Armed one-shot (50 ms) and periodic (10 ms) timers"); + + // Give the one shot timer enough time to fire before cancelling it. + // There should be at least 5 ticks of the periodic timer during this time, + // as well but the exact number is not guaranteed. + timing.stall(Duration::from_millis(50))?; + + // A component that only needed the one-shot would cancel and close it once done. Here we + // close the one-shot event to show the cleanup path; closing drops its closure. The + // periodic timer is left running to demonstrate a long-lived event. + timer_events.set_timer(one_shot, TimerType::Cancel)?; + events.close_event(one_shot)?; + + Ok(()) + } +} diff --git a/components/patina_samples/src/component/uefi_services/tpl_critical_section.rs b/components/patina_samples/src/component/uefi_services/tpl_critical_section.rs new file mode 100644 index 000000000..bc446d8c2 --- /dev/null +++ b/components/patina_samples/src/component/uefi_services/tpl_critical_section.rs @@ -0,0 +1,72 @@ +//! TPL Critical Section Sample Component +//! +//! This component demonstrates [`TplServices`] for serializing access to state shared with an +//! asynchronous event notification. It raises the task priority level (TPL) in `Notify` blocks. +//! +//! The recommended form to use is +//! [`with_raised_tpl`](patina::component::service::uefi_services::tpl::TplServicesExt::with_raised_tpl), +//! which raises the TPL, runs a closure, and restores the previous level even if the closure +//! returns early. [`raise`](patina::component::service::uefi_services::tpl::TplServicesExt::raise) +//! returns a guard for cases where the critical section spans a whole block and TPL should be +//! restored when the block ends. +//! +//! ## License +//! +//! Copyright (c) Microsoft Corporation. +//! +//! SPDX-License-Identifier: Apache-2.0 +//! + +use core::sync::atomic::{AtomicU32, Ordering}; + +use patina::{ + component::{ + component, + service::{ + Service, + uefi_services::tpl::{Tpl, TplServices, TplServicesExt}, + }, + }, + error::Result, +}; + +/// A pair of counters that must always be kept in sync. An interrupting notification that observed +/// them mid-update would see an inconsistent pair, so updates happen inside a raised-TPL section. +static PRIMARY_COUNT: AtomicU32 = AtomicU32::new(0); +static MIRROR_COUNT: AtomicU32 = AtomicU32::new(0); + +/// Updates two related counters atomically with respect to notifications by raising the TPL. +#[derive(Default)] +pub struct TplCriticalSectionSample; + +#[component] +impl TplCriticalSectionSample { + /// Creates a new instance of the component. + pub fn new() -> Self { + Self + } + + fn entry_point(self, tpl: Service) -> Result<()> { + // Option 1: The closure runs at TPL_NOTIFY, and the previous level is restored + // automatically when it returns. + tpl.with_raised_tpl(Tpl::Notify, || { + let next = PRIMARY_COUNT.load(Ordering::Relaxed) + 1; + PRIMARY_COUNT.store(next, Ordering::Relaxed); + MIRROR_COUNT.store(next, Ordering::Relaxed); + }); + + // Option 2: Equivalent guard-based form for a critical section that spans a block. + // The TPL stays raised until `_guard` is dropped at the end of the scope. + { + let _guard = tpl.raise(Tpl::Notify); + let next = PRIMARY_COUNT.load(Ordering::Relaxed) + 1; + PRIMARY_COUNT.store(next, Ordering::Relaxed); + MIRROR_COUNT.store(next, Ordering::Relaxed); + } // previous TPL is restored here + + debug_assert_eq!(PRIMARY_COUNT.load(Ordering::Relaxed), MIRROR_COUNT.load(Ordering::Relaxed)); + log::info!("Counters were kept consistent under raised TPL"); + + Ok(()) + } +} diff --git a/components/patina_samples/src/lib.rs b/components/patina_samples/src/lib.rs index 75c04e863..89bb97393 100644 --- a/components/patina_samples/src/lib.rs +++ b/components/patina_samples/src/lib.rs @@ -7,6 +7,7 @@ //! //! - [`component::hello_world::HelloStruct`]: Demonstrates a struct-based component with default entry point //! - [`component::hello_world::GreetingsEnum`]: Demonstrates an enum-based component with custom entry point +//! - [`component::uefi_services`]: Demonstrates using Patina UEFI Services //! - [`smbios_platform`]: Demonstrates SMBIOS platform configuration and record creation //! //! ## License @@ -19,5 +20,8 @@ #![deny(missing_docs)] #![cfg_attr(coverage, feature(coverage_attribute))] #![cfg_attr(coverage, coverage(off))] // Disable all coverage instrumentation for sample code + +extern crate alloc; + pub mod component; pub mod smbios_platform; diff --git a/components/patina_smbios/src/manager/core.rs b/components/patina_smbios/src/manager/core.rs index 50718b35e..32b892fd2 100644 --- a/components/patina_smbios/src/manager/core.rs +++ b/components/patina_smbios/src/manager/core.rs @@ -14,8 +14,9 @@ extern crate alloc; use alloc::{boxed::Box, collections::BTreeSet, string::String, vec::Vec}; use core::cell::RefCell; -use patina::standard::efi::{Handle, PhysicalAddress}; -use patina::{SIZE_64KB, uefi_size_to_pages}; +use patina::component::service::uefi_services::config_table::ConfigTable; +use patina::standard::efi::{Handle, PhysicalAddress, SMBIOS3_TABLE_GUID}; +use patina::{BinaryGuid, SIZE_64KB, uefi_size_to_pages}; use zerocopy::{IntoBytes, Ref}; use zerocopy_derive::*; @@ -57,6 +58,10 @@ pub struct Smbios30EntryPoint { pub table_address: u64, } +impl ConfigTable for Smbios30EntryPoint { + const TABLE_GUID: BinaryGuid = BinaryGuid(SMBIOS3_TABLE_GUID); +} + /// SMBIOS table manager /// /// Manages SMBIOS records, handles, and table generation. diff --git a/patina_dxe_core/Cargo.toml b/patina_dxe_core/Cargo.toml index 8be15d91c..da263be3a 100644 --- a/patina_dxe_core/Cargo.toml +++ b/patina_dxe_core/Cargo.toml @@ -53,5 +53,7 @@ std = ["patina/std"] compatibility_mode_allowed = [] v1_resource_descriptor_support = [] debugger_reload = [] +# Enables loading images by device using the (unstable) SDK device path API. +unstable-device-path = ["patina/unstable-device-path"] # All non-std features to test in CI -ci_features = ["compatibility_mode_allowed", "v1_resource_descriptor_support", "debugger_reload"] +ci_features = ["compatibility_mode_allowed", "v1_resource_descriptor_support", "debugger_reload", "unstable-device-path"] diff --git a/patina_dxe_core/src/component_dispatcher.rs b/patina_dxe_core/src/component_dispatcher.rs index d7e922b81..eea83e498 100644 --- a/patina_dxe_core/src/component_dispatcher.rs +++ b/patina_dxe_core/src/component_dispatcher.rs @@ -343,6 +343,48 @@ mod tests { assert_eq!(dispatcher.components.len(), 1); } + #[test] + fn test_component_with_unregistered_service_is_deferred_until_service_added() { + trait TestService { + fn value(&self) -> u32; + } + + #[derive(patina::component::service::IntoService)] + #[service(dyn TestService)] + struct TestServiceImpl; + + impl TestService for TestServiceImpl { + fn value(&self) -> u32 { + 42 + } + } + + struct TestComponent; + + #[component] + impl TestComponent { + fn entry_point( + self, + service: patina::component::service::Service, + ) -> patina::error::Result<()> { + assert_eq!(service.value(), 42); + Ok(()) + } + } + + let mut dispatcher = ComponentDispatcher::default(); + dispatcher.insert_component(0, TestComponent.into_component()); + + // The service the component depends on hasn't been added yet, so the component must be deferred for + // a later retry, not permanently failed. + assert!(!dispatcher.dispatch()); + + dispatcher.add_service(TestServiceImpl); + + // Now that the service is present, the previously-deferred component should dispatch successfully. + assert!(dispatcher.dispatch()); + } + #[test] fn test_parse_hob_list_into_storage() { use zerocopy::IntoBytes; diff --git a/patina_dxe_core/src/events.rs b/patina_dxe_core/src/events.rs index 251f17b82..9dd5d13de 100644 --- a/patina_dxe_core/src/events.rs +++ b/patina_dxe_core/src/events.rs @@ -27,6 +27,12 @@ pub static EVENT_DB: SpinLockedEventDb = SpinLockedEventDb::new(); static CURRENT_TPL: AtomicUsize = AtomicUsize::new(efi::TPL_APPLICATION); static SYSTEM_TIME: AtomicU64 = AtomicU64::new(0); +static TIMER_ARCH_READY: AtomicBool = AtomicBool::new(false); + +/// Returns true once the Timer Architectural Protocol has been located and its tick handler registered. +pub(crate) fn timer_arch_protocol_ready() -> bool { + TIMER_ARCH_READY.load(Ordering::SeqCst) +} /// # Safety /// @@ -361,6 +367,7 @@ extern "efiapi" fn timer_available_callback(event: efi::Event, _context: *mut c_ // SAFETY: timer_arch_ptr was successfully returned from locate_protocol. let timer_arch = unsafe { &*(timer_arch_ptr) }; (timer_arch.register_handler)(timer_arch_ptr, timer_tick); + TIMER_ARCH_READY.store(true, Ordering::SeqCst); if let Err(status_err) = EVENT_DB.close_event(event) { log::warn!("Could not close event for timer_available_callback due to error {status_err}"); } diff --git a/patina_dxe_core/src/lib.rs b/patina_dxe_core/src/lib.rs index 740db1378..3912fcf85 100644 --- a/patina_dxe_core/src/lib.rs +++ b/patina_dxe_core/src/lib.rs @@ -94,6 +94,7 @@ mod protocols; mod runtime; mod systemtables; mod tpl_mutex; +mod uefi_services; #[cfg(test)] pub use {component_dispatcher::MockComponentInfo, cpu::MockCpuInfo}; @@ -477,6 +478,12 @@ impl Core

{ component_dispatcher.add_service(CoreMemoryManager); component_dispatcher.add_service(dxe_dispatch_service::CoreDxeDispatch::new(self)); component_dispatcher.add_service(cpu::PerfTimer::with_frequency(perf_frequency)); + component_dispatcher.add_service(uefi_services::CoreEventServices); + component_dispatcher.add_service(uefi_services::CoreProtocolServices); + component_dispatcher.add_service(uefi_services::CoreConfigurationTableServices); + component_dispatcher.add_service(uefi_services::CoreDriverServices); + component_dispatcher.add_service(uefi_services::CoreImageServices::new::

()); + component_dispatcher.add_service(uefi_services::CoreTplServices); self.initialize_performance(perf_frequency, &mut component_dispatcher); relocated_hob_list @@ -514,6 +521,27 @@ impl Core

{ } } + /// Registers the `TimingServices` component service once the Metronome and Watchdog Timer Architectural + /// Protocols are both available. + fn try_register_timing_service(&self) { + static REGISTERED: Once<()> = Once::new(); + if !REGISTERED.is_completed() && misc_boot_services::timing_arch_protocols_ready() { + REGISTERED.call_once(|| { + self.component_dispatcher.lock().add_service(uefi_services::CoreTimingServices); + }); + } + } + + /// Registers the `TimerEventServices` component service once the Timer Architectural Protocol is available. + fn try_register_timer_event_service(&self) { + static REGISTERED: Once<()> = Once::new(); + if !REGISTERED.is_completed() && events::timer_arch_protocol_ready() { + REGISTERED.call_once(|| { + self.component_dispatcher.lock().add_service(uefi_services::CoreTimerEventServices); + }); + } + } + /// Performs a combined dispatch of Patina components and UEFI drivers. /// /// This function will continue to loop and perform dispatching until no components have been dispatched in a full @@ -523,6 +551,9 @@ impl Core

{ /// 2. A single iteration of dispatching UEFI drivers via the dispatcher module. fn core_dispatcher(&'static self) -> Result<()> { loop { + self.try_register_timing_service(); + self.try_register_timer_event_service(); + // Patina component dispatch let dispatched = self.component_dispatcher.lock().dispatch(); diff --git a/patina_dxe_core/src/misc_boot_services.rs b/patina_dxe_core/src/misc_boot_services.rs index cd8964dbb..fdbf932e8 100644 --- a/patina_dxe_core/src/misc_boot_services.rs +++ b/patina_dxe_core/src/misc_boot_services.rs @@ -10,7 +10,9 @@ use core::{ffi::c_void, slice::from_raw_parts, sync::atomic::Ordering}; use patina::arch as interrupts; use patina::standard::efi; use patina::{ - crc32, guid as base_guids, log_debug_assert, + crc32, + error::EfiError, + guid as base_guids, log_debug_assert, pi::{protocol, status_code}, uefi::event::EXIT_BOOT_SERVICES_FAILED_EVENT_GROUP_GUID, }; @@ -53,6 +55,11 @@ unsafe impl Sync for ArchProtocolPtr {} static METRONOME_ARCH_PTR: ArchProtocolPtr = ArchProtocolPtr::new(); static WATCHDOG_ARCH_PTR: ArchProtocolPtr = ArchProtocolPtr::new(); +/// Returns true once both the Metronome and Watchdog Timer Architectural Protocols have been located. +pub(crate) fn timing_arch_protocols_ready() -> bool { + METRONOME_ARCH_PTR.get().is_some() && WATCHDOG_ARCH_PTR.get().is_some() +} + // TODO [BEGIN]: LOCAL (TEMP) GUID DEFINITIONS (MOVE LATER) // These will likely get moved to different places. DXE Core GUID is the GUID of this DXE Core instance. @@ -86,28 +93,37 @@ unsafe extern "efiapi" fn calculate_crc32(data: *mut c_void, data_size: usize, c // Induces a fine-grained stall. Stalls execution on the processor for at least the requested number of microseconds. // Execution of the processor is not yielded for the duration of the stall. -extern "efiapi" fn stall(microseconds: usize) -> efi::Status { - if let Some(metronome_ptr) = METRONOME_ARCH_PTR.get() { - // SAFETY: metronome_ptr is guaranteed to be a valid pointer to the metronome protocol if it is Some. - let metronome = unsafe { metronome_ptr.as_mut().expect("Metronome pointer should not be null.") }; - let ticks_100ns: u128 = (microseconds as u128) * 10; - let mut ticks = ticks_100ns / u128::from(metronome.tick_period); - while ticks > u128::from(u32::MAX) { - let status = (metronome.wait_for_tick)(metronome_ptr, u32::MAX); - if status.is_error() { - log::warn!("metronome.wait_for_tick returned unexpected error {status}"); - } - ticks -= u128::from(u32::MAX); +// +// This is the pure-Rust implementation used by the C ABI `stall` wrapper and by the `TimingServices` component +// service. +pub(crate) fn core_stall(microseconds: usize) -> Result<(), EfiError> { + let Some(metronome_ptr) = METRONOME_ARCH_PTR.get() else { + return Err(EfiError::NotReady); //technically this should be NOT_AVAILABLE_YET. + }; + // SAFETY: metronome_ptr is guaranteed to be a valid pointer to the metronome protocol if it is Some. + let metronome = unsafe { metronome_ptr.as_mut().expect("Metronome pointer should not be null.") }; + let ticks_100ns: u128 = (microseconds as u128) * 10; + let mut ticks = ticks_100ns / u128::from(metronome.tick_period); + while ticks > u128::from(u32::MAX) { + let status = (metronome.wait_for_tick)(metronome_ptr, u32::MAX); + if status.is_error() { + log::warn!("metronome.wait_for_tick returned unexpected error {status}"); } - if ticks != 0 { - let status = (metronome.wait_for_tick)(metronome_ptr, ticks as u32); - if status.is_error() { - log::warn!("metronome.wait_for_tick returned unexpected error {status}"); - } + ticks -= u128::from(u32::MAX); + } + if ticks != 0 { + let status = (metronome.wait_for_tick)(metronome_ptr, ticks as u32); + if status.is_error() { + log::warn!("metronome.wait_for_tick returned unexpected error {status}"); } - efi::Status::SUCCESS - } else { - efi::Status::NOT_READY //technically this should be NOT_AVAILABLE_YET. + } + Ok(()) +} + +extern "efiapi" fn stall(microseconds: usize) -> efi::Status { + match core_stall(microseconds) { + Ok(()) => efi::Status::SUCCESS, + Err(err) => err.into(), } } @@ -120,24 +136,32 @@ extern "efiapi" fn stall(microseconds: usize) -> efi::Status { // // The watchdog timer is only used during boot services. On successful completion of // EFI_BOOT_SERVICES.ExitBootServices() the watchdog timer is disabled. +// Pure-Rust implementation of SetWatchdogTimer used both by the C ABI `set_watchdog_timer` wrapper +// and by the `TimingServices` component service. `timeout` is expressed in seconds. +pub(crate) fn core_set_watchdog_timer(timeout: usize, _watchdog_code: u64) -> Result<(), EfiError> { + const WATCHDOG_TIMER_CALIBRATE_PER_SECOND: u64 = 10000000; + let Some(watchdog_ptr) = WATCHDOG_ARCH_PTR.get() else { + return Err(EfiError::NotReady); + }; + // SAFETY: watchdog_ptr is guaranteed to be a valid pointer to the watchdog protocol if it is Some. + let watchdog = unsafe { watchdog_ptr.as_mut().expect("Watchdog pointer should not be null.") }; + let timeout = (timeout as u64).saturating_mul(WATCHDOG_TIMER_CALIBRATE_PER_SECOND); + let status = (watchdog.set_timer_period)(watchdog_ptr, timeout); + if status.is_error() { + return Err(EfiError::DeviceError); + } + Ok(()) +} + extern "efiapi" fn set_watchdog_timer( timeout: usize, - _watchdog_code: u64, + watchdog_code: u64, _data_size: usize, _data: *mut efi::Char16, ) -> efi::Status { - const WATCHDOG_TIMER_CALIBRATE_PER_SECOND: u64 = 10000000; - if let Some(watchdog_ptr) = WATCHDOG_ARCH_PTR.get() { - // SAFETY: watchdog_ptr is guaranteed to be a valid pointer to the watchdog protocol if it is Some. - let watchdog = unsafe { watchdog_ptr.as_mut().expect("Watchdog pointer should not be null.") }; - let timeout = (timeout as u64).saturating_mul(WATCHDOG_TIMER_CALIBRATE_PER_SECOND); - let status = (watchdog.set_timer_period)(watchdog_ptr, timeout); - if status.is_error() { - return efi::Status::DEVICE_ERROR; - } - efi::Status::SUCCESS - } else { - efi::Status::NOT_READY + match core_set_watchdog_timer(timeout, watchdog_code) { + Ok(()) => efi::Status::SUCCESS, + Err(err) => err.into(), } } // Requires excessive Mocking for the OK case. diff --git a/patina_dxe_core/src/pi_dispatcher/image.rs b/patina_dxe_core/src/pi_dispatcher/image.rs index 32702332d..0f7cbce2f 100644 --- a/patina_dxe_core/src/pi_dispatcher/image.rs +++ b/patina_dxe_core/src/pi_dispatcher/image.rs @@ -19,6 +19,7 @@ use patina::standard::efi::{self, protocols::device_path::Protocol}; use patina::{ Char16Str, component::service::memory::{AllocationOptions, MemoryManager, PageFree}, + component::service::uefi_services::image::ImageError, error::EfiError, guid as base_guids, log_debug_assert, pi::{ @@ -686,6 +687,47 @@ unsafe impl Sync for ImageData {} unsafe impl Send for ImageData {} impl super::PiDispatcher

{ + /// Loads an image from an in-memory buffer for the [`ImageServices`] component service. + /// + /// This resolves the platform-specific dispatcher instance and erases the platform generic + /// `P`, so it can be stored as a plain function pointer by `CoreImageServices`. + /// + /// This is a simplified version of loading an image, which does not require a device path and + /// always indicates that the image load is not originating from the boot manager. In order + /// to change the `boot_policy` or to provide a device path from which the image is loaded, + /// use [`service_load_image_from_device_path`] instead. + /// + /// [`ImageServices`]: patina::component::service::uefi_services::image::ImageServices + pub(crate) fn service_load_image(parent: efi::Handle, source: &[u8]) -> Result { + Self::instance().load_image(false, parent, None, Some(source)).map_err(image_status_to_error) + } + + /// Starts a loaded image for the [`ImageServices`] component service. + /// + /// [`ImageServices`]: patina::component::service::uefi_services::image::ImageServices + pub(crate) fn service_start_image(image: efi::Handle) -> Result<(), ImageError> { + Self::instance().start_image(image).map_err(efi_status_to_image_error) + } + + /// Unloads a loaded image for the [`ImageServices`] component service. + /// + /// [`ImageServices`]: patina::component::service::uefi_services::image::ImageServices + pub(crate) fn service_unload_image(image: efi::Handle) -> Result<(), ImageError> { + Self::instance().unload_image(image, false).map_err(efi_status_to_image_error) + } + + /// Loads an image located by a device path for the [`ImageServices`] component service. + /// + /// [`ImageServices`]: patina::component::service::uefi_services::image::ImageServices + #[cfg(feature = "unstable-device-path")] + pub(crate) fn service_load_image_from_device_path( + parent: efi::Handle, + file_path: NonNull, + boot_policy: bool, + ) -> Result { + Self::instance().load_image(boot_policy, parent, Some(file_path), None).map_err(image_status_to_error) + } + /// Loads the image specified by the device path or slice. /// * `parent_image_handle` - the handle of the image that is loading this one. /// * `file_path` - optional device path describing where to load the image from. @@ -1565,6 +1607,23 @@ impl From for ImageStatus { } } +/// Maps a load-image [`ImageStatus`] to the component-facing [`ImageError`]. +fn image_status_to_error(status: ImageStatus) -> ImageError { + match status { + ImageStatus::LoadError(err) => ImageError::from(err), + ImageStatus::SecurityViolation(_) => ImageError::SecurityViolation, + ImageStatus::AccessDenied => ImageError::AccessDenied, + } +} + +/// Maps a start/unload-image [`efi::Status`] to the component-facing [`ImageError`]. +fn efi_status_to_image_error(status: efi::Status) -> ImageError { + match EfiError::status_to_result(status) { + Ok(()) => ImageError::Internal, + Err(err) => ImageError::from(err), + } +} + /// A buffer of bytes that is either owned or borrowed. enum Buffer { /// Bytes allocated with the page allocator and owned by this struct. diff --git a/patina_dxe_core/src/uefi_services.rs b/patina_dxe_core/src/uefi_services.rs new file mode 100644 index 000000000..2c4234ad0 --- /dev/null +++ b/patina_dxe_core/src/uefi_services.rs @@ -0,0 +1,32 @@ +//! DXE Core implementation of the Patina UEFI Services. +//! +//! Each submodule implements one of the [`patina::component::service::uefi_services`] traits +//! directly against the core's internal Rust APIs (the `core_*` functions and the protocol/event +//! databases) instead of calling out to the C `EFI_BOOT_SERVICES` function-pointer table. The +//! implementations are registered as services by the core during initialization so that components +//! can consume them by declaring a `Service` dependency. +//! +//! ## License +//! +//! Copyright (c) Microsoft Corporation. +//! +//! SPDX-License-Identifier: Apache-2.0 +//! + +mod config_table; +mod driver; +mod event; +mod image; +mod protocol; +mod timer_event; +mod timing; +mod tpl; + +pub(crate) use config_table::CoreConfigurationTableServices; +pub(crate) use driver::CoreDriverServices; +pub(crate) use event::CoreEventServices; +pub(crate) use image::CoreImageServices; +pub(crate) use protocol::CoreProtocolServices; +pub(crate) use timer_event::CoreTimerEventServices; +pub(crate) use timing::CoreTimingServices; +pub(crate) use tpl::CoreTplServices; diff --git a/patina_dxe_core/src/uefi_services/config_table.rs b/patina_dxe_core/src/uefi_services/config_table.rs new file mode 100644 index 000000000..4474cf772 --- /dev/null +++ b/patina_dxe_core/src/uefi_services/config_table.rs @@ -0,0 +1,326 @@ +//! DXE Core implementation of [`ConfigurationTableServices`]. +//! +//! ## License +//! +//! Copyright (c) Microsoft Corporation. +//! +//! SPDX-License-Identifier: Apache-2.0 +//! + +use alloc::collections::btree_map::BTreeMap; +use core::any::TypeId; + +use patina::BinaryGuid; +use patina::component::service::{ + IntoService, + uefi_services::config_table::{ConfigTableError, ConfigTablePtr, ConfigurationTableServices}, +}; +use patina::standard::efi; + +use crate::config_tables::{core_install_configuration_table, get_configuration_table}; +use crate::systemtables::SYSTEM_TABLE; +use crate::tpl_mutex::TplMutex; + +/// Records the Rust type installed under each GUID using [`ConfigurationTableServices::install_typed_table`], so +/// [`ConfigurationTableServices::get_typed_table`] can verify a lookup's type before a caller casts the pointer. +/// +/// This is separate from the real configuration table entries in [`SYSTEM_TABLE`] and is only used internally. +static CONFIG_TABLE_TYPES: TplMutex> = + TplMutex::new(efi::TPL_NOTIFY, BTreeMap::new(), "ConfigTableTypeLock"); + +/// Core implementation of [`ConfigurationTableServices`], operating on the global system table via +/// the core's internal Rust APIs. +#[derive(IntoService)] +#[service(dyn ConfigurationTableServices)] +pub(crate) struct CoreConfigurationTableServices; + +impl ConfigurationTableServices for CoreConfigurationTableServices { + unsafe fn install_table(&self, guid: BinaryGuid, table: ConfigTablePtr) -> Result<(), ConfigTableError> { + let mut st_guard = SYSTEM_TABLE.lock(); + let st = st_guard.as_mut().ok_or(ConfigTableError::NotFound)?; + core_install_configuration_table(guid.into_inner(), table.as_raw(), st) + .map(|_| ()) + .map_err(ConfigTableError::from) + } + + fn remove_table(&self, guid: BinaryGuid) -> Result<(), ConfigTableError> { + let mut st_guard = SYSTEM_TABLE.lock(); + let st = st_guard.as_mut().ok_or(ConfigTableError::NotFound)?; + // Installing a null table removes the entry for the GUID. + core_install_configuration_table(guid.into_inner(), core::ptr::null_mut(), st) + .map(|_| ()) + .map_err(ConfigTableError::from) + } + + fn get_table(&self, guid: BinaryGuid) -> Option { + get_configuration_table(&guid.into_inner()).and_then(|table| ConfigTablePtr::from_raw(table.as_ptr())) + } + + unsafe fn install_typed_table( + &self, + guid: BinaryGuid, + type_id: TypeId, + table: ConfigTablePtr, + ) -> Result<(), ConfigTableError> { + let mut types = CONFIG_TABLE_TYPES.lock(); + // A stale type entry (in the `BTreeMap`) can outlive its table if something removed it using + // `remove_table` (untyped) directly, so only reject the install if the table is still present + // in the actual system table. + if types.contains_key(&guid) && self.get_table(guid).is_some() { + return Err(ConfigTableError::AlreadyExists); + } + // SAFETY: forwarding the precondition on `table` upheld by this function's own caller. + unsafe { self.install_table(guid, table) }?; + types.insert(guid, type_id); + Ok(()) + } + + fn get_typed_table(&self, guid: BinaryGuid, type_id: TypeId) -> Option { + if CONFIG_TABLE_TYPES.lock().get(&guid) != Some(&type_id) { + return None; + } + self.get_table(guid) + } + + fn remove_typed_table(&self, guid: BinaryGuid) -> Result<(), ConfigTableError> { + self.remove_table(guid)?; + CONFIG_TABLE_TYPES.lock().remove(&guid); + Ok(()) + } + + unsafe fn replace_typed_table( + &self, + guid: BinaryGuid, + type_id: TypeId, + table: ConfigTablePtr, + ) -> Result<(), ConfigTableError> { + let mut types = CONFIG_TABLE_TYPES.lock(); + // SAFETY: forwarding the precondition on `table` upheld by this function's own caller. + unsafe { self.install_table(guid, table) }?; + types.insert(guid, type_id); + Ok(()) + } +} + +#[cfg(test)] +#[cfg_attr(coverage, coverage(off))] +mod tests { + use core::ffi::c_void; + + use crate::{systemtables::init_system_table, test_support}; + + use super::*; + + fn with_locked_state(f: F) { + test_support::with_global_lock(|| { + // SAFETY: functions modify global state; called within the global test lock. + unsafe { + test_support::init_test_gcd(None); + test_support::reset_allocators(); + init_system_table(); + } + f(); + }) + .unwrap(); + } + + #[test] + fn install_table_then_get_table_returns_same_pointer() { + with_locked_state(|| { + let svc = CoreConfigurationTableServices; + let guid: BinaryGuid = BinaryGuid::from_string("1a2b3c4d-5e6f-4a1b-9c2d-3e4f5a6b7c8d"); + let table = ConfigTablePtr::from_raw(0x1000usize as *mut c_void).unwrap(); + + // SAFETY: `table` is a dummy address that is never dereferenced. This test only checks + // that the opaque pointer value carries through installation and lookup. + assert_eq!(unsafe { svc.install_table(guid, table) }, Ok(())); + assert_eq!(svc.get_table(guid), Some(table)); + }); + } + + #[test] + fn remove_table_removes_installed_table() { + with_locked_state(|| { + let svc = CoreConfigurationTableServices; + let guid: BinaryGuid = BinaryGuid::from_string("2b3c4d5e-6f7a-4b2c-8d3e-4f5a6b7c8d9e"); + let table = ConfigTablePtr::from_raw(0x2000usize as *mut c_void).unwrap(); + + // SAFETY: `table` is a dummy address that is not dereferenced in this test. + unsafe { svc.install_table(guid, table) }.unwrap(); + assert_eq!(svc.get_table(guid), Some(table)); + + assert_eq!(svc.remove_table(guid), Ok(())); + assert_eq!(svc.get_table(guid), None); + }); + } + + #[test] + fn remove_table_for_unknown_guid_returns_not_found() { + with_locked_state(|| { + let svc = CoreConfigurationTableServices; + let guid: BinaryGuid = BinaryGuid::from_string("3c4d5e6f-7a8b-4c3d-9e4f-5a6b7c8d9e0f"); + + assert_eq!(svc.remove_table(guid), Err(ConfigTableError::NotFound)); + }); + } + + #[test] + fn get_table_for_unknown_guid_returns_none() { + with_locked_state(|| { + let svc = CoreConfigurationTableServices; + let guid: BinaryGuid = BinaryGuid::from_string("4d5e6f7a-8b9c-4d4e-8f5a-6b7c8d9e0f1a"); + + assert_eq!(svc.get_table(guid), None); + }); + } + + #[test] + fn install_table_and_remove_table_return_not_found_when_system_table_uninitialized() { + with_locked_state(|| { + // Simulate an uninitialized system table. Restore it afterward (even on panic) so + // later tests relying on `with_locked_state`'s invariant are unaffected. + *SYSTEM_TABLE.lock() = None; + let _guard = test_support::StateGuard::new(init_system_table); + + let svc = CoreConfigurationTableServices; + let guid: BinaryGuid = BinaryGuid::from_string("5e6f7a8b-9c0d-4e5f-9a6b-7c8d9e0f1a2b"); + let table = ConfigTablePtr::from_raw(0x3000usize as *mut c_void).unwrap(); + + // SAFETY: `table` is a dummy address that is not dereferenced in this test. + assert_eq!(unsafe { svc.install_table(guid, table) }, Err(ConfigTableError::NotFound)); + assert_eq!(svc.remove_table(guid), Err(ConfigTableError::NotFound)); + }); + } + + #[test] + fn install_typed_table_then_get_typed_table_returns_same_pointer() { + with_locked_state(|| { + let svc = CoreConfigurationTableServices; + let guid: BinaryGuid = BinaryGuid::from_string("6f7a8b9c-0d1e-4f5a-8b6c-7d8e9f0a1b2c"); + let type_id = TypeId::of::(); + let table = ConfigTablePtr::from_raw(0x4000usize as *mut c_void).unwrap(); + + // SAFETY: `table` is a dummy address that is not dereferenced in this test. + assert_eq!(unsafe { svc.install_typed_table(guid, type_id, table) }, Ok(())); + assert_eq!(svc.get_typed_table(guid, type_id), Some(table)); + }); + } + + #[test] + fn install_typed_table_rejects_duplicate_guid() { + with_locked_state(|| { + let svc = CoreConfigurationTableServices; + let guid: BinaryGuid = BinaryGuid::from_string("7a8b9c0d-1e2f-4a5b-9c6d-7e8f9a0b1c2d"); + let table = ConfigTablePtr::from_raw(0x5000usize as *mut c_void).unwrap(); + + // SAFETY: `table` is a dummy address that is not dereferenced in this test. + assert_eq!(unsafe { svc.install_typed_table(guid, TypeId::of::(), table) }, Ok(())); + // A second install under the same GUID must fail, even with a different recorded type. + // SAFETY: `table` is a dummy address that is not dereferenced in this test. + let second_install = unsafe { svc.install_typed_table(guid, TypeId::of::(), table) }; + assert_eq!(second_install, Err(ConfigTableError::AlreadyExists)); + }); + } + + #[test] + fn get_typed_table_returns_none_for_type_mismatch() { + with_locked_state(|| { + let svc = CoreConfigurationTableServices; + let guid: BinaryGuid = BinaryGuid::from_string("8b9c0d1e-2f3a-4b6c-8d7e-8f9a0b1c2d3e"); + let table = ConfigTablePtr::from_raw(0x6000usize as *mut c_void).unwrap(); + + // SAFETY: `table` is a dummy address that is not dereferenced in this test. + unsafe { svc.install_typed_table(guid, TypeId::of::(), table) }.unwrap(); + + assert_eq!(svc.get_typed_table(guid, TypeId::of::()), None); + }); + } + + #[test] + fn get_typed_table_returns_none_for_untyped_install() { + with_locked_state(|| { + let svc = CoreConfigurationTableServices; + let guid: BinaryGuid = BinaryGuid::from_string("9c0d1e2f-3a4b-4c6d-8e7f-8a9b0c1d2e3f"); + let table = ConfigTablePtr::from_raw(0x7000usize as *mut c_void).unwrap(); + + // Installed using the untyped, raw API - no type is on record for `guid`. + // SAFETY: `table` is a dummy address that is not dereferenced in this test. + unsafe { svc.install_table(guid, table) }.unwrap(); + + assert_eq!(svc.get_typed_table(guid, TypeId::of::()), None); + }); + } + + #[test] + fn remove_typed_table_removes_installed_table_and_type() { + with_locked_state(|| { + let svc = CoreConfigurationTableServices; + let guid: BinaryGuid = BinaryGuid::from_string("0d1e2f3a-4b5c-4d6e-8f7a-8b9c0d1e2f3a"); + let type_id = TypeId::of::(); + let table = ConfigTablePtr::from_raw(0x8000usize as *mut c_void).unwrap(); + + // SAFETY: `table` is a dummy address that is not dereferenced in this test. + unsafe { svc.install_typed_table(guid, type_id, table) }.unwrap(); + assert_eq!(svc.remove_typed_table(guid), Ok(())); + + assert_eq!(svc.get_table(guid), None); + assert_eq!(svc.get_typed_table(guid, type_id), None); + }); + } + + #[test] + fn install_typed_table_self_gracefully_handles_raw_remove_table() { + with_locked_state(|| { + let svc = CoreConfigurationTableServices; + let guid: BinaryGuid = BinaryGuid::from_string("1e2f3a4b-5c6d-4e7f-8a8b-9c0d1e2f3a4b"); + let type_id = TypeId::of::(); + let table = ConfigTablePtr::from_raw(0x9000usize as *mut c_void).unwrap(); + + // SAFETY: `table` is a dummy address that is not dereferenced in this test. + unsafe { svc.install_typed_table(guid, type_id, table) }.unwrap(); + // Remove the real table using the untyped API, bypassing type-registry cleanup. A stale + // type entry for `guid` is left behind. + svc.remove_table(guid).unwrap(); + + // Re-installing under the same GUID must succeed since the table itself is gone, even + // though the (now stale) type entry was never cleared. + // SAFETY: `table` is a dummy address that is not dereferenced in this test. + assert_eq!(unsafe { svc.install_typed_table(guid, type_id, table) }, Ok(())); + assert_eq!(svc.get_typed_table(guid, type_id), Some(table)); + }); + } + + #[test] + fn replace_typed_table_installs_when_nothing_exists() { + with_locked_state(|| { + let svc = CoreConfigurationTableServices; + let guid: BinaryGuid = BinaryGuid::from_string("2f3a4b5c-6d7e-4f8a-9b8c-0d1e2f3a4b5c"); + let type_id = TypeId::of::(); + let table = ConfigTablePtr::from_raw(0xa000usize as *mut c_void).unwrap(); + + // SAFETY: `table` is a dummy address that is not dereferenced in this test. + assert_eq!(unsafe { svc.replace_typed_table(guid, type_id, table) }, Ok(())); + assert_eq!(svc.get_typed_table(guid, type_id), Some(table)); + }); + } + + #[test] + fn replace_typed_table_replaces_without_error_when_already_installed() { + with_locked_state(|| { + let svc = CoreConfigurationTableServices; + let guid: BinaryGuid = BinaryGuid::from_string("3a4b5c6d-7e8f-4a9b-8c9d-1e2f3a4b5c6d"); + let type_id = TypeId::of::(); + let first_table = ConfigTablePtr::from_raw(0xb000usize as *mut c_void).unwrap(); + let second_table = ConfigTablePtr::from_raw(0xc000usize as *mut c_void).unwrap(); + + // SAFETY: `first_table`/`second_table` are dummy addresses that are not dereferenced in + // this test. + unsafe { svc.replace_typed_table(guid, type_id, first_table) }.unwrap(); + // Republishing under the same GUID (e.g. after mutating the table's contents) must + // succeed rather than fail with `AlreadyExists`, and reflect the newest pointer. + // SAFETY: `second_table` is a dummy address that is not dereferenced in this test. + assert_eq!(unsafe { svc.replace_typed_table(guid, type_id, second_table) }, Ok(())); + assert_eq!(svc.get_typed_table(guid, type_id), Some(second_table)); + }); + } +} diff --git a/patina_dxe_core/src/uefi_services/driver.rs b/patina_dxe_core/src/uefi_services/driver.rs new file mode 100644 index 000000000..5269c9ff9 --- /dev/null +++ b/patina_dxe_core/src/uefi_services/driver.rs @@ -0,0 +1,125 @@ +//! DXE Core implementation of [`DriverServices`]. +//! +//! ## License +//! +//! Copyright (c) Microsoft Corporation. +//! +//! SPDX-License-Identifier: Apache-2.0 +//! + +use alloc::vec::Vec; + +use patina::component::service::{ + IntoService, + uefi_services::driver::{DriverError, DriverServices, Handle}, +}; + +use crate::driver_services::{core_connect_controller, core_disconnect_controller}; + +/// Core implementation of [`DriverServices`], delegating to the core driver model by calling the internal +/// `core_*` Rust APIs. +#[derive(IntoService)] +#[service(dyn DriverServices)] +pub(crate) struct CoreDriverServices; + +impl DriverServices for CoreDriverServices { + fn connect_controller(&self, controller: Handle, recursive: bool) -> Result<(), DriverError> { + // SAFETY: No remaining device path is passed, so there is no device-path pointer whose + // validity the caller must uphold. Handles are validated inside the core routine. + unsafe { core_connect_controller(controller.as_raw(), Vec::new(), None, recursive) }.map_err(DriverError::from) + } + + fn disconnect_controller( + &self, + controller: Handle, + driver: Option, + child: Option, + ) -> Result<(), DriverError> { + // SAFETY: All handles are validated inside `core_disconnect_controller`. + unsafe { + core_disconnect_controller(controller.as_raw(), driver.map(|h| h.as_raw()), child.map(|h| h.as_raw())) + } + .map_err(DriverError::from) + } +} + +#[cfg(test)] +#[cfg_attr(coverage, coverage(off))] +mod tests { + use super::*; + use crate::{protocols::PROTOCOL_DB, test_support}; + use patina::standard::efi; + + fn with_locked_state(f: F) { + test_support::with_global_lock(|| { + test_support::init_test_logger(); + // SAFETY: Called within the global test lock. + unsafe { + test_support::init_test_protocol_db(); + } + f(); + }) + .unwrap(); + } + + #[test] + fn test_connect_controller_no_driver_binding_returns_not_found() { + with_locked_state(|| { + let (raw_handle, _) = PROTOCOL_DB + .install_protocol_interface( + None, + efi::protocols::device_path::PROTOCOL_GUID, + 0x1111usize as *mut core::ffi::c_void, + ) + .unwrap(); + let handle = Handle::from_raw(raw_handle).unwrap(); + + let result = CoreDriverServices.connect_controller(handle, false); + + assert_eq!(result, Err(DriverError::NotFound)); + }); + } + + #[test] + fn test_disconnect_controller_with_no_managing_driver_is_a_no_op() { + with_locked_state(|| { + let (raw_handle, _) = PROTOCOL_DB + .install_protocol_interface( + None, + efi::protocols::device_path::PROTOCOL_GUID, + 0x2222usize as *mut core::ffi::c_void, + ) + .unwrap(); + let handle = Handle::from_raw(raw_handle).unwrap(); + + // core_disconnect_controller treats "no drivers currently managing the controller" as + // success, so this shows that the driver/child Option -> Option translation + // compiles and reaches that path rather than an error. + let result = CoreDriverServices.disconnect_controller(handle, None, None); + + assert_eq!(result, Ok(())); + }); + } + + #[test] + fn test_connect_controller_invalid_handle_returns_invalid_parameter() { + with_locked_state(|| { + let handle = Handle::from_raw(0x9999 as efi::Handle).unwrap(); + + let result = CoreDriverServices.connect_controller(handle, false); + + assert_eq!(result, Err(DriverError::InvalidParameter)); + }); + } + + #[test] + fn test_disconnect_controller_invalid_handle_returns_invalid_parameter() { + with_locked_state(|| { + let handle = Handle::from_raw(0x9999 as efi::Handle).unwrap(); + + let result = CoreDriverServices.disconnect_controller(handle, None, None); + + assert_eq!(result, Err(DriverError::InvalidParameter)); + }); + } +} diff --git a/patina_dxe_core/src/uefi_services/event.rs b/patina_dxe_core/src/uefi_services/event.rs new file mode 100644 index 000000000..de10019c4 --- /dev/null +++ b/patina_dxe_core/src/uefi_services/event.rs @@ -0,0 +1,256 @@ +//! DXE Core implementation of [`EventServices`]. +//! +//! Notification callbacks supplied by components as Rust closures are boxed and stored with the +//! event's notification context. A single C-ABI "trampoline" recovers the closure and invokes it +//! when the event fires. The closure is reclaimed and dropped when the event is closed. +//! +//! ## License +//! +//! Copyright (c) Microsoft Corporation. +//! +//! SPDX-License-Identifier: Apache-2.0 +//! + +use alloc::boxed::Box; +use core::ffi::c_void; + +use patina::BinaryGuid; +use patina::component::service::{ + IntoService, + uefi_services::event::{Event, EventError, EventNotifyCallback, EventServices, Tpl}, +}; +use patina::error::EfiError; +use patina::standard::efi; + +use crate::events::{EVENT_DB, check_event as core_check_event}; + +/// Owns a component-supplied notification closure for the lifetime of an event. +struct ClosureHolder { + callback: EventNotifyCallback, +} + +/// C-ABI trampoline registered with every event created through [`create_event_internal`]. +/// +/// It recovers the [`ClosureHolder`] from the notification context and invokes the closure. +extern "efiapi" fn notify_trampoline(_event: efi::Event, context: *mut c_void) { + if context.is_null() { + return; + } + // SAFETY: `context` was produced by `Box::into_raw` of a `ClosureHolder` in + // `create_event_internal` and remains valid until `close_event` reclaims and drops it. UEFI + // dispatches notifications serially at the event's TPL, so there is no concurrent access. + let holder = unsafe { &mut *(context as *mut ClosureHolder) }; + (holder.callback)(); +} + +/// Creates an event backed by a boxed notification closure. +/// +/// Shared by [`CoreEventServices`] and [`CoreTimerEventServices`](super::timer_event::CoreTimerEventServices), +/// since both create events through the same closure mechanism. +pub(crate) fn create_event_internal( + event_type: u32, + notify_tpl: Tpl, + callback: EventNotifyCallback, + event_group: Option, +) -> Result { + let holder = Box::new(ClosureHolder { callback }); + let context = Box::into_raw(holder) as *mut c_void; + + match EVENT_DB.create_event(event_type, tpl_to_efi(notify_tpl), Some(notify_trampoline), Some(context), event_group) + { + Ok(efi_event) => Event::from_raw(efi_event).ok_or_else(|| { + // The event database returned a null handle. Reclaim the closure to avoid a leak. + // SAFETY: `context` came from `Box::into_raw` above and has not been freed. + drop(unsafe { Box::from_raw(context as *mut ClosureHolder) }); + EventError::Internal + }), + Err(err) => { + // SAFETY: `context` came from `Box::into_raw` above and has not been freed. + drop(unsafe { Box::from_raw(context as *mut ClosureHolder) }); + Err(EventError::from(err)) + } + } +} + +/// Core implementation of [`EventServices`], delegating to the core event database. +#[derive(IntoService)] +#[service(dyn EventServices)] +pub(crate) struct CoreEventServices; + +impl EventServices for CoreEventServices { + fn create_event(&self, notify_tpl: Tpl, callback: EventNotifyCallback) -> Result { + create_event_internal(efi::EVT_NOTIFY_SIGNAL, notify_tpl, callback, None) + } + + fn create_event_for_group( + &self, + group: BinaryGuid, + notify_tpl: Tpl, + callback: EventNotifyCallback, + ) -> Result { + create_event_internal(efi::EVT_NOTIFY_SIGNAL, notify_tpl, callback, Some(group.into_inner())) + } + + fn signal_event(&self, event: Event) -> Result<(), EventError> { + EVENT_DB.signal_event(event.as_raw()).map_err(EventError::from) + } + + fn check_event(&self, event: Event) -> Result { + match core_check_event(event.as_raw()) { + efi::Status::SUCCESS => Ok(true), + efi::Status::NOT_READY => Ok(false), + status => Err(EventError::from(EfiError::status_to_result(status).unwrap_err())), + } + } + + fn close_event(&self, event: Event) -> Result<(), EventError> { + let efi_event = event.as_raw(); + + // Retrieve the closure context before closing so it can be reclaimed afterward. + let context = EVENT_DB.get_notification_data(efi_event).ok().and_then(|data| data.notify_context); + + EVENT_DB.close_event(efi_event).map_err(EventError::from)?; + + if let Some(context) = context + && !context.is_null() + { + // SAFETY: `context` is the `ClosureHolder` pointer created in `create_event_internal`. + // The event has just been closed, so no further notifications can reference it. + drop(unsafe { Box::from_raw(context as *mut ClosureHolder) }); + } + + Ok(()) + } +} + +pub(crate) fn tpl_to_efi(tpl: Tpl) -> efi::Tpl { + match tpl { + Tpl::Application => efi::TPL_APPLICATION, + Tpl::Callback => efi::TPL_CALLBACK, + Tpl::Notify => efi::TPL_NOTIFY, + Tpl::HighLevel => efi::TPL_HIGH_LEVEL, + } +} + +#[cfg(test)] +#[cfg_attr(coverage, coverage(off))] +mod tests { + use super::*; + use std::sync::{ + Arc, + atomic::{AtomicUsize, Ordering}, + }; + + extern "efiapi" fn noop_wait_notify(_event: efi::Event, _context: *mut c_void) {} + + #[test] + fn test_tpl_to_efi_maps_all_variants() { + assert_eq!(tpl_to_efi(Tpl::Application), efi::TPL_APPLICATION); + assert_eq!(tpl_to_efi(Tpl::Callback), efi::TPL_CALLBACK); + assert_eq!(tpl_to_efi(Tpl::Notify), efi::TPL_NOTIFY); + assert_eq!(tpl_to_efi(Tpl::HighLevel), efi::TPL_HIGH_LEVEL); + } + + #[test] + fn test_core_event_services_create_event_rejects_invalid_notify_tpl() { + crate::test_support::with_global_lock(|| { + let service = CoreEventServices; + + // TPL_APPLICATION is one level below the minimum notify TPL that `Event::new` accepts. + let result = service.create_event(Tpl::Application, Box::new(|| {})); + + assert_eq!(result, Err(EventError::InvalidParameter)); + }) + .unwrap(); + } + + #[test] + fn test_core_event_services_signal_event_invokes_closure_using_trampoline() { + crate::test_support::with_global_lock(|| { + let service = CoreEventServices; + let counter = Arc::new(AtomicUsize::new(0)); + let callback_counter = counter.clone(); + let callback: EventNotifyCallback = Box::new(move || { + callback_counter.fetch_add(1, Ordering::SeqCst); + }); + + let event = service.create_event(Tpl::Callback, callback).unwrap(); + service.signal_event(event).unwrap(); + + assert_eq!(counter.load(Ordering::SeqCst), 1); + + service.close_event(event).unwrap(); + }) + .unwrap(); + } + + #[test] + fn test_core_event_services_create_event_for_group_signal_group_invokes_closure() { + crate::test_support::with_global_lock(|| { + let service = CoreEventServices; + let counter = Arc::new(AtomicUsize::new(0)); + let callback_counter = counter.clone(); + let callback: EventNotifyCallback = Box::new(move || { + callback_counter.fetch_add(1, Ordering::SeqCst); + }); + + let event = service.create_event_for_group(BinaryGuid::ZERO, Tpl::Callback, callback).unwrap(); + EVENT_DB.signal_group(BinaryGuid::ZERO.into_inner()); + + assert_eq!(counter.load(Ordering::SeqCst), 1); + + service.close_event(event).unwrap(); + }) + .unwrap(); + } + + #[test] + fn test_core_event_services_check_event_reflects_signaled_state_for_wait_event() { + crate::test_support::with_global_lock(|| { + let service = CoreEventServices; + + // The service can only create NOTIFY_SIGNAL events, so create a NOTIFY_WAIT event + // directly through the event database to exercise check_event's other branches. + let raw_event = EVENT_DB + .create_event(efi::EVT_NOTIFY_WAIT, efi::TPL_NOTIFY, Some(noop_wait_notify), None, None) + .unwrap(); + let event = Event::from_raw(raw_event).unwrap(); + + assert_eq!(service.check_event(event), Ok(false)); + + service.signal_event(event).unwrap(); + + assert_eq!(service.check_event(event), Ok(true)); + + service.close_event(event).unwrap(); + }) + .unwrap(); + } + + #[test] + fn test_core_event_services_check_event_rejects_notify_signal_event() { + crate::test_support::with_global_lock(|| { + let service = CoreEventServices; + let event = service.create_event(Tpl::Callback, Box::new(|| {})).unwrap(); + + // check_event's UEFI semantics never accept a NOTIFY_SIGNAL event, which is the only + // kind this service creates. + assert_eq!(service.check_event(event), Err(EventError::InvalidParameter)); + + service.close_event(event).unwrap(); + }) + .unwrap(); + } + + #[test] + fn test_core_event_services_close_event_then_double_close_fails() { + crate::test_support::with_global_lock(|| { + let service = CoreEventServices; + let event = service.create_event(Tpl::Callback, Box::new(|| {})).unwrap(); + + assert_eq!(service.close_event(event), Ok(())); + assert_eq!(service.close_event(event), Err(EventError::InvalidParameter)); + }) + .unwrap(); + } +} diff --git a/patina_dxe_core/src/uefi_services/image.rs b/patina_dxe_core/src/uefi_services/image.rs new file mode 100644 index 000000000..5696935e8 --- /dev/null +++ b/patina_dxe_core/src/uefi_services/image.rs @@ -0,0 +1,84 @@ +//! DXE Core implementation of [`ImageServices`]. +//! +//! The UEFI image services live on the platform-generic `PiDispatcher

`. To register a single +//! non-generic service, [`CoreImageServices::new`] captures the dispatcher's platform-erased +//! operations as function pointers when the platform type `P` is known. +//! +//! ## License +//! +//! Copyright (c) Microsoft Corporation. +//! +//! SPDX-License-Identifier: Apache-2.0 +//! + +use patina::component::service::{ + IntoService, + uefi_services::image::{Handle, ImageError, ImageServices}, +}; +use patina::standard::efi; +#[cfg(feature = "unstable-device-path")] +use patina::uefi::device_path::paths::DevicePath; + +#[cfg(feature = "unstable-device-path")] +use core::ptr::NonNull; + +use crate::PlatformInfo; +use crate::pi_dispatcher::PiDispatcher; + +/// Core implementation of [`ImageServices`]. +/// +/// Holds the dispatcher's image operations as platform-erased function pointers so the service can +/// be registered without carrying the platform generic `P`. +#[derive(IntoService)] +#[service(dyn ImageServices)] +pub(crate) struct CoreImageServices { + load: fn(efi::Handle, &[u8]) -> Result, + start: fn(efi::Handle) -> Result<(), ImageError>, + unload: fn(efi::Handle) -> Result<(), ImageError>, + #[cfg(feature = "unstable-device-path")] + load_from_device_path: + fn(efi::Handle, NonNull, bool) -> Result, +} + +impl CoreImageServices { + /// Creates the service, binding the platform-specific dispatcher operations for platform `P`. + pub(crate) fn new() -> Self { + Self { + load: PiDispatcher::

::service_load_image, + start: PiDispatcher::

::service_start_image, + unload: PiDispatcher::

::service_unload_image, + #[cfg(feature = "unstable-device-path")] + load_from_device_path: PiDispatcher::

::service_load_image_from_device_path, + } + } +} + +impl ImageServices for CoreImageServices { + fn load_image(&self, parent: Handle, source: &[u8]) -> Result { + let handle = (self.load)(parent.as_raw(), source)?; + Handle::from_raw(handle).ok_or(ImageError::Internal) + } + + fn start_image(&self, image: Handle) -> Result<(), ImageError> { + (self.start)(image.as_raw()) + } + + fn unload_image(&self, image: Handle) -> Result<(), ImageError> { + (self.unload)(image.as_raw()) + } + + #[cfg(feature = "unstable-device-path")] + fn load_image_from_device_path( + &self, + parent: Handle, + device_path: &DevicePath, + boot_policy: bool, + ) -> Result { + // SAFETY-adjacent: `DevicePath` is a validated, well-formed device path (repr(transparent) + // over its byte buffer), so the first byte is the first device path node header. + let ptr = NonNull::new(device_path.as_bytes().as_ptr() as *mut efi::protocols::device_path::Protocol) + .ok_or(ImageError::InvalidParameter)?; + let handle = (self.load_from_device_path)(parent.as_raw(), ptr, boot_policy)?; + Handle::from_raw(handle).ok_or(ImageError::Internal) + } +} diff --git a/patina_dxe_core/src/uefi_services/protocol.rs b/patina_dxe_core/src/uefi_services/protocol.rs new file mode 100644 index 000000000..d93ceba2e --- /dev/null +++ b/patina_dxe_core/src/uefi_services/protocol.rs @@ -0,0 +1,449 @@ +//! DXE Core implementation of [`ProtocolServices`]. +//! +//! ## License +//! +//! Copyright (c) Microsoft Corporation. +//! +//! SPDX-License-Identifier: Apache-2.0 +//! + +use alloc::boxed::Box; +use alloc::vec; +use alloc::vec::Vec; +use core::ffi::c_void; + +use patina::BinaryGuid; +use patina::component::service::{ + IntoService, + uefi_services::protocol::{ + Handle, NotifyCallback, NotifyRegistration, ProtocolError, ProtocolPtr, ProtocolServices, Tpl, + }, +}; +use patina::error::EfiError; +use patina::standard::efi; + +use crate::events::EVENT_DB; +use crate::protocols::{PROTOCOL_DB, core_install_protocol_interface, core_uninstall_protocol_interface}; + +use super::event::tpl_to_efi; + +/// Owns a component-supplied installation-notify closure for the lifetime of a registration (until cancelled). +struct NotifyHolder { + callback: NotifyCallback, + registration: *mut c_void, +} + +/// C-ABI trampoline signaled when a watched protocol is installed. +/// +/// Drains the handles that the registration has flagged as installed and invokes the +/// component-supplied closure for each. +extern "efiapi" fn notify_install_trampoline(_event: efi::Event, context: *mut c_void) { + if context.is_null() { + return; + } + // SAFETY: `context` is the `NotifyHolder` created in `register_install_notify`, valid until + // `cancel_install_notify` reclaims it. Notifications dispatch serially at the event's TPL. + let holder = unsafe { &mut *(context as *mut NotifyHolder) }; + while let Some(handle) = PROTOCOL_DB.next_handle_for_registration(holder.registration) { + if let Some(handle) = Handle::from_raw(handle) { + (holder.callback)(handle); + } + } +} + +/// Core implementation of [`ProtocolServices`], delegating to the core protocol database through the +/// internal `core_*` Rust APIs. +#[derive(IntoService)] +#[service(dyn ProtocolServices)] +pub(crate) struct CoreProtocolServices; + +impl ProtocolServices for CoreProtocolServices { + fn install_interface( + &self, + handle: Option, + protocol: BinaryGuid, + interface: ProtocolPtr, + ) -> Result { + let caller_handle = handle.map(|handle| handle.as_raw()); + let installed = core_install_protocol_interface(caller_handle, protocol.into_inner(), interface.as_raw()) + .map_err(ProtocolError::from)?; + Handle::from_raw(installed).ok_or(ProtocolError::Internal) + } + + fn uninstall_interface( + &self, + handle: Handle, + protocol: BinaryGuid, + interface: ProtocolPtr, + ) -> Result<(), ProtocolError> { + core_uninstall_protocol_interface(handle.as_raw(), protocol.into_inner(), interface.as_raw()) + .map_err(ProtocolError::from) + } + + fn locate_interface(&self, protocol: BinaryGuid) -> Result { + let interface = PROTOCOL_DB.locate_protocol(protocol.into_inner()).map_err(ProtocolError::from)?; + ProtocolPtr::from_raw(interface).ok_or(ProtocolError::NotFound) + } + + fn locate_handles(&self, protocol: BinaryGuid) -> Result, ProtocolError> { + // "No handles present" is not an error, report it as an empty list so callers can iterate + // without special-casing absence. + match PROTOCOL_DB.locate_handles(Some(protocol.into_inner())) { + Ok(handles) => Ok(handles.into_iter().filter_map(Handle::from_raw).collect()), + Err(EfiError::NotFound) => Ok(Vec::new()), + Err(err) => Err(ProtocolError::from(err)), + } + } + + fn interface_on_handle(&self, handle: Handle, protocol: BinaryGuid) -> Result { + let interface = PROTOCOL_DB + .get_interface_for_handle(handle.as_raw(), protocol.into_inner()) + .map_err(ProtocolError::from)?; + ProtocolPtr::from_raw(interface).ok_or(ProtocolError::NotFound) + } + + fn register_install_notify( + &self, + protocol: BinaryGuid, + notify_tpl: Tpl, + callback: NotifyCallback, + ) -> Result { + let holder = Box::new(NotifyHolder { callback, registration: core::ptr::null_mut() }); + let context = Box::into_raw(holder) as *mut c_void; + + // Create a notify-signal event whose closure drains newly installed handles. + let event = match EVENT_DB.create_event( + efi::EVT_NOTIFY_SIGNAL, + tpl_to_efi(notify_tpl), + Some(notify_install_trampoline), + Some(context), + None, + ) { + Ok(event) => event, + Err(err) => { + // SAFETY: `context` came from `Box::into_raw` above and has not been freed. + drop(unsafe { Box::from_raw(context as *mut NotifyHolder) }); + return Err(ProtocolError::from(err)); + } + }; + + let registration = match PROTOCOL_DB.register_protocol_notify(protocol.into_inner(), event) { + Ok(registration) => registration, + Err(err) => { + let _ = EVENT_DB.close_event(event); + // SAFETY: `context` came from `Box::into_raw` above and has not been freed. + drop(unsafe { Box::from_raw(context as *mut NotifyHolder) }); + return Err(ProtocolError::from(err)); + } + }; + + // Record the registration key so the trampoline can drain matching handles. This runs + // before any notification can fire, since queued notifications dispatch only once the TPL + // drops below TPL_CALLBACK after this call returns. + // SAFETY: `context` is the `NotifyHolder` pointer created above and has not been freed. + unsafe { + (*(context as *mut NotifyHolder)).registration = registration; + } + + // Deliver handles that already have the protocol installed. + let _ = EVENT_DB.signal_event(event); + + Ok(NotifyRegistration::from_raw(event, registration, context)) + } + + fn cancel_install_notify(&self, registration: NotifyRegistration) -> Result<(), ProtocolError> { + let event = registration.event(); + let context = registration.context(); + + PROTOCOL_DB.unregister_protocol_notify_events(vec![event]); + + EVENT_DB.close_event(event).map_err(ProtocolError::from)?; + + if !context.is_null() { + // SAFETY: `context` is the `NotifyHolder` created in `register_install_notify`. The + // event is now closed and unregistered, so no further notification can reference it. + drop(unsafe { Box::from_raw(context as *mut NotifyHolder) }); + } + + Ok(()) + } +} + +#[cfg(test)] +#[cfg_attr(coverage, coverage(off))] +mod tests { + use super::*; + use crate::{events::restore_tpl, test_support}; + use core::str::FromStr; + use std::{cell::RefCell, rc::Rc}; + use uuid::Uuid; + + fn with_locked_state(f: F) { + test_support::with_clean_global_lock(|| { + test_support::init_test_logger(); + // Bring the shared TPL back to TPL_APPLICATION so `EVENT_DB.signal_event` (used by + // `register_install_notify`) dispatches queued notifications synchronously within this + // test, regardless of what an earlier test left it at. + restore_tpl(efi::TPL_APPLICATION); + f(); + }) + .unwrap(); + } + + fn test_guid(uuid_str: &str) -> BinaryGuid { + BinaryGuid::from_bytes(Uuid::from_str(uuid_str).unwrap().as_bytes()) + } + + fn fake_interface(addr: usize) -> ProtocolPtr { + ProtocolPtr::from_raw(addr as *mut c_void).unwrap() + } + + #[test] + fn test_protocol_services_install_interface_creates_new_handle() { + with_locked_state(|| { + let service = CoreProtocolServices; + let guid = test_guid("11111111-1111-1111-1111-111111111111"); + + let handle = service.install_interface(None, guid, fake_interface(0x1000)).unwrap(); + + assert!(!handle.as_raw().is_null()); + }); + } + + #[test] + fn test_protocol_services_install_interface_reuses_provided_handle() { + with_locked_state(|| { + let service = CoreProtocolServices; + let guid_a = test_guid("11111111-2222-1111-1111-111111111111"); + let guid_b = test_guid("22222222-3333-2222-2222-222222222222"); + + let handle = service.install_interface(None, guid_a, fake_interface(0x1000)).unwrap(); + let same_handle = service.install_interface(Some(handle), guid_b, fake_interface(0x2000)).unwrap(); + + assert_eq!(handle, same_handle); + }); + } + + #[test] + fn test_protocol_services_uninstall_interface_removes_protocol() { + with_locked_state(|| { + let service = CoreProtocolServices; + let guid = test_guid("33333333-3333-3333-3333-333333333333"); + let interface = fake_interface(0x1000); + + let handle = service.install_interface(None, guid, interface).unwrap(); + service.uninstall_interface(handle, guid, interface).unwrap(); + + assert_eq!(service.locate_interface(guid), Err(ProtocolError::NotFound)); + }); + } + + #[test] + fn test_protocol_services_uninstall_interface_mismatched_pointer_not_found() { + with_locked_state(|| { + let service = CoreProtocolServices; + let guid = test_guid("44444444-4444-4444-4444-444444444444"); + + let handle = service.install_interface(None, guid, fake_interface(0x1000)).unwrap(); + let result = service.uninstall_interface(handle, guid, fake_interface(0x2000)); + + assert_eq!(result, Err(ProtocolError::NotFound)); + }); + } + + #[test] + fn test_protocol_services_locate_interface_not_found() { + with_locked_state(|| { + let service = CoreProtocolServices; + let guid = test_guid("55555555-5555-5555-5555-555555555555"); + + assert_eq!(service.locate_interface(guid), Err(ProtocolError::NotFound)); + }); + } + + #[test] + fn test_protocol_services_locate_interface_returns_installed_interface() { + with_locked_state(|| { + let service = CoreProtocolServices; + let guid = test_guid("66666666-6666-6666-6666-666666666666"); + let interface = fake_interface(0x3000); + + service.install_interface(None, guid, interface).unwrap(); + + assert_eq!(service.locate_interface(guid).unwrap(), interface); + }); + } + + #[test] + fn test_protocol_services_locate_interface_null_interface_reports_not_found() { + with_locked_state(|| { + let service = CoreProtocolServices; + let guid = test_guid("77777777-7777-7777-7777-777777777777"); + + // A protocol can legitimately be installed with a null interface. + // `ProtocolPtr` cannot represent a null interface, so this reports NotFound even + //though PROTOCOL_DB has an entry for it. + PROTOCOL_DB.install_protocol_interface(None, guid.into_inner(), core::ptr::null_mut()).unwrap(); + + assert_eq!(service.locate_interface(guid), Err(ProtocolError::NotFound)); + }); + } + + #[test] + fn test_protocol_services_locate_handles_empty_when_not_found() { + with_locked_state(|| { + let service = CoreProtocolServices; + let guid = test_guid("88888888-8888-8888-8888-888888888888"); + + assert!(service.locate_handles(guid).unwrap().is_empty()); + }); + } + + #[test] + fn test_protocol_services_locate_handles_returns_installed_handles() { + with_locked_state(|| { + let service = CoreProtocolServices; + let guid = test_guid("99999999-9999-9999-9999-999999999999"); + + let handle_a = service.install_interface(None, guid, fake_interface(0x1000)).unwrap(); + let handle_b = service.install_interface(None, guid, fake_interface(0x2000)).unwrap(); + + let handles = service.locate_handles(guid).unwrap(); + + assert_eq!(handles.len(), 2); + assert!(handles.contains(&handle_a)); + assert!(handles.contains(&handle_b)); + }); + } + + #[test] + fn test_protocol_services_interface_on_handle_found() { + with_locked_state(|| { + let service = CoreProtocolServices; + let guid = test_guid("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"); + let interface = fake_interface(0x4000); + + let handle = service.install_interface(None, guid, interface).unwrap(); + + assert_eq!(service.interface_on_handle(handle, guid).unwrap(), interface); + }); + } + + #[test] + fn test_protocol_services_interface_on_handle_not_found() { + with_locked_state(|| { + let service = CoreProtocolServices; + let installed_guid = test_guid("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"); + let other_guid = test_guid("cccccccc-cccc-cccc-cccc-cccccccccccc"); + + let handle = service.install_interface(None, installed_guid, fake_interface(0x5000)).unwrap(); + + assert_eq!(service.interface_on_handle(handle, other_guid), Err(ProtocolError::NotFound)); + }); + } + + #[test] + fn test_protocol_services_register_install_notify_fires_for_already_installed_handle() { + with_locked_state(|| { + let service = CoreProtocolServices; + let guid = test_guid("dddddddd-dddd-dddd-dddd-dddddddddddd"); + let handle = service.install_interface(None, guid, fake_interface(0x6000)).unwrap(); + + let seen: Rc>> = Rc::new(RefCell::new(Vec::new())); + let recorder = Rc::clone(&seen); + let registration = service + .register_install_notify(guid, Tpl::Callback, Box::new(move |h| recorder.borrow_mut().push(h))) + .unwrap(); + + // The handle already had the protocol installed, so the callback fires synchronously + // as part of registration, before any future install occurs. + assert_eq!(seen.borrow().len(), 1); + assert_eq!(seen.borrow()[0], handle); + + service.cancel_install_notify(registration).unwrap(); + }); + } + + #[test] + fn test_protocol_services_register_install_notify_fires_for_future_install() { + with_locked_state(|| { + let service = CoreProtocolServices; + let guid = test_guid("eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee"); + + let seen: Rc>> = Rc::new(RefCell::new(Vec::new())); + let recorder = Rc::clone(&seen); + let registration = service + .register_install_notify(guid, Tpl::Callback, Box::new(move |h| recorder.borrow_mut().push(h))) + .unwrap(); + + // Nothing was installed for this protocol at registration time. + assert!(seen.borrow().is_empty()); + + let handle = service.install_interface(None, guid, fake_interface(0x7000)).unwrap(); + + assert_eq!(seen.borrow().len(), 1); + assert_eq!(seen.borrow()[0], handle); + + service.cancel_install_notify(registration).unwrap(); + }); + } + + #[test] + fn test_protocol_services_cancel_install_notify_stops_future_notifications() { + with_locked_state(|| { + let service = CoreProtocolServices; + let guid = test_guid("ffffffff-ffff-ffff-ffff-ffffffffffff"); + + let count = Rc::new(RefCell::new(0usize)); + let counter = Rc::clone(&count); + let registration = service + .register_install_notify(guid, Tpl::Callback, Box::new(move |_h| *counter.borrow_mut() += 1)) + .unwrap(); + + service.cancel_install_notify(registration).unwrap(); + service.install_interface(None, guid, fake_interface(0x8000)).unwrap(); + + assert_eq!(*count.borrow(), 0); + }); + } + + #[test] + fn test_protocol_services_cancel_install_notify_frees_context() { + with_locked_state(|| { + let service = CoreProtocolServices; + let guid = test_guid("12121212-1212-1212-1212-121212121212"); + + // The closure's capture is the only thing that can prove the boxed `NotifyHolder` (and + // this `Rc`) was actually dropped, rather than leaked, by `cancel_install_notify`. + let marker = Rc::new(()); + let captured = Rc::clone(&marker); + let registration = service + .register_install_notify( + guid, + Tpl::Callback, + Box::new(move |_h| { + let _ = &captured; + }), + ) + .unwrap(); + + assert_eq!(Rc::strong_count(&marker), 2); + + service.cancel_install_notify(registration).unwrap(); + + assert_eq!(Rc::strong_count(&marker), 1); + }); + } + + #[test] + fn test_protocol_services_cancel_install_notify_invalid_registration() { + with_locked_state(|| { + let service = CoreProtocolServices; + // An event value that was never created by `EVENT_DB`, so closing it must fail. + let bogus = + NotifyRegistration::from_raw(0x7FFF_FFFF as *mut c_void, core::ptr::null_mut(), core::ptr::null_mut()); + + assert_eq!(service.cancel_install_notify(bogus), Err(ProtocolError::InvalidParameter)); + }); + } +} diff --git a/patina_dxe_core/src/uefi_services/timer_event.rs b/patina_dxe_core/src/uefi_services/timer_event.rs new file mode 100644 index 000000000..95fb7b1b2 --- /dev/null +++ b/patina_dxe_core/src/uefi_services/timer_event.rs @@ -0,0 +1,124 @@ +//! DXE Core implementation of [`TimerEventServices`]. +//! +//! ## License +//! +//! Copyright (c) Microsoft Corporation. +//! +//! SPDX-License-Identifier: Apache-2.0 +//! + +use patina::component::service::{ + IntoService, + uefi_services::{ + event::{Event, EventError, EventNotifyCallback}, + timer_event::{TimerEventServices, TimerType, Tpl}, + }, +}; +use patina::error::EfiError; +use patina::standard::efi; + +use crate::events::set_timer as core_set_timer; + +use super::event::create_event_internal; + +/// Core implementation of [`TimerEventServices`], delegating to the core event database. +/// +/// Registered with the component dispatcher only once the Timer Architectural Protocol is +/// installed, so components depending on this service are not dispatched until `set_timer` can +/// actually take effect. +#[derive(IntoService)] +#[service(dyn TimerEventServices)] +pub(crate) struct CoreTimerEventServices; + +impl TimerEventServices for CoreTimerEventServices { + fn create_timer_event(&self, notify_tpl: Tpl, callback: EventNotifyCallback) -> Result { + create_event_internal(efi::EVT_TIMER | efi::EVT_NOTIFY_SIGNAL, notify_tpl, callback, None) + } + + fn set_timer(&self, event: Event, timer_type: TimerType) -> Result<(), EventError> { + // Note: UEFI timer intervals are expressed in units of 100ns. + let (delay, trigger_time) = match timer_type { + TimerType::Cancel => (efi::TIMER_CANCEL, 0), + TimerType::Relative(interval) => (efi::TIMER_RELATIVE, (interval.as_nanos() / 100) as u64), + TimerType::Periodic(interval) => (efi::TIMER_PERIODIC, (interval.as_nanos() / 100) as u64), + }; + + match core_set_timer(event.as_raw(), delay, trigger_time) { + efi::Status::SUCCESS => Ok(()), + status => Err(EventError::from(EfiError::status_to_result(status).unwrap_err())), + } + } +} + +#[cfg(test)] +#[cfg_attr(coverage, coverage(off))] +mod tests { + use super::*; + use crate::{events::EVENT_DB, test_support}; + use alloc::boxed::Box; + use core::time::Duration; + + fn with_locked_state(f: F) { + test_support::with_global_lock(f).unwrap(); + } + + fn create_timer_event() -> Event { + CoreTimerEventServices.create_timer_event(Tpl::Notify, Box::new(|| {})).unwrap() + } + + #[test] + fn test_timer_event_services_create_timer_event_smoke() { + with_locked_state(|| { + let event = create_timer_event(); + + assert!(EVENT_DB.close_event(event.as_raw()).is_ok()); + }); + } + + #[test] + fn test_timer_event_services_set_timer_cancel() { + with_locked_state(|| { + let event = create_timer_event(); + + assert_eq!(CoreTimerEventServices.set_timer(event, TimerType::Cancel), Ok(())); + + assert!(EVENT_DB.close_event(event.as_raw()).is_ok()); + }); + } + + #[test] + fn test_timer_event_services_set_timer_relative() { + with_locked_state(|| { + let event = create_timer_event(); + + let result = CoreTimerEventServices.set_timer(event, TimerType::Relative(Duration::from_millis(500))); + assert_eq!(result, Ok(())); + + assert!(EVENT_DB.close_event(event.as_raw()).is_ok()); + }); + } + + #[test] + fn test_timer_event_services_set_timer_periodic() { + with_locked_state(|| { + let event = create_timer_event(); + + let result = CoreTimerEventServices.set_timer(event, TimerType::Periodic(Duration::from_millis(100))); + assert_eq!(result, Ok(())); + + assert!(EVENT_DB.close_event(event.as_raw()).is_ok()); + }); + } + + #[test] + fn test_timer_event_services_set_timer_invalid_event_returns_err() { + with_locked_state(|| { + let event = create_timer_event(); + assert!(EVENT_DB.close_event(event.as_raw()).is_ok()); + + // Same handle, now absent from EVENT_DB after close, exercises set_timer's error path. + let result = CoreTimerEventServices.set_timer(event, TimerType::Relative(Duration::from_millis(10))); + assert_eq!(result, Err(EventError::InvalidParameter)); + }); + } +} diff --git a/patina_dxe_core/src/uefi_services/timing.rs b/patina_dxe_core/src/uefi_services/timing.rs new file mode 100644 index 000000000..f3764aa11 --- /dev/null +++ b/patina_dxe_core/src/uefi_services/timing.rs @@ -0,0 +1,96 @@ +//! DXE Core implementation of [`TimingServices`]. +//! +//! ## License +//! +//! Copyright (c) Microsoft Corporation. +//! +//! SPDX-License-Identifier: Apache-2.0 +//! + +use patina::component::service::{ + IntoService, + uefi_services::timing::{TimingError, TimingServices}, +}; + +use crate::misc_boot_services::{core_set_watchdog_timer, core_stall}; + +/// Core implementation of [`TimingServices`], delegating to the metronome and watchdog +/// architectural support through the core's internal Rust APIs. +/// +/// Registered with the component dispatcher only once both the Metronome and Watchdog Timer +/// Architectural Protocols are installed, so components depending on this service are not dispatched +/// until stall and watchdog timer control is available. +#[derive(IntoService)] +#[service(dyn TimingServices)] +pub(crate) struct CoreTimingServices; + +impl TimingServices for CoreTimingServices { + fn stall(&self, duration: core::time::Duration) -> Result<(), TimingError> { + core_stall(duration.as_micros() as usize).map_err(TimingError::from) + } + + fn set_watchdog_timer(&self, timeout_seconds: u64, watchdog_code: u64) -> Result<(), TimingError> { + core_set_watchdog_timer(timeout_seconds as usize, watchdog_code).map_err(TimingError::from) + } +} + +#[cfg(test)] +#[cfg_attr(coverage, coverage(off))] +mod tests { + use super::*; + use crate::test_support; + use core::time::Duration; + + // The arch protocols don't have test reset at the moment, so these just log instead of asserting. + fn check_stall_result(duration: Duration, result: Result<(), TimingError>) { + match result { + Err(TimingError::NotReady) => { + log::debug!("stall({duration:?}) correctly returned NotReady"); + } + Ok(()) => { + log::debug!("stall({duration:?}) returned Ok (metronome arch protocol available)"); + } + Err(other) => { + log::warn!("stall({duration:?}) returned unexpected error: {other:?}"); + } + } + } + + #[test] + fn stall_delegates_and_translates_not_ready_error() { + test_support::with_global_lock(|| { + let svc = CoreTimingServices; + check_stall_result(Duration::ZERO, svc.stall(Duration::ZERO)); + check_stall_result(Duration::from_micros(1), svc.stall(Duration::from_micros(1))); + check_stall_result(Duration::from_millis(10), svc.stall(Duration::from_millis(10))); + }) + .unwrap(); + } + + fn check_watchdog_result(timeout_seconds: u64, result: Result<(), TimingError>) { + // The arch protocols don't have test reset at the moment, so these just log instead of asserting. + match result { + Err(TimingError::NotReady) => { + log::debug!("set_watchdog_timer({timeout_seconds}) correctly returned NotReady"); + } + Ok(()) => { + log::debug!("set_watchdog_timer({timeout_seconds}) returned Ok (watchdog available)"); + } + Err(other) => { + log::warn!("set_watchdog_timer({timeout_seconds}) returned unexpected error: {other:?}"); + } + } + } + + #[test] + fn set_watchdog_timer_delegates_and_translates_not_ready_error() { + test_support::with_global_lock(|| { + let svc = CoreTimingServices; + // A timeout of 0 disables the watchdog timer (per the UEFI spec). + check_watchdog_result(0, svc.set_watchdog_timer(0, 0)); + check_watchdog_result(300, svc.set_watchdog_timer(300, 0)); + check_watchdog_result(300, svc.set_watchdog_timer(300, 0x1234)); + }) + .unwrap(); + } +} diff --git a/patina_dxe_core/src/uefi_services/tpl.rs b/patina_dxe_core/src/uefi_services/tpl.rs new file mode 100644 index 000000000..1a59470ba --- /dev/null +++ b/patina_dxe_core/src/uefi_services/tpl.rs @@ -0,0 +1,73 @@ +//! DXE Core implementation of [`TplServices`]. +//! +//! ## License +//! +//! Copyright (c) Microsoft Corporation. +//! +//! SPDX-License-Identifier: Apache-2.0 +//! + +use patina::component::service::{ + IntoService, + uefi_services::tpl::{PreviousTpl, Tpl, TplServices}, +}; + +use crate::events::{raise_tpl, restore_tpl}; +use crate::uefi_services::event::tpl_to_efi; +/// Core implementation of [`TplServices`], delegating to the core TPL primitives. +#[derive(IntoService)] +#[service(dyn TplServices)] +pub(crate) struct CoreTplServices; + +impl TplServices for CoreTplServices { + fn raise_tpl(&self, tpl: Tpl) -> PreviousTpl { + PreviousTpl::from_raw(raise_tpl(tpl_to_efi(tpl))) + } + + fn restore_tpl(&self, previous: PreviousTpl) { + restore_tpl(previous.as_raw()); + } +} + +#[cfg(test)] +#[cfg_attr(coverage, coverage(off))] +mod tests { + use super::*; + use patina::standard::efi; + + #[test] + fn test_tpl_to_efi_maps_all_variants() { + assert_eq!(tpl_to_efi(Tpl::Application), efi::TPL_APPLICATION); + assert_eq!(tpl_to_efi(Tpl::Callback), efi::TPL_CALLBACK); + assert_eq!(tpl_to_efi(Tpl::Notify), efi::TPL_NOTIFY); + assert_eq!(tpl_to_efi(Tpl::HighLevel), efi::TPL_HIGH_LEVEL); + } + + #[test] + fn test_core_tpl_services_raise_and_restore_round_trip() { + crate::test_support::with_global_lock(|| { + let service = CoreTplServices; + + let previous = service.raise_tpl(Tpl::Callback); + assert_eq!(previous.as_raw(), efi::TPL_APPLICATION); + + service.restore_tpl(previous); + }) + .unwrap(); + } + + #[test] + fn test_core_tpl_services_nested_raise_restore_round_trip() { + crate::test_support::with_global_lock(|| { + let service = CoreTplServices; + + let previous_callback = service.raise_tpl(Tpl::Callback); + let previous_notify = service.raise_tpl(Tpl::Notify); + + // Check that the innermost raise restores first. + service.restore_tpl(previous_notify); + service.restore_tpl(previous_callback); + }) + .unwrap(); + } +} diff --git a/sdk/patina/src/component/service.rs b/sdk/patina/src/component/service.rs index d128e5b36..461c14eb6 100644 --- a/sdk/patina/src/component/service.rs +++ b/sdk/patina/src/component/service.rs @@ -128,6 +128,7 @@ pub mod dxe_dispatch; pub mod memory; pub mod perf_timer; pub mod performance; +pub mod uefi_services; pub use patina_macro::IntoService; diff --git a/sdk/patina/src/component/service/uefi_services.rs b/sdk/patina/src/component/service/uefi_services.rs new file mode 100644 index 000000000..eac7c2a54 --- /dev/null +++ b/sdk/patina/src/component/service/uefi_services.rs @@ -0,0 +1,53 @@ +//! UEFI Services for Patina components. +//! +//! This module defines a set of service traits that expose UEFI Boot Services (and, +//! over time, Runtime Services) to Patina components as Rust APIs. +//! +//! These services are the recommended way for components to consume UEFI functionality. +//! Unlike the raw [`crate::uefi::boot_services`] abstractions, which wrap the C +//! `EFI_BOOT_SERVICES` function-pointer table, these services are implemented by the +//! Patina DXE Core directly against its internal Rust APIs. Components therefore do not +//! interact with C-style constructs such as the boot services table or raw pointers until +//! they go down an unavoidable path into C code, such as calling a protocol function +//! pointer when the protocol is produced by a C driver. +//! +//! # Design +//! +//! Each service group is defined as a trait in this module and implemented by the core. +//! Components declare a dependency on a service by adding a [`Service`] parameter +//! to their entry point. The trait is only made available once the core registers its +//! implementation, so a component depending on a service is guaranteed the service is ready. +//! Some services may be deferred until underlying dependencies such as an architectural protocol +//! or other platform-dependent service is available. +//! +//! Services are split into cohesive functional groups so that a component can declare a more +//! granular dependency on the functionality it needs: +//! +//! - [`config_table::ConfigurationTableServices`] - Configuration table installation and lookup. +//! - [`driver::DriverServices`] - Connecting and disconnecting drivers to controllers. +//! - [`event::EventServices`] - Events, using Rust closures for notifications. +//! - [`image::ImageServices`] - Loading, starting, and unloading UEFI images. +//! - [`protocol::ProtocolServices`] - Typed protocol installation and discovery. +//! - [`timer_event::TimerEventServices`] - Timer events, available once the Timer Architectural +//! Protocol is installed. +//! - [`timing::TimingServices`] - Delays and the Watchdog timer. +//! - [`tpl::TplServices`] - Raising and restoring the Task Priority Level (TPL). +//! +//! [`Service`]: crate::component::service::Service +//! +//! ## License +//! +//! Copyright (c) Microsoft Corporation. +//! +//! SPDX-License-Identifier: Apache-2.0 +//! + +pub mod config_table; +pub mod driver; +pub mod event; +pub mod handle; +pub mod image; +pub mod protocol; +pub mod timer_event; +pub mod timing; +pub mod tpl; diff --git a/sdk/patina/src/component/service/uefi_services/config_table.rs b/sdk/patina/src/component/service/uefi_services/config_table.rs new file mode 100644 index 000000000..0718286eb --- /dev/null +++ b/sdk/patina/src/component/service/uefi_services/config_table.rs @@ -0,0 +1,521 @@ +//! Configuration table services for Patina components. +//! +//! [`ConfigurationTableServices`] exposes UEFI configuration table installation and lookup. The +//! trait is object-safe and works with the opaque [`ConfigTablePtr`] token. +//! +//! Configuration tables associate a GUID with a pointer to a vendor-defined table (for example +//! ACPI or SMBIOS tables). The UEFI Specification allows at most one table per GUID. +//! +//! Component authors should install and retrieve fixed-size tables through +//! [`ConfigurationTableServicesExt`] rather than using [`ConfigurationTableServices`] directly. +//! +//! Implement [`ConfigTable`] to bind a GUID to a concrete Rust type, then use +//! [`ConfigurationTableServicesExt::install`] (or [`ConfigurationTableServicesExt::install_or_replace`] +//! for a table that is republished, such as after each record added to it) and [`ConfigurationTableServicesExt::get`]. +//! These methods never expose a raw pointer to the caller, and verify (at runtime) that a lookup's +//! requested type matches the type it was installed with, so [`ConfigurationTableServicesExt::get`] is not `unsafe`. +//! +//! [`ConfigTable`] still requires a fixed-size Rust type, but that type may be a self-describing +//! header for a table with trailing variable-length data. Override [`ConfigTable::table_len`] to report +//! the table's real, total size, and read the whole table (header plus trailing data) with +//! [`ConfigurationTableServicesExt::get_bytes`]. Tables that cannot be represented as a single Rust +//! type at all, because their total size depends on data assembled outside of Patina's control, +//! can fall back to using the `unsafe` [`ConfigurationTableServices::install_table`] with a raw +//! [`ConfigTablePtr`]. +//! +//! ## License +//! +//! Copyright (c) Microsoft Corporation. +//! +//! SPDX-License-Identifier: Apache-2.0 +//! + +use core::any::TypeId; +use core::ffi::c_void; +use core::ptr::NonNull; + +use crate::base::error::EfiError; +use crate::base::guid::BinaryGuid; + +#[cfg(any(test, feature = "mockall"))] +use mockall::automock; + +/// An opaque pointer to a vendor configuration table. +/// +/// This token is consumed by [`ConfigurationTableServices::install_table`] and returned by +/// [`ConfigurationTableServices::get_table`]. Components should generally use the typed methods on +/// [`ConfigurationTableServicesExt`] rather than handling this token directly. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ConfigTablePtr(NonNull); + +impl ConfigTablePtr { + /// Wraps a raw table pointer. + /// + /// This is intended for use by service implementations and the typed extension methods, not + /// component authors. + #[doc(hidden)] + pub fn from_raw(table: *mut c_void) -> Option { + NonNull::new(table).map(Self) + } + + /// Returns the raw table pointer. + /// + /// This is intended for use by service implementations and the typed extension methods, not + /// component authors. + #[doc(hidden)] + pub fn as_raw(&self) -> *mut c_void { + self.0.as_ptr() + } +} + +/// Errors that can occur when using [`ConfigurationTableServices`]. +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub enum ConfigTableError { + /// A provided parameter was invalid. + InvalidParameter, + /// The requested table (or the system table) was not found. + NotFound, + /// The system is out of resources to complete the operation. + OutOfResources, + /// A table is already installed under the requested GUID. + AlreadyExists, + /// An unexpected internal error occurred. + Internal, +} + +impl From for EfiError { + fn from(value: ConfigTableError) -> Self { + match value { + ConfigTableError::InvalidParameter => EfiError::InvalidParameter, + ConfigTableError::NotFound => EfiError::NotFound, + ConfigTableError::OutOfResources => EfiError::OutOfResources, + ConfigTableError::AlreadyExists => EfiError::AlreadyStarted, + ConfigTableError::Internal => EfiError::Unsupported, + } + } +} + +impl From for ConfigTableError { + fn from(value: EfiError) -> Self { + match value { + EfiError::InvalidParameter => ConfigTableError::InvalidParameter, + EfiError::NotFound => ConfigTableError::NotFound, + EfiError::OutOfResources => ConfigTableError::OutOfResources, + EfiError::AlreadyStarted => ConfigTableError::AlreadyExists, + _ => ConfigTableError::Internal, + } + } +} + +/// Configuration table installation and lookup services. +/// +/// This trait is object-safe and deals in opaque tokens. Component authors should generally use the +/// type-safe methods provided by [`ConfigurationTableServicesExt`] instead of this trait directly. +/// +/// This service is implemented by the Patina DXE Core. Components consume it by adding a +/// [`Service`](crate::component::service::Service) parameter to +/// their entry point. +/// +/// # Examples +/// +/// ```rust,no_run +/// use patina::BinaryGuid; +/// use patina::component::service::{ +/// Service, +/// uefi_services::config_table::{ConfigTable, ConfigurationTableServices, ConfigurationTableServicesExt}, +/// }; +/// use patina::error::Result; +/// +/// #[repr(C)] +/// struct VendorTable { +/// version: u32, +/// } +/// +/// impl ConfigTable for VendorTable { +/// const TABLE_GUID: BinaryGuid = BinaryGuid::from_string("0fedcba9-8765-4321-fedc-ba9876543210"); +/// } +/// +/// static TABLE: VendorTable = VendorTable { version: 1 }; +/// +/// fn entry_point(config: Service) -> Result<()> { +/// // Publish the table so an OS or later component can find it by GUID. +/// config.install(&TABLE)?; +/// Ok(()) +/// } +/// ``` +#[cfg_attr(any(test, feature = "mockall"), automock)] +pub trait ConfigurationTableServices { + /// Installs or replaces the configuration table associated with `guid`. + /// + /// # Errors + /// + /// Returns [`ConfigTableError::NotFound`] if the system table is not available, or + /// [`ConfigTableError::OutOfResources`] if the table could not be stored. + /// + /// # Safety + /// + /// `table` must point to valid, initialized memory of the type expected by consumers of `guid`, and + /// that memory must remain valid for as long as the table stays installed. Since a configuration + /// table may be looked up at any later point, including by other components or by the OS after + /// `ExitBootServices`, this is effectively a `'static` requirement. + unsafe fn install_table(&self, guid: BinaryGuid, table: ConfigTablePtr) -> Result<(), ConfigTableError>; + + /// Removes the configuration table associated with `guid`. + /// + /// # Errors + /// + /// Returns [`ConfigTableError::NotFound`] if no table is installed for `guid`. + fn remove_table(&self, guid: BinaryGuid) -> Result<(), ConfigTableError>; + + /// Returns the configuration table associated with `guid`, if present. + fn get_table(&self, guid: BinaryGuid) -> Option; + + /// Installs the configuration table associated with `guid`, recording `type_id` alongside it. + /// + /// This is the primitive backing [`ConfigurationTableServicesExt::install`]. Component authors + /// should generally use that method instead. + /// + /// # Errors + /// + /// Returns [`ConfigTableError::AlreadyExists`] if a table is already installed under `guid`. + /// + /// # Safety + /// + /// `table` must point to valid, initialized memory of the type identified by `type_id`, and that + /// memory must remain valid for as long as the table stays installed (effectively `'static`), since + /// [`ConfigurationTableServicesExt::get`] later trusts `type_id` alone before casting the pointer. + unsafe fn install_typed_table( + &self, + guid: BinaryGuid, + type_id: TypeId, + table: ConfigTablePtr, + ) -> Result<(), ConfigTableError>; + + /// Returns the configuration table associated with `guid`, if present and if it was installed + /// with the same `type_id`. + /// + /// This is the primitive backing [`ConfigurationTableServicesExt::get`]. Component authors + /// should generally use that method instead. + fn get_typed_table(&self, guid: BinaryGuid, type_id: TypeId) -> Option; + + /// Removes the configuration table associated with `guid`, along with its recorded type. + /// + /// This is the primitive backing [`ConfigurationTableServicesExt::remove`]. Component authors + /// should generally use that method instead. + /// + /// # Errors + /// + /// Returns [`ConfigTableError::NotFound`] if no table is installed for `guid`. + fn remove_typed_table(&self, guid: BinaryGuid) -> Result<(), ConfigTableError>; + + /// Installs the configuration table associated with `guid`, replacing any existing table (typed + /// or not) under the same GUID and recording `type_id` alongside it. + /// + /// This is the primitive backing [`ConfigurationTableServicesExt::install_or_replace`]. Component + /// authors should generally use that method instead. + /// + /// # Errors + /// + /// Returns [`ConfigTableError::NotFound`] if the system table is not available, or + /// [`ConfigTableError::OutOfResources`] if the table could not be stored. + /// + /// # Safety + /// + /// `table` must point to valid, initialized memory of the type identified by `type_id`, and that + /// memory must remain valid for as long as the table stays installed (effectively `'static`), since + /// [`ConfigurationTableServicesExt::get`] later trusts `type_id` alone before casting the pointer. + unsafe fn replace_typed_table( + &self, + guid: BinaryGuid, + type_id: TypeId, + table: ConfigTablePtr, + ) -> Result<(), ConfigTableError>; +} + +/// A Rust type that can be installed as a fixed-size UEFI configuration table. +/// +/// Implementing this trait binds a given type to the GUID it is published under, so +/// [`ConfigurationTableServicesExt::install`] and [`ConfigurationTableServicesExt::get`] can install +/// and retrieve it without handling a raw pointer or repeating the GUID at each call site. +/// +/// This trait is only for tables whose size is known at compile time. Dynamically-sized tables must use +/// the `unsafe` [`ConfigurationTableServices::install_table`] with a raw [`ConfigTablePtr`] instead. +/// +/// ## Example +/// +/// ```rust +/// use patina::BinaryGuid; +/// use patina::component::service::{ +/// Service, +/// uefi_services::config_table::{ConfigTable, ConfigurationTableServices, ConfigurationTableServicesExt}, +/// }; +/// use patina::error::Result; +/// +/// #[repr(C)] +/// struct VendorTable { +/// version: u32, +/// } +/// +/// impl ConfigTable for VendorTable { +/// const TABLE_GUID: BinaryGuid = BinaryGuid::from_string("0fedcba9-8765-4321-fedc-ba9876543210"); +/// } +/// +/// static TABLE: VendorTable = VendorTable { version: 1 }; +/// +/// fn entry_point(config: Service) -> Result<()> { +/// config.install(&TABLE)?; +/// let installed: Option<&VendorTable> = config.get::(); +/// Ok(()) +/// } +/// ``` +pub trait ConfigTable: Sized + 'static { + /// The GUID this table type is published under. + const TABLE_GUID: BinaryGuid; + + /// The total size, in bytes, of the table starting at `self`'s address, including any + /// trailing variable-length data laid out immediately after `self` in the same allocation. + /// + /// Defaults to `size_of::()`, which is correct for a table with no trailing data. + /// Override this for a self-describing header (for example, one with its own `Length` field) + /// whose real total size is larger than the header type alone. + /// + /// [`ConfigurationTableServicesExt::get_bytes`] uses this value to return the whole table. + fn table_len(&self) -> usize { + size_of::() + } +} + +/// Type-safe extension methods for [`ConfigurationTableServices`]. +/// +/// [`Self::install`], [`Self::install_or_replace`], [`Self::get`], [`Self::get_bytes`], and +/// [`Self::remove`] work with any `T: `[`ConfigTable`]. The GUID is always `T::TABLE_GUID`, and the +/// underlying service verifies the installed type before a lookup casts the pointer, so +/// [`Self::get`] is not `unsafe`. +pub trait ConfigurationTableServicesExt: ConfigurationTableServices { + /// Installs `table` as a [`ConfigTable`]. + /// + /// Fails rather than silently replacing an existing table if one is already installed under + /// `T::TABLE_GUID`. Use [`Self::install_or_replace`] for a table that is expected to be + /// republished. + /// + /// # Errors + /// + /// Returns [`ConfigTableError::AlreadyExists`] if a table is already installed under + /// `T::TABLE_GUID`. + fn install(&self, table: &'static T) -> Result<(), ConfigTableError> { + let ptr = ConfigTablePtr::from_raw(core::ptr::from_ref(table) as *mut c_void) + .ok_or(ConfigTableError::InvalidParameter)?; + // SAFETY: `ptr` was derived from `table`, a `&'static T`, so it remains valid for as long as + // the table could possibly stay installed. + unsafe { self.install_typed_table(T::TABLE_GUID, TypeId::of::(), ptr) } + } + + /// Installs `table` as a [`ConfigTable`], replacing any table already installed under + /// `T::TABLE_GUID`. + /// + /// Use this instead of [`Self::install`] for a table that is republished throughout boot + /// (for example, after each record added to it), rather than installed exactly once. + /// + /// # Errors + /// + /// Returns [`ConfigTableError::NotFound`] if the system table is not available, or + /// [`ConfigTableError::OutOfResources`] if the table could not be stored. + fn install_or_replace(&self, table: &'static T) -> Result<(), ConfigTableError> { + let ptr = ConfigTablePtr::from_raw(core::ptr::from_ref(table) as *mut c_void) + .ok_or(ConfigTableError::InvalidParameter)?; + // SAFETY: `ptr` was derived from `table`, a `&'static T`, so it remains valid for as long as + // the table could possibly stay installed. + unsafe { self.replace_typed_table(T::TABLE_GUID, TypeId::of::(), ptr) } + } + + /// Returns the table installed under `T::TABLE_GUID`, if present. + /// + /// Returns `None` if no table is installed under `T::TABLE_GUID`, or if the installed table was + /// not installed as `T` (for example, using [`ConfigurationTableServices::install_table`] with a + /// mismatched type). + fn get(&self) -> Option<&'static T> { + let ptr = self.get_typed_table(T::TABLE_GUID, TypeId::of::())?; + // SAFETY: `get_typed_table` only returns `Some` when the table under `T::TABLE_GUID` was + // recorded with `TypeId::of::()`, which only happens in `install`, so the pointer was + // installed from a `&'static T` and is valid, aligned, and lives for a `'static` lifetime. + Some(unsafe { &*(ptr.as_raw() as *const T) }) + } + + /// Returns the whole table installed under `T::TABLE_GUID` as bytes, including any + /// trailing variable-length data, if present. + /// + /// Returns `None` if no table is installed under `T::TABLE_GUID`, or if the installed table was + /// not installed as `T`. + /// + /// # Safety + /// + /// The caller must ensure `T::table_len` accurately reports the number of bytes allocated + /// starting at the installed table's address. + unsafe fn get_bytes(&self) -> Option<&'static [u8]> { + let ptr = self.get_typed_table(T::TABLE_GUID, TypeId::of::())?; + // SAFETY: see `Self::get` for why `ptr` is a valid, aligned, `'static` `T`. + let table = unsafe { &*(ptr.as_raw() as *const T) }; + // SAFETY: the caller guarantees `table.table_len()` bytes are valid to read starting here. + Some(unsafe { core::slice::from_raw_parts(ptr.as_raw() as *const u8, table.table_len()) }) + } + + /// Removes the table installed under `T::TABLE_GUID`. + /// + /// # Errors + /// + /// Returns [`ConfigTableError::NotFound`] if no table is installed under `T::TABLE_GUID`. + fn remove(&self) -> Result<(), ConfigTableError> { + self.remove_typed_table(T::TABLE_GUID) + } +} + +impl ConfigurationTableServicesExt for T {} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_configuration_table_services_error_conversions() { + assert_eq!(EfiError::from(ConfigTableError::InvalidParameter), EfiError::InvalidParameter); + assert_eq!(EfiError::from(ConfigTableError::NotFound), EfiError::NotFound); + assert_eq!(EfiError::from(ConfigTableError::OutOfResources), EfiError::OutOfResources); + assert_eq!(EfiError::from(ConfigTableError::AlreadyExists), EfiError::AlreadyStarted); + assert_eq!(EfiError::from(ConfigTableError::Internal), EfiError::Unsupported); + assert_eq!(ConfigTableError::from(EfiError::NotFound), ConfigTableError::NotFound); + assert_eq!(ConfigTableError::from(EfiError::AlreadyStarted), ConfigTableError::AlreadyExists); + assert_eq!(ConfigTableError::from(EfiError::DeviceError), ConfigTableError::Internal); + } + + #[test] + fn test_configuration_table_services_ptr_from_raw_is_none() { + assert!(ConfigTablePtr::from_raw(core::ptr::null_mut()).is_none()); + } + + #[repr(C)] + struct FakeConfigTable { + value: u32, + } + + impl ConfigTable for FakeConfigTable { + const TABLE_GUID: BinaryGuid = + BinaryGuid::from_fields(0x1111_2222, 0x3333, 0x4444, 0x55, 0x66, &[7, 8, 9, 10, 11, 12]); + } + + static FAKE_CONFIG_TABLE: FakeConfigTable = FakeConfigTable { value: 42 }; + + #[test] + fn test_config_table_ext_install_then_get() { + let mut mock = MockConfigurationTableServices::new(); + mock.expect_install_typed_table().times(1).returning(|guid, type_id, _| { + assert_eq!(guid, FakeConfigTable::TABLE_GUID); + assert_eq!(type_id, TypeId::of::()); + Ok(()) + }); + mock.expect_get_typed_table().times(1).returning(|guid, type_id| { + assert_eq!(guid, FakeConfigTable::TABLE_GUID); + assert_eq!(type_id, TypeId::of::()); + ConfigTablePtr::from_raw(&raw const FAKE_CONFIG_TABLE as *mut c_void) + }); + + mock.install(&FAKE_CONFIG_TABLE).unwrap(); + let table = mock.get::().unwrap(); + assert_eq!(table.value, 42); + } + + #[test] + fn test_config_table_ext_install_rejects_duplicate() { + let mut mock = MockConfigurationTableServices::new(); + mock.expect_install_typed_table().times(1).returning(|_, _, _| Err(ConfigTableError::AlreadyExists)); + + assert_eq!(mock.install(&FAKE_CONFIG_TABLE), Err(ConfigTableError::AlreadyExists)); + } + + #[test] + fn test_config_table_ext_get_returns_none_on_type_mismatch() { + let mut mock = MockConfigurationTableServices::new(); + mock.expect_get_typed_table().times(1).returning(|_, _| None); + + assert!(mock.get::().is_none()); + } + + #[test] + fn test_config_table_ext_remove() { + let mut mock = MockConfigurationTableServices::new(); + mock.expect_remove_typed_table().times(1).returning(|guid| { + assert_eq!(guid, FakeConfigTable::TABLE_GUID); + Ok(()) + }); + + assert_eq!(mock.remove::(), Ok(())); + } + + #[test] + fn test_config_table_ext_install_or_replace() { + let mut mock = MockConfigurationTableServices::new(); + mock.expect_replace_typed_table().times(1).returning(|guid, type_id, _| { + assert_eq!(guid, FakeConfigTable::TABLE_GUID); + assert_eq!(type_id, TypeId::of::()); + Ok(()) + }); + + mock.install_or_replace(&FAKE_CONFIG_TABLE).unwrap(); + } + + #[test] + fn test_config_table_table_len_defaults_to_size_of_self() { + assert_eq!(FAKE_CONFIG_TABLE.table_len(), core::mem::size_of::()); + } + + /// A self-describing header whose total size is reported by `table_len` and covers trailing + /// data past the header itself. + #[repr(C)] + struct HeaderWithTrailingData { + total_len: u32, + } + + impl ConfigTable for HeaderWithTrailingData { + const TABLE_GUID: BinaryGuid = + BinaryGuid::from_fields(0x2222_3333, 0x4444, 0x5555, 0x66, 0x77, &[8, 9, 10, 11, 12, 13]); + + fn table_len(&self) -> usize { + self.total_len as usize + } + } + + #[repr(C)] + struct HeaderWithTrailingDataBuffer { + header: HeaderWithTrailingData, + trailing: [u8; 4], + } + + static HEADER_WITH_TRAILING_DATA_BUFFER: HeaderWithTrailingDataBuffer = HeaderWithTrailingDataBuffer { + header: HeaderWithTrailingData { total_len: 8 }, + trailing: [0xaa, 0xbb, 0xcc, 0xdd], + }; + + #[test] + fn test_config_table_ext_get_bytes_returns_whole_table() { + let mut mock = MockConfigurationTableServices::new(); + mock.expect_get_typed_table().times(1).returning(|guid, type_id| { + assert_eq!(guid, HeaderWithTrailingData::TABLE_GUID); + assert_eq!(type_id, TypeId::of::()); + ConfigTablePtr::from_raw(&raw const HEADER_WITH_TRAILING_DATA_BUFFER as *mut c_void) + }); + + // SAFETY: `HEADER_WITH_TRAILING_DATA_BUFFER.header.total_len` (8) matches the number of + // bytes actually allocated for the buffer (a 4-byte header plus 4 trailing bytes). + let bytes = unsafe { mock.get_bytes::() }.unwrap(); + assert_eq!(bytes.len(), 8); + assert_eq!(&bytes[4..], &[0xaa, 0xbb, 0xcc, 0xdd]); + } + + #[test] + fn test_config_table_ext_get_bytes_returns_none_on_type_mismatch() { + let mut mock = MockConfigurationTableServices::new(); + mock.expect_get_typed_table().times(1).returning(|_, _| None); + + // SAFETY: no table is returned, so no memory is read. + assert!(unsafe { mock.get_bytes::() }.is_none()); + } +} diff --git a/sdk/patina/src/component/service/uefi_services/driver.rs b/sdk/patina/src/component/service/uefi_services/driver.rs new file mode 100644 index 000000000..363e06d2f --- /dev/null +++ b/sdk/patina/src/component/service/uefi_services/driver.rs @@ -0,0 +1,160 @@ +//! Driver connection services for Patina components. +//! +//! [`DriverServices`] exposes the UEFI driver model's connect and disconnect operations, allowing a +//! component to bind drivers to a controller handle or tear those bindings down. +//! +//! ## License +//! +//! Copyright (c) Microsoft Corporation. +//! +//! SPDX-License-Identifier: Apache-2.0 +//! + +use crate::base::error::EfiError; + +pub use super::handle::Handle; + +#[cfg(any(test, feature = "mockall"))] +use mockall::automock; + +/// Errors that can occur when using [`DriverServices`]. +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub enum DriverError { + /// A provided handle or parameter was invalid. + InvalidParameter, + /// The requested handle, driver, or child was not found. + NotFound, + /// No drivers could be connected to the controller. + Unsupported, + /// Access to the controller or one of its protocols was denied. + AccessDenied, + /// An unexpected internal error occurred. + Internal, +} + +impl From for EfiError { + fn from(value: DriverError) -> Self { + match value { + DriverError::InvalidParameter => EfiError::InvalidParameter, + DriverError::NotFound => EfiError::NotFound, + DriverError::Unsupported => EfiError::Unsupported, + DriverError::AccessDenied => EfiError::AccessDenied, + DriverError::Internal => EfiError::DeviceError, + } + } +} + +impl From for DriverError { + fn from(value: EfiError) -> Self { + match value { + EfiError::InvalidParameter => DriverError::InvalidParameter, + EfiError::NotFound => DriverError::NotFound, + EfiError::Unsupported => DriverError::Unsupported, + EfiError::AccessDenied => DriverError::AccessDenied, + _ => DriverError::Internal, + } + } +} + +/// Driver connection and disconnection services. +/// +/// This service is implemented by the Patina DXE Core. Components consume it by adding a +/// [`Service`](crate::component::service::Service) parameter to their entry +/// point. +/// +/// # Examples +/// +/// ```rust,no_run +/// use patina::component::service::{ +/// Service, +/// uefi_services::{ +/// driver::DriverServices, +/// protocol::{ProtocolServices, ProtocolServicesExt}, +/// }, +/// }; +/// use patina::error::Result; +/// use patina::standard::efi::protocols::block_io::Protocol as BlockIo; +/// +/// fn entry_point( +/// protocols: Service, +/// drivers: Service, +/// ) -> Result<()> { +/// // Bind drivers to every Block I/O controller, recursing into child controllers. +/// if let Ok(controllers) = protocols.locate_handles_for::() { +/// for controller in controllers { +/// let _ = drivers.connect_controller(controller, true); +/// } +/// } +/// Ok(()) +/// } +/// ``` +#[cfg_attr(any(test, feature = "mockall"), automock)] +pub trait DriverServices { + /// Connects one or more drivers to a controller handle. + /// + /// The platform's driver binding protocols are used to select and start the best matching + /// drivers. When `recursive` is `true`, the newly created child controllers are connected as + /// well. + /// + /// # Errors + /// + /// Returns [`DriverError::NotFound`] or [`DriverError::Unsupported`] if no driver could be + /// connected. + fn connect_controller(&self, controller: Handle, recursive: bool) -> Result<(), DriverError>; + + /// Disconnects drivers from a controller handle. + /// + /// If `driver` is `Some`, only that driver is disconnected; otherwise all drivers are. If + /// `child` is `Some`, only that child is destroyed; otherwise all children are. + /// + /// # Errors + /// + /// Returns [`DriverError::InvalidParameter`] if any handle is invalid, or + /// [`DriverError::NotFound`] if the driver is not managing the controller. + fn disconnect_controller( + &self, + controller: Handle, + driver: Option, + child: Option, + ) -> Result<(), DriverError>; +} + +#[cfg(test)] +mod tests { + use super::*; + use core::ffi::c_void; + use core::ptr::NonNull; + + fn dummy_handle() -> Handle { + Handle::from_raw(NonNull::::dangling().as_ptr()).unwrap() + } + + #[test] + fn test_driver_services_error_conversions() { + assert_eq!(EfiError::from(DriverError::InvalidParameter), EfiError::InvalidParameter); + assert_eq!(EfiError::from(DriverError::NotFound), EfiError::NotFound); + assert_eq!(EfiError::from(DriverError::Unsupported), EfiError::Unsupported); + assert_eq!(EfiError::from(DriverError::AccessDenied), EfiError::AccessDenied); + assert_eq!(EfiError::from(DriverError::Internal), EfiError::DeviceError); + assert_eq!(DriverError::from(EfiError::AccessDenied), DriverError::AccessDenied); + assert_eq!(DriverError::from(EfiError::OutOfResources), DriverError::Internal); + } + + #[test] + fn test_driver_services_mock_flow() { + let mut mock = MockDriverServices::new(); + mock.expect_connect_controller().times(1).returning(|_, recursive| { + assert!(recursive); + Ok(()) + }); + mock.expect_disconnect_controller().times(1).returning(|_, driver, child| { + assert!(driver.is_none()); + assert!(child.is_none()); + Err(DriverError::NotFound) + }); + + let handle = dummy_handle(); + assert!(mock.connect_controller(handle, true).is_ok()); + assert_eq!(mock.disconnect_controller(handle, None, None), Err(DriverError::NotFound)); + } +} diff --git a/sdk/patina/src/component/service/uefi_services/event.rs b/sdk/patina/src/component/service/uefi_services/event.rs new file mode 100644 index 000000000..6237b126a --- /dev/null +++ b/sdk/patina/src/component/service/uefi_services/event.rs @@ -0,0 +1,274 @@ +//! Event services for Patina components. +//! +//! [`EventServices`] exposes UEFI event operations as a Rust service. Notification callbacks are +//! supplied as Rust closures rather than C function pointers with an opaque context argument, and +//! events are represented by the opaque [`Event`] handle rather than a raw pointer. +//! +//! Timer events are handled separately by [`TimerEventServices`](super::timer_event::TimerEventServices), +//! since arming a timer depends on the Timer Architectural Protocol. +//! +//! ## License +//! +//! Copyright (c) Microsoft Corporation. +//! +//! SPDX-License-Identifier: Apache-2.0 +//! + +use alloc::boxed::Box; +use core::ffi::c_void; +use core::ptr::NonNull; + +use crate::base::error::EfiError; +use crate::base::guid::BinaryGuid; + +#[cfg(any(test, feature = "mockall"))] +use mockall::automock; + +/// A notification callback invoked when an event is signaled. +/// +/// The callback is supplied to [`EventServices::create_event`], +/// [`EventServices::create_event_for_group`], and +/// [`TimerEventServices::create_timer_event`](super::timer_event::TimerEventServices::create_timer_event), +/// and is owned by the event until the event is closed. +pub type EventNotifyCallback = Box; + +/// An opaque handle to a created event. +/// +/// A handle is returned by [`EventServices::create_event`] and +/// [`TimerEventServices::create_timer_event`](super::timer_event::TimerEventServices::create_timer_event) +/// and is passed back to the other service methods to refer to the event. The handle is a +/// copyable token; it does not own the event, and the event must be released with +/// [`EventServices::close_event`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Event(NonNull); + +impl Event { + /// Wraps a raw event handle produced by the service implementation. + /// + /// This is intended for use by service implementations, not component authors. + #[doc(hidden)] + pub fn from_raw(handle: *mut c_void) -> Option { + NonNull::new(handle).map(Self) + } + + /// Returns the raw event handle for use by the service implementation. + /// + /// This is intended for use by service implementations, not component authors. + #[doc(hidden)] + pub fn as_raw(&self) -> *mut c_void { + self.0.as_ptr() + } +} + +/// The task priority level (TPL) at which an event's notification runs. +pub use super::tpl::Tpl; + +/// Errors that can occur when using [`EventServices`]. +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub enum EventError { + /// A provided parameter (such as an unknown event handle) was invalid. + InvalidParameter, + /// The requested resource was not found. + NotFound, + /// An unexpected internal error occurred. + Internal, +} + +impl From for EfiError { + fn from(value: EventError) -> Self { + match value { + EventError::InvalidParameter => EfiError::InvalidParameter, + EventError::NotFound => EfiError::NotFound, + EventError::Internal => EfiError::Unsupported, + } + } +} + +impl From for EventError { + fn from(value: EfiError) -> Self { + match value { + EfiError::InvalidParameter => EventError::InvalidParameter, + EfiError::NotFound => EventError::NotFound, + _ => EventError::Internal, + } + } +} + +/// Event services. +/// +/// This trait is object-safe and its creation methods take a pre-boxed [`EventNotifyCallback`]. +/// Component authors should generally use the type-safe methods provided by [`EventServicesExt`] +/// instead of calling these methods directly. +/// +/// This service is implemented by the Patina DXE Core. Components consume it by adding a +/// [`Service`](crate::component::service::Service) parameter to their entry +/// point. +#[cfg_attr(any(test, feature = "mockall"), automock)] +pub trait EventServices { + /// Creates an event with a notification callback that runs when the event is signaled. + /// + /// The `callback` is invoked at `notify_tpl` each time the event is signaled. The callback is + /// owned by the event and is dropped when the event is closed with [`Self::close_event`]. + /// + /// # Errors + /// + /// Returns [`EventError::InvalidParameter`] if the event could not be created. + fn create_event(&self, notify_tpl: Tpl, callback: EventNotifyCallback) -> Result; + + /// Creates an event with a notification callback that runs whenever `group` is signaled. + /// + /// The `callback` runs at `notify_tpl` when any event that is a member of `group` (including + /// this one) is signaled. For example, [`crate::pi::event::END_OF_DXE_EVENT_GROUP_GUID`]. This + /// lets a component defer work until an event group fires, without taking a dispatch + /// dependency on whatever signals it. The callback is owned by the event and is dropped when + /// the event is closed with [`Self::close_event`]. + /// + /// # Errors + /// + /// Returns [`EventError::InvalidParameter`] if the event could not be created. + fn create_event_for_group( + &self, + group: BinaryGuid, + notify_tpl: Tpl, + callback: EventNotifyCallback, + ) -> Result; + + /// Signals an event, queuing its notification callback for dispatch. + /// + /// # Errors + /// + /// Returns [`EventError::InvalidParameter`] if `event` is not a valid event. + fn signal_event(&self, event: Event) -> Result<(), EventError>; + + /// Checks whether an event is in the signaled state, clearing it if so. + /// + /// Returns `true` if the event was signaled. The event must not be a notify-signal event. + /// + /// # Errors + /// + /// Returns [`EventError::InvalidParameter`] if `event` is not a valid non-signal event. + fn check_event(&self, event: Event) -> Result; + + /// Closes an event, releasing it and dropping its notification callback. + /// + /// # Errors + /// + /// Returns [`EventError::InvalidParameter`] if `event` is not a valid event. + fn close_event(&self, event: Event) -> Result<(), EventError>; +} + +/// Type-safe extension methods for [`EventServices`]. +/// +/// These methods accept a plain closure and box it internally, so callers never write +/// `Box::new` themselves. The trait is implemented for every [`EventServices`] implementor +/// (including [`Service`]). +/// +/// [`Service`]: crate::component::service::Service +/// +/// # Examples +/// +/// ```rust,no_run +/// use patina::component::service::{Service, uefi_services::event::{EventServices, EventServicesExt, Tpl}}; +/// use patina::error::Result; +/// +/// fn entry_point(events: Service) -> Result<()> { +/// let _event = events.on_event(Tpl::Callback, || { +/// log::info!("signaled"); +/// })?; +/// Ok(()) +/// } +/// ``` +pub trait EventServicesExt: EventServices { + /// Creates an event with a notification callback that runs when the event is signaled. + /// + /// Equivalent to [`EventServices::create_event`], but takes a plain closure instead of a + /// pre-boxed [`EventNotifyCallback`]. + /// + /// # Errors + /// + /// Returns [`EventError::InvalidParameter`] if the event could not be created. + fn on_event(&self, notify_tpl: Tpl, callback: impl FnMut() + 'static) -> Result { + self.create_event(notify_tpl, Box::new(callback)) + } + + /// Creates an event with a notification callback that runs whenever `group` is signaled. + /// + /// Equivalent to [`EventServices::create_event_for_group`], but takes a plain closure instead + /// of a pre-boxed [`EventNotifyCallback`]. + /// + /// # Errors + /// + /// Returns [`EventError::InvalidParameter`] if the event could not be created. + fn on_event_group( + &self, + group: BinaryGuid, + notify_tpl: Tpl, + callback: impl FnMut() + 'static, + ) -> Result { + self.create_event_for_group(group, notify_tpl, Box::new(callback)) + } +} + +impl EventServicesExt for T {} + +#[cfg(test)] +mod tests { + use super::*; + + fn dummy_event() -> Event { + Event::from_raw(NonNull::::dangling().as_ptr()).unwrap() + } + + #[test] + fn test_event_services_error_to_efi() { + assert_eq!(EfiError::from(EventError::InvalidParameter), EfiError::InvalidParameter); + assert_eq!(EfiError::from(EventError::NotFound), EfiError::NotFound); + assert_eq!(EfiError::from(EventError::Internal), EfiError::Unsupported); + } + + #[test] + fn test_event_services_error_from_efi() { + assert_eq!(EventError::from(EfiError::InvalidParameter), EventError::InvalidParameter); + assert_eq!(EventError::from(EfiError::NotFound), EventError::NotFound); + assert_eq!(EventError::from(EfiError::DeviceError), EventError::Internal); + } + + #[test] + fn test_event_services_handle_dummy() { + assert!(Event::from_raw(core::ptr::null_mut()).is_none()); + let event = dummy_event(); + assert_eq!(event.as_raw(), NonNull::::dangling().as_ptr()); + } + + #[test] + fn test_event_services_mock_event_group_flow() { + let mut mock = MockEventServices::new(); + mock.expect_create_event_for_group() + .times(1) + .returning(|_, _, _| Ok(Event::from_raw(NonNull::::dangling().as_ptr()).unwrap())); + mock.expect_close_event().times(1).returning(|_| Ok(())); + + let event = mock.create_event_for_group(BinaryGuid::ZERO, Tpl::Callback, Box::new(|| {})).unwrap(); + assert!(mock.close_event(event).is_ok()); + } + + #[test] + fn test_event_services_ext_on_event() { + let mut mock = MockEventServices::new(); + mock.expect_create_event() + .times(1) + .returning(|_, _| Ok(Event::from_raw(NonNull::::dangling().as_ptr()).unwrap())); + + assert!(mock.on_event(Tpl::Callback, || {}).is_ok()); + } + + #[test] + fn test_event_services_ext_on_event_group() { + let mut mock = MockEventServices::new(); + mock.expect_create_event_for_group() + .times(1) + .returning(|_, _, _| Ok(Event::from_raw(NonNull::::dangling().as_ptr()).unwrap())); + + assert!(mock.on_event_group(BinaryGuid::ZERO, Tpl::Callback, || {}).is_ok()); + } +} diff --git a/sdk/patina/src/component/service/uefi_services/handle.rs b/sdk/patina/src/component/service/uefi_services/handle.rs new file mode 100644 index 000000000..f088c6bca --- /dev/null +++ b/sdk/patina/src/component/service/uefi_services/handle.rs @@ -0,0 +1,40 @@ +//! Shared opaque handle type for the Patina UEFI Services. +//! +//! ## License +//! +//! Copyright (c) Microsoft Corporation. +//! +//! SPDX-License-Identifier: Apache-2.0 +//! + +use core::ffi::c_void; +use core::ptr::NonNull; + +use crate::standard::efi; + +/// An opaque handle to an object in the UEFI handle database (a device, driver, or image). +/// +/// A handle is a copyable token. Components obtain handles from services (for example +/// [`ProtocolServices`](super::protocol::ProtocolServices)) and pass them back to other service +/// methods. They never construct or dereference the underlying pointer. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Handle(NonNull); + +impl Handle { + /// Wraps a raw handle produced by the service implementation. + /// + /// This is intended for use by service implementations, not component authors. + #[doc(hidden)] + pub fn from_raw(handle: efi::Handle) -> Option { + NonNull::new(handle).map(Self) + } + + /// Returns the raw handle for use by the service implementation. + /// + /// This is intended for use by service implementations, not component authors. + #[doc(hidden)] + #[cfg_attr(coverage, coverage(off))] + pub fn as_raw(&self) -> efi::Handle { + self.0.as_ptr() + } +} diff --git a/sdk/patina/src/component/service/uefi_services/image.rs b/sdk/patina/src/component/service/uefi_services/image.rs new file mode 100644 index 000000000..c3a0545e4 --- /dev/null +++ b/sdk/patina/src/component/service/uefi_services/image.rs @@ -0,0 +1,210 @@ +//! Image services for Patina components. +//! +//! [`ImageServices`] exposes the UEFI image services such as loading, starting, and unloading UEFI +//! images. Images are referred to by the opaque [`Handle`] token, and image contents are supplied as +//! a byte slice rather than a raw pointer. +//! +//! Images can be loaded either from an in-memory buffer ([`ImageServices::load_image`]) or, when the +//! `unstable-device-path` feature is enabled, by device path. +//! +//! ## License +//! +//! Copyright (c) Microsoft Corporation. +//! +//! SPDX-License-Identifier: Apache-2.0 +//! + +use crate::base::error::EfiError; +#[cfg(feature = "unstable-device-path")] +use crate::uefi::device_path::paths::DevicePath; + +pub use super::handle::Handle; + +#[cfg(any(test, feature = "mockall"))] +use mockall::automock; + +/// Errors that can occur when using [`ImageServices`]. +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub enum ImageError { + /// A provided handle or parameter was invalid. + InvalidParameter, + /// The image (or a resource it requires) was not found. + NotFound, + /// The image failed to load. + LoadError, + /// The image loaded but failed authentication. + SecurityViolation, + /// The image was not loaded or started due to platform policy. + AccessDenied, + /// The operation is not supported. + Unsupported, + /// An unexpected internal error occurred. + Internal, +} + +impl From for EfiError { + fn from(value: ImageError) -> Self { + match value { + ImageError::InvalidParameter => EfiError::InvalidParameter, + ImageError::NotFound => EfiError::NotFound, + ImageError::LoadError => EfiError::LoadError, + ImageError::SecurityViolation => EfiError::SecurityViolation, + ImageError::AccessDenied => EfiError::AccessDenied, + ImageError::Unsupported => EfiError::Unsupported, + ImageError::Internal => EfiError::DeviceError, + } + } +} + +impl From for ImageError { + fn from(value: EfiError) -> Self { + match value { + EfiError::InvalidParameter => ImageError::InvalidParameter, + EfiError::NotFound => ImageError::NotFound, + EfiError::LoadError => ImageError::LoadError, + EfiError::SecurityViolation => ImageError::SecurityViolation, + EfiError::AccessDenied => ImageError::AccessDenied, + EfiError::Unsupported => ImageError::Unsupported, + _ => ImageError::Internal, + } + } +} + +/// UEFI image services: load, start, and unload images. +/// +/// This service is implemented by the Patina DXE Core. Components consume it by adding a +/// [`Service`](crate::component::service::Service) parameter to their entry +/// point. +/// +/// # Examples +/// +/// ```rust,no_run +/// use patina::component::service::{Service, uefi_services::image::{ImageServices, Handle}}; +/// use patina::error::Result; +/// +/// fn entry_point(images: Service, parent: Handle, pe_image: &[u8]) -> Result<()> { +/// let image = images.load_image(parent, pe_image)?; +/// images.start_image(image)?; +/// Ok(()) +/// } +/// ``` +#[cfg_attr(any(test, feature = "mockall"), automock)] +pub trait ImageServices { + /// Loads a UEFI image from an in-memory buffer. + /// + /// `parent` must be a valid image handle (for example, an image handle the caller already + /// holds). The loaded image's handle is returned. It can then be started with + /// [`Self::start_image`]. + /// + /// This is a simpler alternative to `load_image_from_device_path` when the image is + /// already available in memory that sets `boot_policy` to `false`. If a device path needs + /// to be specified or `boot_policy` needs to be `true`, use `load_image_from_device_path` + /// instead. `load_image_from_device_path` is only available when the `unstable-device-path` + /// feature is enabled. + /// + /// # Errors + /// + /// Returns [`ImageError::LoadError`] if the image could not be loaded, + /// [`ImageError::SecurityViolation`] if it failed authentication, or + /// [`ImageError::AccessDenied`] if platform policy prevented loading. + fn load_image(&self, parent: Handle, source: &[u8]) -> Result; + + /// Starts a previously loaded image, transferring control to its entry point. + /// + /// # Errors + /// + /// Returns [`ImageError::InvalidParameter`] if `image` is not a loaded, not started image. + fn start_image(&self, image: Handle) -> Result<(), ImageError>; + + /// Unloads a previously loaded image. + /// + /// # Errors + /// + /// Returns [`ImageError::InvalidParameter`] if `image` is not a valid image handle. + fn unload_image(&self, image: Handle) -> Result<(), ImageError>; + + /// Loads a UEFI image located by a device path (for example, a file on a file system or a + /// `LoadFile`/`LoadFile2` provider). + /// + /// `parent` must be a valid image handle. When `boot_policy` is `true`, the request is treated + /// as originating from the boot manager (matching the UEFI `LoadImage` `BootPolicy` parameter); + /// most callers pass `false`. + /// + /// This method is available only when the `unstable-device-path` feature is enabled, as it + /// depends on the SDK's unstable device path API. + /// + /// # Errors + /// + /// Returns [`ImageError::NotFound`] if no provider could produce the image for the given device + /// path, or the other [`ImageError`] variants as for [`Self::load_image`]. + #[cfg(feature = "unstable-device-path")] + fn load_image_from_device_path( + &self, + parent: Handle, + device_path: &DevicePath, + boot_policy: bool, + ) -> Result; +} + +#[cfg(test)] +mod tests { + use super::*; + use core::ffi::c_void; + use core::ptr::NonNull; + + fn dummy_handle() -> Handle { + Handle::from_raw(NonNull::::dangling().as_ptr()).unwrap() + } + + #[test] + fn test_image_services_error_to_efi() { + assert_eq!(EfiError::from(ImageError::LoadError), EfiError::LoadError); + assert_eq!(EfiError::from(ImageError::SecurityViolation), EfiError::SecurityViolation); + assert_eq!(EfiError::from(ImageError::AccessDenied), EfiError::AccessDenied); + assert_eq!(EfiError::from(ImageError::Internal), EfiError::DeviceError); + } + + #[test] + fn test_image_services_error_from_efi() { + assert_eq!(ImageError::from(EfiError::LoadError), ImageError::LoadError); + assert_eq!(ImageError::from(EfiError::SecurityViolation), ImageError::SecurityViolation); + assert_eq!(ImageError::from(EfiError::NotFound), ImageError::NotFound); + assert_eq!(ImageError::from(EfiError::DeviceError), ImageError::Internal); + } + + #[test] + fn test_image_services_mock_flow() { + let mut mock = MockImageServices::new(); + mock.expect_load_image().times(1).returning(|_, source| { + assert_eq!(source, b"pe"); + Ok(Handle::from_raw(NonNull::::dangling().as_ptr()).unwrap()) + }); + mock.expect_start_image().times(1).returning(|_| Ok(())); + mock.expect_unload_image().times(1).returning(|_| Err(ImageError::InvalidParameter)); + + let parent = dummy_handle(); + let image = mock.load_image(parent, b"pe").unwrap(); + assert!(mock.start_image(image).is_ok()); + assert_eq!(mock.unload_image(image), Err(ImageError::InvalidParameter)); + } + + #[cfg(feature = "unstable-device-path")] + #[test] + fn test_image_services_mock_load_from_device_path() { + use crate::uefi::device_path::paths::DevicePath; + + // Minimal valid device path: a single End-of-Entire node (type 0x7F, sub-type 0xFF, len 4). + let bytes = [0x7Fu8, 0xFF, 0x04, 0x00]; + // SAFETY: `bytes` is a valid, well-formed device path (a single End-of-Entire node) that + // outlives the returned reference. + let device_path = unsafe { DevicePath::try_from_ptr(bytes.as_ptr()) }.unwrap(); + + let mut mock = MockImageServices::new(); + mock.expect_load_image_from_device_path().times(1).returning(|_, _, boot_policy| { + assert!(!boot_policy); + Ok(Handle::from_raw(NonNull::::dangling().as_ptr()).unwrap()) + }); + + assert!(mock.load_image_from_device_path(dummy_handle(), device_path, false).is_ok()); + } +} diff --git a/sdk/patina/src/component/service/uefi_services/protocol.rs b/sdk/patina/src/component/service/uefi_services/protocol.rs new file mode 100644 index 000000000..677a93952 --- /dev/null +++ b/sdk/patina/src/component/service/uefi_services/protocol.rs @@ -0,0 +1,709 @@ +//! Protocol services for Patina components. +//! +//! [`ProtocolServices`] exposes UEFI protocol installation and discovery to components. The trait +//! itself is object-safe (and mockable) and works with the opaque [`Handle`] and [`ProtocolPtr`] +//! tokens rather than raw pointers. Type-safe access is provided by the [`ProtocolServicesExt`] +//! extension trait, whose generic methods bind a protocol interface type to its GUID using +//! [`ProtocolInterface`], so components can install and locate protocols without handling a +//! raw pointer or GUID directly. +//! +//! Most components should prefer consuming higher-level Patina services over using protocols +//! directly. This service exists for the cases where a component must publish or consume a +//! protocol. For example, when providing or using code with C drivers that use protocols. +//! +//! # Consuming a protocol over time +//! +//! A UEFI protocol can be installed and uninstalled at any point, so a plain `&P` handed out once +//! can dangle later. [`ProtocolServicesExt`] offers four access styles, focusing on different +//! use cases: +//! +//! - [`with_protocol`](ProtocolServicesExt::with_protocol) - Run a closure with the interface. +//! Useful for a single, immediate use. The reference cannot escape the closure. +//! - [`open_protocol`](ProtocolServicesExt::open_protocol) - A [`ProtocolGuard`] that dereferences +//! to the interface for a block scope. Useful when several statements need the interface. +//! - [`locate_token`](ProtocolServicesExt::locate_token) - A [`ProtocolToken`] that stores +//! only a handle and never dangles. Useful for keeping a reference for the rest of boot. +//! Re-validate each use with [`resolve`](ProtocolServicesExt::resolve). +//! - [`on_protocol_installed`](ProtocolServicesExt::on_protocol_installed) - A callback that runs +//! for each present and future install of the protocol. Useful when another component installs the +//! protocol later. +//! +//! ## License +//! +//! Copyright (c) Microsoft Corporation. +//! +//! SPDX-License-Identifier: Apache-2.0 +//! + +use alloc::boxed::Box; +use alloc::vec::Vec; +use core::ffi::c_void; +use core::marker::PhantomData; +use core::ptr::NonNull; + +use crate::base::error::EfiError; +use crate::base::guid::BinaryGuid; +use crate::base::protocol::ProtocolInterface; + +pub use super::handle::Handle; +pub use super::tpl::Tpl; + +#[cfg(any(test, feature = "mockall"))] +use mockall::automock; + +/// An opaque pointer to a protocol interface. +/// +/// This token is returned by the erased [`ProtocolServices::locate_interface`] method and consumed +/// by [`ProtocolServices::install_interface`]. Components should generally use the typed methods on +/// [`ProtocolServicesExt`] rather than handling this token directly. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ProtocolPtr(NonNull); + +impl ProtocolPtr { + /// Wraps a raw interface pointer. + /// + /// This is intended for use by service implementations and the typed extension methods, not + /// component authors. + #[doc(hidden)] + pub fn from_raw(interface: *mut c_void) -> Option { + NonNull::new(interface).map(Self) + } + + /// Returns the raw interface pointer. + /// + /// This is intended for use by service implementations and the typed extension methods, not + /// component authors. + #[doc(hidden)] + pub fn as_raw(&self) -> *mut c_void { + self.0.as_ptr() + } +} + +/// A callback invoked with the handle of each interface installed for a watched protocol. +/// +/// Supplied to [`ProtocolServices::register_install_notify`] (usually using +/// [`ProtocolServicesExt::on_protocol_installed`]). It runs for handles already present when the +/// notification is registered and for every future install, until the registration is cancelled. +pub type NotifyCallback = Box; + +/// An opaque token for an active protocol-installation notification. +/// +/// Returned by [`ProtocolServicesExt::on_protocol_installed`]. Keep it for as long as the +/// notification should stay active, then pass it to [`ProtocolServicesExt::cancel`] to stop it. +/// Dropping the token does not cancel the notification. The registration is intentionally long-lived +/// so it can outlive the component that created it. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct NotifyRegistration { + event: usize, + registration: usize, + context: usize, +} + +impl NotifyRegistration { + /// Wraps the raw pieces of a registration produced by the service implementation. + /// + /// This is intended for use by service implementations, not component authors. + #[cfg(any(test, feature = "core"))] + #[doc(hidden)] + pub fn from_raw(event: *mut c_void, registration: *mut c_void, context: *mut c_void) -> Self { + Self { event: event as usize, registration: registration as usize, context: context as usize } + } + + /// Returns the raw notification event handle. + #[cfg(any(test, feature = "core"))] + #[doc(hidden)] + pub fn event(&self) -> *mut c_void { + self.event as *mut c_void + } + + /// Returns the raw registration key. + #[cfg(any(test, feature = "core"))] + #[doc(hidden)] + pub fn registration(&self) -> *mut c_void { + self.registration as *mut c_void + } + + /// Returns the raw notification context pointer. + #[cfg(any(test, feature = "core"))] + #[doc(hidden)] + pub fn context(&self) -> *mut c_void { + self.context as *mut c_void + } +} + +/// A view of a protocol interface on a specific handle. +/// +/// Returned by [`ProtocolServicesExt::open_protocol`]. It dereferences to the protocol interface +/// and ties that reference to the borrow of the service, so the reference cannot escape the scope +/// in which access is held. For access that must persist across dispatch, store a [`ProtocolToken`] +/// instead. +#[must_use] +pub struct ProtocolGuard<'a, P: ProtocolInterface> { + interface: &'a P, +} + +impl core::ops::Deref for ProtocolGuard<'_, P> { + type Target = P; + + fn deref(&self) -> &P { + self.interface + } +} + +/// A revocable reference to a protocol interface on a specific handle. +/// +/// Unlike a `&P`, a token never dangles. It only stores a handle, so it is safe to keep for the +/// whole boot. Each use is re-validated through [`ProtocolServicesExt::resolve`], which returns +/// `None` if the interface is no longer installed on the handle. +/// +/// The token identifies a specific handle. It does not detect handle reuse. For example, if the +/// DXE Core were to recycle a handle value for a different object that also installs `P`, a stale +/// token could resolve to that object. Since the Patina DXE Core does not recycle handle values, +/// this is not expected to occur in practice. +#[derive(Debug)] +pub struct ProtocolToken { + handle: Handle, + _marker: PhantomData P>, +} + +impl ProtocolToken

{ + /// Creates a token referring to `handle`. + /// + /// This is intended for use by the typed extension methods. Component authors obtain a token + /// from [`ProtocolServicesExt::locate_token`]. + #[doc(hidden)] + pub fn new(handle: Handle) -> Self { + Self { handle, _marker: PhantomData } + } + + /// Returns the handle this token refers to. + pub fn handle(&self) -> Handle { + self.handle + } +} + +impl Clone for ProtocolToken

{ + fn clone(&self) -> Self { + *self + } +} + +impl Copy for ProtocolToken

{} + +/// Errors that can occur when using [`ProtocolServices`]. +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub enum ProtocolError { + /// A provided parameter was invalid. + InvalidParameter, + /// The requested protocol or handle was not found. + NotFound, + /// The system is out of resources to complete the operation. + OutOfResources, + /// An unexpected internal error occurred. + Internal, +} + +impl From for EfiError { + fn from(value: ProtocolError) -> Self { + match value { + ProtocolError::InvalidParameter => EfiError::InvalidParameter, + ProtocolError::NotFound => EfiError::NotFound, + ProtocolError::OutOfResources => EfiError::OutOfResources, + ProtocolError::Internal => EfiError::Unsupported, + } + } +} + +impl From for ProtocolError { + fn from(value: EfiError) -> Self { + match value { + EfiError::InvalidParameter => ProtocolError::InvalidParameter, + EfiError::NotFound => ProtocolError::NotFound, + EfiError::OutOfResources => ProtocolError::OutOfResources, + _ => ProtocolError::Internal, + } + } +} + +/// Protocol installation and discovery services. +/// +/// This trait is object-safe and works with opaque tokens. Component authors should generally use +/// the type-safe methods provided by [`ProtocolServicesExt`] instead of calling these methods +/// directly. +/// +/// This service is implemented by the Patina DXE Core. Components consume it by adding a +/// [`Service`](crate::component::service::Service) parameter to their entry +/// point. +#[cfg_attr(any(test, feature = "mockall"), automock)] +pub trait ProtocolServices { + /// Installs a protocol interface identified by `protocol` on a handle. + /// + /// If `handle` is `None`, a new handle is created. The (possibly new) handle is returned. + /// + /// # Errors + /// + /// Returns [`ProtocolError::InvalidParameter`] if the interface could not be installed. + fn install_interface( + &self, + handle: Option, + protocol: BinaryGuid, + interface: ProtocolPtr, + ) -> Result; + + /// Uninstalls a protocol interface identified by `protocol` from `handle`. + /// + /// # Errors + /// + /// Returns [`ProtocolError::NotFound`] if the handle/protocol/interface is not present. + fn uninstall_interface( + &self, + handle: Handle, + protocol: BinaryGuid, + interface: ProtocolPtr, + ) -> Result<(), ProtocolError>; + + /// Locates the first interface installed for `protocol`, from any handle. + /// + /// # Errors + /// + /// Returns [`ProtocolError::NotFound`] if no matching interface is installed. + fn locate_interface(&self, protocol: BinaryGuid) -> Result; + + /// Returns all handles that have `protocol` installed. + /// + /// Returns an empty list if no handle has the protocol installed. + fn locate_handles(&self, protocol: BinaryGuid) -> Result, ProtocolError>; + + /// Returns the interface for `protocol` installed on a specific `handle`. + /// + /// # Errors + /// + /// Returns [`ProtocolError::NotFound`] if `handle` does not have `protocol` installed. + fn interface_on_handle(&self, handle: Handle, protocol: BinaryGuid) -> Result; + + /// Registers `callback` to run for each handle on which `protocol` is installed. + /// + /// The callback runs once for each handle that already has the protocol installed, and again + /// for every future install, until the returned [`NotifyRegistration`] is cancelled with + /// [`Self::cancel_install_notify`]. The callback runs at `notify_tpl`. + /// + /// # Errors + /// + /// Returns [`ProtocolError::OutOfResources`] if the notification could not be registered. + fn register_install_notify( + &self, + protocol: BinaryGuid, + notify_tpl: Tpl, + callback: NotifyCallback, + ) -> Result; + + /// Cancels a notification previously registered with [`Self::register_install_notify`]. + /// + /// # Errors + /// + /// Returns [`ProtocolError::InvalidParameter`] if `registration` is not an active registration. + fn cancel_install_notify(&self, registration: NotifyRegistration) -> Result<(), ProtocolError>; +} + +// `Sealed` is inside the private `sealed` module so that it is `pub` (can be a supertrait of `IntoStaticInterface`) +// but it cannot be implemented itself outside the module (https://rust-lang.github.io/api-guidelines/future-proofing.html). +mod sealed { + //! Restricts [`IntoStaticInterface`] to the two forms `install_protocol` supports. + pub trait Sealed {} + impl

Sealed for &'static P {} + impl

Sealed for &'static mut P {} + impl

Sealed for alloc::boxed::Box

{} +} + +/// A value that [`ProtocolServicesExt::install_protocol`] can commit as a permanent interface. +/// +/// Implemented for `&'static P` (and `&'static mut P`), which install at no extra cost since they +/// are already permanent, and for `Box

`, which is only leaked once installation actually +/// succeeds. A failed install drops the box instead of leaking it. +pub trait IntoStaticInterface

: sealed::Sealed { + /// Returns a pointer to the interface without giving up ownership yet. + #[doc(hidden)] + fn as_interface_ptr(&self) -> *const P; + + /// Called once installation succeeds, committing the interface as permanently `'static`. + #[doc(hidden)] + fn commit(self); +} + +impl

IntoStaticInterface

for &'static P { + fn as_interface_ptr(&self) -> *const P { + *self + } + + fn commit(self) {} +} + +impl

IntoStaticInterface

for &'static mut P { + fn as_interface_ptr(&self) -> *const P { + &raw const **self + } + + fn commit(self) {} +} + +impl

IntoStaticInterface

for Box

{ + fn as_interface_ptr(&self) -> *const P { + &raw const **self + } + + fn commit(self) { + Box::leak(self); + } +} + +/// Type-safe extension methods for [`ProtocolServices`]. +/// +/// These generic methods bind a protocol interface type `P` to its GUID with [`ProtocolInterface`], +/// so callers do not need to handle a raw pointer or GUID. The trait is implemented for every +/// [`ProtocolServices`] implementor (including [`Service`]). +/// +/// [`Service`]: crate::component::service::Service +/// +/// # Examples +/// +/// ```rust,no_run +/// use patina::component::service::{Service, uefi_services::protocol::{ProtocolServices, ProtocolServicesExt}}; +/// use patina::error::Result; +/// use patina::standard::efi::protocols::graphics_output::Protocol as GraphicsOutput; +/// +/// fn entry_point(protocols: Service) -> Result<()> { +/// if let Ok(gop) = protocols.locate_protocol::() { +/// let mode = gop.mode; +/// // ... use the protocol ... +/// } +/// Ok(()) +/// } +/// ``` +pub trait ProtocolServicesExt: ProtocolServices { + /// Locates the first interface for protocol `P` and returns a typed reference to it. + /// + /// # Errors + /// + /// Returns [`ProtocolError::NotFound`] if no interface for `P` is installed. + fn locate_protocol(&self) -> Result<&'static P, ProtocolError> { + let ptr = self.locate_interface(P::PROTOCOL_GUID)?; + // SAFETY: `ProtocolInterface` is an `unsafe trait` whose contract guarantees that the + // interface installed for `P::PROTOCOL_GUID` has the memory layout of `P`. The core + // returns a non-null pointer to that interface. + Ok(unsafe { &*(ptr.as_raw() as *const P) }) + } + + /// Installs `interface` for protocol `P` on a handle, creating a new handle if `handle` is + /// `None`. + /// + /// Accepts a `&'static P`, which installs directly, or a `Box

`, which is only leaked once + /// installation succeeds, so a failed install does not leak memory. + /// + /// # Errors + /// + /// Returns [`ProtocolError::InvalidParameter`] if the interface could not be installed. + fn install_protocol( + &self, + handle: Option, + interface: impl IntoStaticInterface

, + ) -> Result { + let ptr = ProtocolPtr::from_raw(interface.as_interface_ptr() as *mut c_void) + .ok_or(ProtocolError::InvalidParameter)?; + let handle = self.install_interface(handle, P::PROTOCOL_GUID, ptr)?; + interface.commit(); + Ok(handle) + } + + /// Returns all handles that have protocol `P` installed. + /// + /// # Errors + /// + /// Returns [`ProtocolError::NotFound`] if no handle has protocol `P` installed. + fn locate_handles_for(&self) -> Result, ProtocolError> { + self.locate_handles(P::PROTOCOL_GUID) + } + + /// Runs `f` with the first installed interface for protocol `P`. + /// + /// Use this for a single, immediate use of a protocol. The reference passed to `f` cannot + /// escape the closure, so it cannot dangle. + /// + /// # Errors + /// + /// Returns [`ProtocolError::NotFound`] if no interface for `P` is installed. + fn with_protocol(&self, f: impl FnOnce(&P) -> R) -> Result { + let ptr = self.locate_interface(P::PROTOCOL_GUID)?; + // SAFETY: `ProtocolInterface` guarantees the interface installed for `P::PROTOCOL_GUID` has + // the layout of `P`. The pointer is non-null and valid for the duration of the call. + let interface = unsafe { &*(ptr.as_raw() as *const P) }; + Ok(f(interface)) + } + + /// Runs `f` with the interface for protocol `P` installed on a specific `handle`. + /// + /// # Errors + /// + /// Returns [`ProtocolError::NotFound`] if `handle` does not have `P` installed. + fn with_protocol_on( + &self, + handle: Handle, + f: impl FnOnce(&P) -> R, + ) -> Result { + let ptr = self.interface_on_handle(handle, P::PROTOCOL_GUID)?; + // SAFETY: as in `with_protocol`. + let interface = unsafe { &*(ptr.as_raw() as *const P) }; + Ok(f(interface)) + } + + /// Opens protocol `P` on `handle`, returning a [`ProtocolGuard`] for block-scoped access. + /// + /// The guard dereferences to the interface and keeps the reference tied to this service borrow. + /// Use it when several statements need the interface. For one expression prefer [`Self::with_protocol_on`]. + /// + /// # Errors + /// + /// Returns [`ProtocolError::NotFound`] if `handle` does not have `P` installed. + fn open_protocol(&self, handle: Handle) -> Result, ProtocolError> { + let ptr = self.interface_on_handle(handle, P::PROTOCOL_GUID)?; + // SAFETY: as in `with_protocol`. The reference is bound to `&self`, so it cannot outlive + // the borrow of the service. + let interface = unsafe { &*(ptr.as_raw() as *const P) }; + Ok(ProtocolGuard { interface }) + } + + /// Locates the first handle with protocol `P` and returns a [`ProtocolToken`]. + /// + /// The token can be stored and re-validated later with [`Self::resolve`]. + /// + /// # Errors + /// + /// Returns [`ProtocolError::NotFound`] if no handle has `P` installed. + fn locate_token(&self) -> Result, ProtocolError> { + let handle = self.locate_handles(P::PROTOCOL_GUID)?.into_iter().next().ok_or(ProtocolError::NotFound)?; + Ok(ProtocolToken::new(handle)) + } + + /// Resolves `token`, returning the interface if `P` is still installed on the token's handle. + /// + /// Returns `None` if the interface has since been uninstalled. Re-validating on each use is + /// what makes a token safe to hold across time. + fn resolve(&self, token: &ProtocolToken

) -> Option<&P> { + let ptr = self.interface_on_handle(token.handle(), P::PROTOCOL_GUID).ok()?; + // SAFETY: as in `with_protocol`. The reference is bound to `&self`. + Some(unsafe { &*(ptr.as_raw() as *const P) }) + } + + /// Registers `callback` to run for each present and future install of protocol `P`. + /// + /// Store the returned [`NotifyRegistration`] for as long as the notification should stay active + /// (for example in a service), and cancel it with [`Self::cancel`] when done. Dropping the + /// registration does not cancel it. The callback runs at `notify_tpl`. + /// + /// # Errors + /// + /// Returns [`ProtocolError::OutOfResources`] if the notification could not be registered. + fn on_protocol_installed( + &self, + notify_tpl: Tpl, + callback: impl FnMut(Handle) + 'static, + ) -> Result { + self.register_install_notify(P::PROTOCOL_GUID, notify_tpl, Box::new(callback)) + } + + /// Cancels a notification returned by [`Self::on_protocol_installed`]. + /// + /// # Errors + /// + /// Returns [`ProtocolError::InvalidParameter`] if `registration` is not active. + fn cancel(&self, registration: NotifyRegistration) -> Result<(), ProtocolError> { + self.cancel_install_notify(registration) + } +} + +impl ProtocolServicesExt for T {} + +#[cfg(test)] +mod tests { + use super::*; + + #[repr(C)] + struct FakeProtocol { + value: u32, + } + + // SAFETY: This test-only type declares a fixed GUID that is used consistently for both the + // (mocked) install and locate paths, so the GUID to layout binding holds within the test. + unsafe impl ProtocolInterface for FakeProtocol { + const PROTOCOL_GUID: BinaryGuid = BinaryGuid::from_string("abcdabcd-1234-5678-9abc-def012345678"); + } + + static FAKE_INSTANCE: FakeProtocol = FakeProtocol { value: 42 }; + + #[test] + fn test_protocol_services_error_to_efi() { + assert_eq!(EfiError::from(ProtocolError::InvalidParameter), EfiError::InvalidParameter); + assert_eq!(EfiError::from(ProtocolError::NotFound), EfiError::NotFound); + assert_eq!(EfiError::from(ProtocolError::OutOfResources), EfiError::OutOfResources); + assert_eq!(EfiError::from(ProtocolError::Internal), EfiError::Unsupported); + } + + #[test] + fn test_protocol_services_error_from_efi() { + assert_eq!(ProtocolError::from(EfiError::NotFound), ProtocolError::NotFound); + assert_eq!(ProtocolError::from(EfiError::OutOfResources), ProtocolError::OutOfResources); + assert_eq!(ProtocolError::from(EfiError::DeviceError), ProtocolError::Internal); + } + + #[test] + fn test_protocol_services_handle_null() { + assert!(Handle::from_raw(core::ptr::null_mut()).is_none()); + assert!(ProtocolPtr::from_raw(core::ptr::null_mut()).is_none()); + } + + #[test] + fn test_protocol_services_ext_locate_protocol() { + let mut mock = MockProtocolServices::new(); + mock.expect_locate_interface().times(1).returning(|guid| { + assert_eq!(guid, FakeProtocol::PROTOCOL_GUID); + Ok(ProtocolPtr::from_raw(&raw const FAKE_INSTANCE as *mut c_void).unwrap()) + }); + + let located = mock.locate_protocol::().unwrap(); + assert_eq!(located.value, 42); + } + + #[test] + fn test_protocol_services_ext_locate_handles_for() { + let mut mock = MockProtocolServices::new(); + mock.expect_locate_handles().times(1).returning(|guid| { + assert_eq!(guid, FakeProtocol::PROTOCOL_GUID); + Ok(Vec::new()) + }); + + assert!(mock.locate_handles_for::().unwrap().is_empty()); + } + + fn fake_handle() -> Handle { + Handle::from_raw(NonNull::::dangling().as_ptr()).unwrap() + } + + fn fake_ptr() -> ProtocolPtr { + ProtocolPtr::from_raw(&raw const FAKE_INSTANCE as *mut c_void).unwrap() + } + + #[test] + fn test_protocol_services_ext_install_protocol_static_ref() { + let mut mock = MockProtocolServices::new(); + mock.expect_install_interface().times(1).returning(|handle, guid, _| { + assert!(handle.is_none()); + assert_eq!(guid, FakeProtocol::PROTOCOL_GUID); + Ok(fake_handle()) + }); + + let handle = mock.install_protocol(None, &FAKE_INSTANCE).unwrap(); + assert_eq!(handle, fake_handle()); + } + + // A protocol type that records whether it was dropped, so tests can tell a leak apart from a drop. + #[repr(C)] + struct TrackedProtocol { + _value: u32, + dropped: alloc::sync::Arc, + } + + impl Drop for TrackedProtocol { + fn drop(&mut self) { + self.dropped.store(true, core::sync::atomic::Ordering::SeqCst); + } + } + + // SAFETY: This test-only type is only ever installed and located under its own GUID. + unsafe impl ProtocolInterface for TrackedProtocol { + const PROTOCOL_GUID: BinaryGuid = BinaryGuid::from_string("11111111-2222-3333-4444-555555555555"); + } + + #[test] + fn test_protocol_services_ext_install_protocol_boxed_leaks_on_success() { + let dropped = alloc::sync::Arc::new(core::sync::atomic::AtomicBool::new(false)); + let protocol = Box::new(TrackedProtocol { _value: 7, dropped: dropped.clone() }); + + let mut mock = MockProtocolServices::new(); + mock.expect_install_interface().times(1).returning(|_, guid, _| { + assert_eq!(guid, TrackedProtocol::PROTOCOL_GUID); + Ok(fake_handle()) + }); + + let handle = mock.install_protocol(None, protocol).unwrap(); + assert_eq!(handle, fake_handle()); + assert!(!dropped.load(core::sync::atomic::Ordering::SeqCst), "a successful install must leak, not drop"); + } + + #[test] + fn test_protocol_services_ext_install_protocol_boxed_drops_on_failure() { + let dropped = alloc::sync::Arc::new(core::sync::atomic::AtomicBool::new(false)); + let protocol = Box::new(TrackedProtocol { _value: 7, dropped: dropped.clone() }); + + let mut mock = MockProtocolServices::new(); + mock.expect_install_interface().times(1).returning(|_, _, _| Err(ProtocolError::InvalidParameter)); + + let result = mock.install_protocol(None, protocol); + assert!(result.is_err()); + assert!(dropped.load(core::sync::atomic::Ordering::SeqCst), "a failed install must drop, not leak"); + } + + #[test] + fn test_protocol_services_ext_with_protocol() { + let mut mock = MockProtocolServices::new(); + mock.expect_locate_interface().times(1).returning(|_| Ok(fake_ptr())); + + let value = mock.with_protocol::(|p| p.value).unwrap(); + assert_eq!(value, 42); + } + + #[test] + fn test_protocol_services_ext_open_protocol() { + let mut mock = MockProtocolServices::new(); + mock.expect_interface_on_handle().times(1).returning(|_, guid| { + assert_eq!(guid, FakeProtocol::PROTOCOL_GUID); + Ok(fake_ptr()) + }); + + let guard = mock.open_protocol::(fake_handle()).unwrap(); + assert_eq!(guard.value, 42); + } + + #[test] + fn test_protocol_services_ext_token_resolve() { + let mut mock = MockProtocolServices::new(); + mock.expect_locate_handles().times(1).returning(|_| Ok(alloc::vec![fake_handle()])); + mock.expect_interface_on_handle().times(1).returning(|_, _| Ok(fake_ptr())); + + let token = mock.locate_token::().unwrap(); + let resolved = mock.resolve(&token).unwrap(); + assert_eq!(resolved.value, 42); + } + + #[test] + fn test_protocol_services_ext_token_resolve_gone() { + let mut mock = MockProtocolServices::new(); + mock.expect_interface_on_handle().times(1).returning(|_, _| Err(ProtocolError::NotFound)); + + let token = ProtocolToken::::new(fake_handle()); + assert!(mock.resolve(&token).is_none()); + } + + #[test] + fn test_protocol_services_ext_notify() { + let mut mock = MockProtocolServices::new(); + mock.expect_register_install_notify().times(1).returning(|_, _, _| { + Ok(NotifyRegistration::from_raw(core::ptr::dangling_mut::(), 2 as *mut c_void, 3 as *mut c_void)) + }); + mock.expect_cancel_install_notify().times(1).returning(|reg| { + assert_eq!(reg.event(), core::ptr::dangling_mut::()); + Ok(()) + }); + + let registration = mock.on_protocol_installed::(Tpl::Callback, |_handle| {}).unwrap(); + assert!(mock.cancel(registration).is_ok()); + } +} diff --git a/sdk/patina/src/component/service/uefi_services/timer_event.rs b/sdk/patina/src/component/service/uefi_services/timer_event.rs new file mode 100644 index 000000000..50ca8663c --- /dev/null +++ b/sdk/patina/src/component/service/uefi_services/timer_event.rs @@ -0,0 +1,140 @@ +//! Timer event services for Patina components. +//! +//! [`TimerEventServices`] exposes UEFI timer-event operations, creating and arming a timer event, +//! as a safe, idiomatic Rust service. It is split out from [`EventServices`](super::event::EventServices) +//! because arming a timer depends on the Timer Architectural Protocol, so a component depending on +//! this service is not dispatched until the protocol is available and timers can actually fire. +//! +//! ## License +//! +//! Copyright (c) Microsoft Corporation. +//! +//! SPDX-License-Identifier: Apache-2.0 +//! + +use alloc::boxed::Box; +use core::time::Duration; + +use super::event::{Event, EventError, EventNotifyCallback}; +pub use super::tpl::Tpl; + +#[cfg(any(test, feature = "mockall"))] +use mockall::automock; + +/// Describes how a timer configured with [`TimerEventServices::set_timer`] should fire. +/// +/// The duration can be expressed in any unit supported by [`core::time::Duration`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TimerType { + /// Cancels a previously configured timer. + Cancel, + /// Fires the timer once, `Duration` from now. + Relative(Duration), + /// Fires the timer repeatedly, every `Duration`. + Periodic(Duration), +} + +/// Timer event services. +/// +/// This trait is object-safe and [`Self::create_timer_event`] takes a pre-boxed +/// [`EventNotifyCallback`]. Component authors should generally use the type-safe method provided +/// by [`TimerEventServicesExt`] instead of calling it directly. +/// +/// This service is implemented by the Patina DXE Core. Components consume it by adding a +/// [`Service`](crate::component::service::Service) parameter to their +/// entry point. The DXE Core only registers this service once the Timer Architectural Protocol is +/// installed, so a component depending on it is not dispatched until `set_timer` can actually +/// succeed. +/// +/// # Examples +/// +/// ```rust,no_run +/// use core::time::Duration; +/// use patina::component::service::{Service, uefi_services::timer_event::{TimerEventServices, TimerEventServicesExt, Tpl, TimerType}}; +/// use patina::error::Result; +/// +/// fn entry_point(events: Service) -> Result<()> { +/// let event = events.on_timer_event(Tpl::Callback, || { +/// log::info!("tick"); +/// })?; +/// events.set_timer(event, TimerType::Periodic(Duration::from_secs(1)))?; +/// Ok(()) +/// } +/// ``` +#[cfg_attr(any(test, feature = "mockall"), automock)] +pub trait TimerEventServices { + /// Creates a timer event with a notification callback. + /// + /// The returned event can be armed with [`Self::set_timer`]. The `callback` runs at + /// `notify_tpl` each time the timer fires, and is dropped when the event is closed with + /// [`EventServices::close_event`](super::event::EventServices::close_event). + /// + /// # Errors + /// + /// Returns [`EventError::InvalidParameter`] if the event could not be created. + fn create_timer_event(&self, notify_tpl: Tpl, callback: EventNotifyCallback) -> Result; + + /// Arms, re-arms, or cancels the timer on a timer event. + /// + /// The event must have been created with [`Self::create_timer_event`]. + /// + /// # Errors + /// + /// Returns [`EventError::InvalidParameter`] if `event` is not a valid timer event. + fn set_timer(&self, event: Event, timer_type: TimerType) -> Result<(), EventError>; +} + +/// Type-safe extension methods for [`TimerEventServices`]. +/// +/// This method accepts a plain closure and boxes it internally, so callers don't need to write +/// `Box::new` themselves. The trait is implemented for every [`TimerEventServices`] implementor +/// (including [`Service`]). +/// +/// [`Service`]: crate::component::service::Service +pub trait TimerEventServicesExt: TimerEventServices { + /// Creates a timer event with a notification callback. + /// + /// Equivalent to [`TimerEventServices::create_timer_event`], but takes a plain closure instead + /// of a pre-boxed [`EventNotifyCallback`]. + /// + /// # Errors + /// + /// Returns [`EventError::InvalidParameter`] if the event could not be created. + fn on_timer_event(&self, notify_tpl: Tpl, callback: impl FnMut() + 'static) -> Result { + self.create_timer_event(notify_tpl, Box::new(callback)) + } +} + +impl TimerEventServicesExt for T {} + +#[cfg(test)] +mod tests { + use super::*; + use core::ffi::c_void; + use core::ptr::NonNull; + + #[test] + fn test_timer_event_services_mock_timer_flow() { + let mut mock = MockTimerEventServices::new(); + mock.expect_create_timer_event() + .times(1) + .returning(|_, _| Ok(Event::from_raw(NonNull::::dangling().as_ptr()).unwrap())); + mock.expect_set_timer().times(1).returning(|_, timer_type| { + assert_eq!(timer_type, TimerType::Periodic(Duration::from_millis(10))); + Ok(()) + }); + + let event = mock.create_timer_event(Tpl::Callback, Box::new(|| {})).unwrap(); + assert!(mock.set_timer(event, TimerType::Periodic(Duration::from_millis(10))).is_ok()); + } + + #[test] + fn test_timer_event_services_ext_on_timer_event() { + let mut mock = MockTimerEventServices::new(); + mock.expect_create_timer_event() + .times(1) + .returning(|_, _| Ok(Event::from_raw(NonNull::::dangling().as_ptr()).unwrap())); + + assert!(mock.on_timer_event(Tpl::Callback, || {}).is_ok()); + } +} diff --git a/sdk/patina/src/component/service/uefi_services/timing.rs b/sdk/patina/src/component/service/uefi_services/timing.rs new file mode 100644 index 000000000..394e57099 --- /dev/null +++ b/sdk/patina/src/component/service/uefi_services/timing.rs @@ -0,0 +1,140 @@ +//! Timing services for Patina components. +//! +//! [`TimingServices`] exposes UEFI timing operations such as a fine-grained stall and the +//! system watchdog timer as a Rust service. +//! +//! Delays are expressed with [`core::time::Duration`] so callers never juggle raw microsecond or +//! 100ns counts. +//! +//! ## License +//! +//! Copyright (c) Microsoft Corporation. +//! +//! SPDX-License-Identifier: Apache-2.0 +//! + +use core::time::Duration; + +use crate::base::error::EfiError; + +#[cfg(any(test, feature = "mockall"))] +use mockall::automock; + +/// Errors that can occur when using [`TimingServices`]. +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub enum TimingError { + /// The underlying architectural support (metronome or watchdog) is not yet available. + NotReady, + /// The underlying device reported an error while performing the operation. + DeviceError, + /// A provided parameter was invalid. + InvalidParameter, + /// An unexpected internal error occurred. + Internal, +} + +impl From for EfiError { + fn from(value: TimingError) -> Self { + match value { + TimingError::NotReady => EfiError::NotReady, + TimingError::DeviceError => EfiError::DeviceError, + TimingError::InvalidParameter => EfiError::InvalidParameter, + TimingError::Internal => EfiError::Unsupported, + } + } +} + +impl From for TimingError { + fn from(value: EfiError) -> Self { + match value { + EfiError::NotReady => TimingError::NotReady, + EfiError::DeviceError => TimingError::DeviceError, + EfiError::InvalidParameter => TimingError::InvalidParameter, + _ => TimingError::Internal, + } + } +} + +/// Timing services providing delays and watchdog timer control. +/// +/// This service is implemented by the Patina DXE Core. Components consume it by adding a +/// [`Service`](crate::component::service::Service) parameter to their +/// entry point. +/// +/// The DXE Core only registers this service once the Metronome and Watchdog Timer Architectural +/// Protocols are both installed. +/// +/// # Examples +/// +/// ```rust,no_run +/// use core::time::Duration; +/// use patina::component::service::{Service, uefi_services::timing::TimingServices}; +/// use patina::error::Result; +/// +/// fn entry_point(timing: Service) -> Result<()> { +/// timing.stall(Duration::from_millis(10))?; +/// Ok(()) +/// } +/// ``` +#[cfg_attr(any(test, feature = "mockall"), automock)] +pub trait TimingServices { + /// Stalls execution for at least `duration`. + /// + /// `duration` can be expressed in any unit supported by [`core::time::Duration`]. + /// + /// Execution of the processor is not yielded for the duration of the stall. The delay is + /// rounded to the resolution the underlying metronome supports. + /// + /// # Errors + /// + /// Returns [`TimingError::NotReady`] if the metronome architectural support is not yet + /// available. + fn stall(&self, duration: Duration) -> Result<(), TimingError>; + + /// Sets the system watchdog timer. + /// + /// The watchdog timer will fire after `timeout_seconds` seconds unless it is reset or + /// disabled. A `timeout_seconds` of `0` disables the watchdog timer. `watchdog_code` is a + /// caller-defined code logged by the firmware if the watchdog fires. Codes `0x0000`-`0xffff` + /// are reserved by the UEFI specification for firmware use. + /// + /// # Errors + /// + /// Returns [`TimingError::NotReady`] if the watchdog architectural support is not yet + /// available, or [`TimingError::DeviceError`] if the underlying device reports an error. + fn set_watchdog_timer(&self, timeout_seconds: u64, watchdog_code: u64) -> Result<(), TimingError>; +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_timing_services_error_to_efi() { + assert_eq!(EfiError::from(TimingError::NotReady), EfiError::NotReady); + assert_eq!(EfiError::from(TimingError::DeviceError), EfiError::DeviceError); + assert_eq!(EfiError::from(TimingError::InvalidParameter), EfiError::InvalidParameter); + assert_eq!(EfiError::from(TimingError::Internal), EfiError::Unsupported); + } + + #[test] + fn test_timing_services_error_from_efi() { + assert_eq!(TimingError::from(EfiError::NotReady), TimingError::NotReady); + assert_eq!(TimingError::from(EfiError::DeviceError), TimingError::DeviceError); + assert_eq!(TimingError::from(EfiError::InvalidParameter), TimingError::InvalidParameter); + assert_eq!(TimingError::from(EfiError::NotFound), TimingError::Internal); + } + + #[test] + fn test_timing_services_mock_delegation() { + let mut mock = MockTimingServices::new(); + mock.expect_stall().times(1).returning(|duration| { + assert_eq!(duration, Duration::from_millis(1)); + Ok(()) + }); + mock.expect_set_watchdog_timer().times(1).returning(|_, _| Err(TimingError::NotReady)); + + assert_eq!(mock.stall(Duration::from_millis(1)), Ok(())); + assert_eq!(mock.set_watchdog_timer(5, 0), Err(TimingError::NotReady)); + } +} diff --git a/sdk/patina/src/component/service/uefi_services/tpl.rs b/sdk/patina/src/component/service/uefi_services/tpl.rs new file mode 100644 index 000000000..173c2edaa --- /dev/null +++ b/sdk/patina/src/component/service/uefi_services/tpl.rs @@ -0,0 +1,205 @@ +//! Task priority level (TPL) services for Patina components. +//! +//! [`TplServices`] exposes the UEFI `RaiseTPL`/`RestoreTPL` boot services. It offers two styles of +//! use: +//! +//! - **Ergonomic (recommended):** [`TplServicesExt::raise`] returns a [`TplGuard`] that +//! restores the previous TPL when dropped, and [`TplServicesExt::with_raised_tpl`] runs a closure +//! at a raised TPL. +//! - **Manual:** [`TplServices::raise_tpl`] returns an opaque [`PreviousTpl`] token that is passed +//! back to [`TplServices::restore_tpl`], for cases where the raise and restore cannot be scoped +//! to a single lexical block. +//! +//! Raising to a level below the current TPL, or restoring to a level above the current TPL, is a +//! programming error. +//! +//! ## License +//! +//! Copyright (c) Microsoft Corporation. +//! +//! SPDX-License-Identifier: Apache-2.0 +//! + +#[cfg(any(test, feature = "mockall"))] +use mockall::automock; + +/// A task priority level, used to serialize access to shared state in the UEFI event model. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Tpl { + /// The lowest priority level, used for normal execution (`TPL_APPLICATION`). + Application, + /// The priority level for most notification callbacks (`TPL_CALLBACK`). + Callback, + /// The priority level for notifications that must not be interrupted by other callbacks + /// (`TPL_NOTIFY`). + Notify, + /// The highest priority level. Disables interrupts for the duration (`TPL_HIGH_LEVEL`). + HighLevel, +} + +/// An opaque token representing the TPL that was active before a raise. +/// +/// It is produced by [`TplServices::raise_tpl`] and consumed by [`TplServices::restore_tpl`]. It +/// captures the exact previous level (including intermediate levels), so restoring is always +/// faithful. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct PreviousTpl(usize); + +impl PreviousTpl { + /// Wraps a raw TPL value produced by the service implementation. + /// + /// This is intended for use by service implementations, not component authors. + #[doc(hidden)] + pub fn from_raw(tpl: usize) -> Self { + Self(tpl) + } + + /// Returns the raw TPL value for use by the service implementation. + /// + /// This is intended for use by service implementations, not component authors. + #[doc(hidden)] + pub fn as_raw(&self) -> usize { + self.0 + } +} + +/// Task Priority Level (TPL) Services. +/// +/// This service is implemented by the Patina DXE Core. Components consume it by adding a +/// [`Service`](crate::component::service::Service) parameter to their entry point. +/// +/// Most components should prefer the ergonomic [`TplServicesExt`] methods over calling +/// [`Self::raise_tpl`]/[`Self::restore_tpl`] directly. +#[cfg_attr(any(test, feature = "mockall"), automock)] +pub trait TplServices { + /// Raises the task priority level to `tpl`, returning a token for the previous level. + /// + /// The returned [`PreviousTpl`] must be passed to [`Self::restore_tpl`] to restore the prior + /// level. Prefer [`TplServicesExt::raise`] or [`TplServicesExt::with_raised_tpl`], which handle + /// the restore automatically. + /// + /// # Panics + /// + /// Panics if `tpl` is below the current TPL, matching the UEFI specification. + fn raise_tpl(&self, tpl: Tpl) -> PreviousTpl; + + /// Restores the task priority level to a previously raised level. + /// + /// # Panics + /// + /// Panics if `previous` is above the current TPL, matching the UEFI specification. + fn restore_tpl(&self, previous: PreviousTpl); +} + +/// A guard that restores the previous task priority level when dropped. +/// +/// Created by [`TplServicesExt::raise`]. While the guard is alive the TPL remains raised. When it +/// is dropped (for example at the end of a block) the previous level is restored. +#[must_use = "the TPL is restored when the guard is dropped; bind it to a variable to keep the TPL raised"] +pub struct TplGuard<'a, T: TplServices + ?Sized> { + services: &'a T, + previous: PreviousTpl, +} + +impl Drop for TplGuard<'_, T> { + fn drop(&mut self) { + self.services.restore_tpl(self.previous); + } +} + +/// Ergonomic extension methods for [`TplServices`]. +/// +/// Implemented for every [`TplServices`] implementor (including +/// [`Service`](crate::component::service::Service)). +/// +/// # Examples +/// +/// ```rust,no_run +/// use patina::component::service::{Service, uefi_services::tpl::{TplServices, TplServicesExt, Tpl}}; +/// use patina::error::Result; +/// +/// fn entry_point(tpl: Service) -> Result<()> { +/// // Scoped raise via a guard. +/// { +/// let _guard = tpl.raise(Tpl::Notify); +/// // ... critical section runs at TPL_NOTIFY ... +/// } // previous TPL restored here +/// +/// // Or run a closure at a raised TPL. +/// let value = tpl.with_raised_tpl(Tpl::Notify, || 42); +/// assert_eq!(value, 42); +/// Ok(()) +/// } +/// ``` +pub trait TplServicesExt: TplServices { + /// Raises the TPL to `tpl` and returns a [`TplGuard`] that restores it when dropped. + fn raise(&self, tpl: Tpl) -> TplGuard<'_, Self> { + let previous = self.raise_tpl(tpl); + TplGuard { services: self, previous } + } + + /// Runs `f` with the TPL raised to `tpl`, restoring the previous level afterward. + fn with_raised_tpl(&self, tpl: Tpl, f: impl FnOnce() -> R) -> R { + let _guard = self.raise(tpl); + f() + } +} + +impl TplServicesExt for T {} + +#[cfg(test)] +mod tests { + use super::*; + use core::cell::Cell; + + #[test] + fn test_tpl_services_previous_previous_raw_is_correct() { + let previous = PreviousTpl::from_raw(16); + assert_eq!(previous.as_raw(), 16); + } + + #[test] + fn test_tpl_services_manual_raise_restore() { + let mut mock = MockTplServices::new(); + mock.expect_raise_tpl().times(1).returning(|tpl| { + assert_eq!(tpl, Tpl::Notify); + PreviousTpl::from_raw(4) + }); + mock.expect_restore_tpl().times(1).returning(|previous| { + assert_eq!(previous.as_raw(), 4); + }); + + let previous = mock.raise_tpl(Tpl::Notify); + mock.restore_tpl(previous); + } + + #[test] + fn test_tpl_services_guard_restores_on_drop() { + let mut mock = MockTplServices::new(); + mock.expect_raise_tpl().times(1).returning(|_| PreviousTpl::from_raw(8)); + mock.expect_restore_tpl().times(1).returning(|previous| { + assert_eq!(previous.as_raw(), 8); + }); + + { + let _guard = mock.raise(Tpl::HighLevel); + // Guard is alive here. Restore has not been called yet. + } + // Guard dropped. Restore has now been called (verified by mock expectations). + } + + #[test] + fn test_tpl_services_with_raised_tpl_runs_closure() { + let mut mock = MockTplServices::new(); + mock.expect_raise_tpl().times(1).returning(|_| PreviousTpl::from_raw(8)); + mock.expect_restore_tpl().times(1).returning(|_| {}); + + let ran = Cell::new(false); + let result = mock.with_raised_tpl(Tpl::Callback, || { + ran.set(true); + 123 + }); + assert!(ran.get()); + assert_eq!(result, 123); + } +} diff --git a/sdk/patina/src/performance/measurement.rs b/sdk/patina/src/performance/measurement.rs index e8ec7895a..5e1f4b269 100644 --- a/sdk/patina/src/performance/measurement.rs +++ b/sdk/patina/src/performance/measurement.rs @@ -8,8 +8,10 @@ //! use core::{ffi::c_void, mem, ops::BitOr}; +use crate::component::service::uefi_services::config_table::ConfigTable; +use crate::performance::guid::PERFORMANCE_PROTOCOL_GUID; use crate::standard::efi; -use crate::{bit, performance::record::known::KnownPerfId}; +use crate::{BinaryGuid, bit, performance::record::known::KnownPerfId}; /// The attribute of the measurement. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] @@ -181,6 +183,10 @@ impl PerformanceProperty { } } +impl ConfigTable for PerformanceProperty { + const TABLE_GUID: BinaryGuid = PERFORMANCE_PROTOCOL_GUID; +} + #[cfg(test)] #[cfg_attr(coverage, coverage(off))] mod tests {