From 313c67ea233a7008208e64b1ab9b471c643d9cb5 Mon Sep 17 00:00:00 2001 From: Kat Perez Date: Wed, 27 May 2026 19:04:36 -0400 Subject: [PATCH] Add patina_sre crate (System Recovery Environment boot orchestrator) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds patina_sre at uefi/crates/patina_sre/ — implements patina_boot::BootOrchestrator for platforms shipping a System Recovery Environment alongside the main OS. The skeleton implements the normal boot path: 1. interleave connect+dispatch (10-round cap) 2. extra connect_all before EndOfDxe so PartitionDxe can bind GPT child handles during the open driver-binding window 3. signal EndOfDxe 4. discover console devices 5. boot-partition write-lock (currently a log::warn! stub pending odp-platform-common#61's patina_boot::partition helper) 6. discover_boot_options + iterate each Boot#### entry through signal_ready_to_boot + boot_from_device_path; logs the device path and underlying error on each failure 7. fall back to the constructor-provided main_os_path if discovery yields no entries OR fails 8. return EfiError::NotFound once every attempt is exhausted The crate re-exports DevicePathBuf + EndEntire from its own patina source so callers (e.g. surface_patina_intel/patina_bin) can construct the constructor's device-path arguments without picking up a different patina (which would break trait coherence). Hotkey-to-SRE entry, WIM-to-RAM-disk boot, and capsule pre-boot hook are tracked separately and will layer onto this skeleton. Verified end-to-end on Maa Intel Surface hardware (Kioxia KBG8 NVMe, NVMe 2.0, CAP.BPS=1) via the paired surface_patina_intel feature branch: BootDispatcher dispatches, SreBootManager.execute() runs the full BDS phase, discover_boot_options finds the Windows Boot Manager Boot####, expand_device_path resolves the short-form HD(GPT,GUID) path against the live device topology, and bootmgfw.efi loads + starts cleanly. Closes #91. --- uefi/crates/patina_sre/.gitignore | 2 + uefi/crates/patina_sre/Cargo.toml | 46 +++ uefi/crates/patina_sre/README.md | 38 +++ uefi/crates/patina_sre/rust-toolchain.toml | 9 + uefi/crates/patina_sre/rustfmt.toml | 6 + uefi/crates/patina_sre/src/lib.rs | 27 ++ .../crates/patina_sre/src/sre_boot_manager.rs | 262 ++++++++++++++++++ 7 files changed, 390 insertions(+) create mode 100644 uefi/crates/patina_sre/.gitignore create mode 100644 uefi/crates/patina_sre/Cargo.toml create mode 100644 uefi/crates/patina_sre/README.md create mode 100644 uefi/crates/patina_sre/rust-toolchain.toml create mode 100644 uefi/crates/patina_sre/rustfmt.toml create mode 100644 uefi/crates/patina_sre/src/lib.rs create mode 100644 uefi/crates/patina_sre/src/sre_boot_manager.rs diff --git a/uefi/crates/patina_sre/.gitignore b/uefi/crates/patina_sre/.gitignore new file mode 100644 index 0000000..96ef6c0 --- /dev/null +++ b/uefi/crates/patina_sre/.gitignore @@ -0,0 +1,2 @@ +/target +Cargo.lock diff --git a/uefi/crates/patina_sre/Cargo.toml b/uefi/crates/patina_sre/Cargo.toml new file mode 100644 index 0000000..cdd7fc5 --- /dev/null +++ b/uefi/crates/patina_sre/Cargo.toml @@ -0,0 +1,46 @@ +# Cargo manifest for the patina_sre crate. +# +# SPDX-License-Identifier: MIT +# + +[package] +name = "patina_sre" +version = "0.1.0" +edition = "2024" +rust-version = "1.89" +license = "MIT" +repository = "https://github.com/OpenDevicePartnership/odp-platform-common" +homepage = "https://github.com/OpenDevicePartnership/odp-platform-common" +description = """ +System Recovery Environment boot orchestrator. Implements +patina_boot::BootOrchestrator for platforms shipping an SRE alongside the main +OS — normal boot today; hotkey-to-SRE, WIM RAM-disk boot, and capsule pre-boot +hook land in follow-up issues. +""" +keywords = ["uefi", "patina", "firmware", "recovery", "sre"] +categories = ["embedded", "no-std"] +readme = "README.md" + +[dependencies] +log = { version = "0.4", default-features = false } +r-efi = { version = "5", default-features = false } +spin = { version = "0.9", default-features = false, features = ["spin_mutex"] } + +# patina_boot lives in OpenDevicePartnership/patina-components and is not yet +# published to crates.io. Pinning to main here; swap to a `version = ...` line +# once it publishes. +patina_boot = { git = "https://github.com/OpenDevicePartnership/patina-components", branch = "main" } + +# Must match the patina that patina_boot itself uses, or trait/type identity +# breaks (two distinct `patina::DevicePathBuf` types, mismatched impls). +patina = { git = "https://github.com/OpenDevicePartnership/patina", branch = "feature/patina-boot", features = ["unstable-device-path"] } + +[dev-dependencies] +patina = { git = "https://github.com/OpenDevicePartnership/patina", branch = "feature/patina-boot", features = ["mockall", "unstable-device-path"] } +mockall = { version = "0.13" } + +[lints.clippy] +suspicious = "deny" +correctness = "deny" +perf = "deny" +style = "deny" diff --git a/uefi/crates/patina_sre/README.md b/uefi/crates/patina_sre/README.md new file mode 100644 index 0000000..353f020 --- /dev/null +++ b/uefi/crates/patina_sre/README.md @@ -0,0 +1,38 @@ +# patina_sre + +System Recovery Environment boot orchestrator for Patina firmware. + +`SreBootManager` implements [`patina_boot::BootOrchestrator`] for platforms that +ship a System Recovery Environment alongside the main OS. The current crate is +a **skeleton** implementing the normal boot path only: + +1. Interleave controller connection with DXE driver dispatch +2. Extra `connect_all` pass before EndOfDxe so platforms whose driver-binding + runs only in the open window get a chance to bind (e.g. `PartitionDxe` + creating GPT child handles) +3. Signal `EndOfDxe` (security lockdown) +4. Discover console devices +5. Write-lock the NVMe boot partition *(pending — TODO referencing + [odp-platform-common#61](https://github.com/OpenDevicePartnership/odp-platform-common/issues/61))* +6. Enumerate firmware `Boot####` EFI variables via `discover_boot_options` + and try each in order +7. Fall back to the constructor-provided `main_os_path` if discovery yields + nothing (or fails) + +Follow-ups (tracked separately): Power+Vol-Up hotkey to enter SRE, SRE WIM +RAM-disk boot, capsule-update pre-boot hook. + +## Use + +```rust,ignore +use patina_boot::BootDispatcher; +use patina_sre::SreBootManager; + +add.component(BootDispatcher::new( + SreBootManager::new(boot_partition_path, main_os_path), +)); +``` + +## License + +MIT diff --git a/uefi/crates/patina_sre/rust-toolchain.toml b/uefi/crates/patina_sre/rust-toolchain.toml new file mode 100644 index 0000000..9e754e5 --- /dev/null +++ b/uefi/crates/patina_sre/rust-toolchain.toml @@ -0,0 +1,9 @@ +# Pinned Rust toolchain for patina_sre (UEFI targets). +# +# SPDX-License-Identifier: MIT +# + +[toolchain] +channel = "nightly-2025-12-12" +targets = ["x86_64-unknown-uefi", "aarch64-unknown-uefi"] +components = ["rust-src", "clippy", "rustfmt"] diff --git a/uefi/crates/patina_sre/rustfmt.toml b/uefi/crates/patina_sre/rustfmt.toml new file mode 100644 index 0000000..e6ee16b --- /dev/null +++ b/uefi/crates/patina_sre/rustfmt.toml @@ -0,0 +1,6 @@ +# rustfmt configuration for the patina_sre crate. +# +# SPDX-License-Identifier: MIT +# + +max_width = 120 diff --git a/uefi/crates/patina_sre/src/lib.rs b/uefi/crates/patina_sre/src/lib.rs new file mode 100644 index 0000000..3fe85fc --- /dev/null +++ b/uefi/crates/patina_sre/src/lib.rs @@ -0,0 +1,27 @@ +//! System Recovery Environment boot orchestrator for Patina firmware. +//! +//! See the crate [README](https://github.com/OpenDevicePartnership/odp-platform-common/tree/main/uefi/crates/patina_sre) +//! for the full boot-path description and follow-up issues. +//! +//! ## License +//! +//! Copyright (c) Microsoft Corporation. +//! +//! SPDX-License-Identifier: MIT +//! +#![cfg_attr(not(test), no_std)] +#![feature(coverage_attribute)] +#![feature(never_type)] + +extern crate alloc; + +mod sre_boot_manager; + +pub use sre_boot_manager::SreBootManager; + +// Re-export the patina device-path types SreBootManager::new consumes, so callers +// (e.g. surface_patina_intel/patina_bin) don't need to depend on the same patina +// source patina_sre uses. Constructing DevicePathBuf via these re-exports +// guarantees type identity with SreBootManager's constructor signature. +pub use patina::device_path::node_defs::EndEntire; +pub use patina::device_path::paths::DevicePathBuf; diff --git a/uefi/crates/patina_sre/src/sre_boot_manager.rs b/uefi/crates/patina_sre/src/sre_boot_manager.rs new file mode 100644 index 0000000..e5af6ea --- /dev/null +++ b/uefi/crates/patina_sre/src/sre_boot_manager.rs @@ -0,0 +1,262 @@ +//! System Recovery Environment boot manager. +//! +//! [`SreBootManager`] implements [`patina_boot::BootOrchestrator`] for platforms +//! shipping a System Recovery Environment alongside the main OS. The current +//! skeleton implements the **normal** boot path only: +//! +//! 1. Interleave controller connection with DXE driver dispatch +//! 2. Extra `connect_all` pass before EndOfDxe so platforms whose driver-binding +//! runs only in the open window get a chance to bind (e.g. PartitionDxe +//! creating GPT child handles) +//! 3. Signal `EndOfDxe` (security lockdown) +//! 4. Discover console devices +//! 5. Write-lock the NVMe boot partition (volatile, until power cycle) +//! — currently a stub pending `patina_boot::partition` (odp-platform-common#61) +//! 6. Enumerate firmware `Boot####` EFI variables via `discover_boot_options` +//! and try each in order: `signal_ready_to_boot` then `boot_from_device_path` +//! 7. If discovery yields no entries or fails, fall back to the +//! constructor-provided `main_os_path` (one final `signal_ready_to_boot` + +//! `boot_from_device_path`) +//! 8. Return `EfiError::NotFound` if every boot attempt has been exhausted +//! +//! Hotkey detection (Power+Vol-Up → SRE), SRE WIM RAM-disk boot, and capsule +//! update orchestration are tracked separately and will layer onto this skeleton +//! without changing the public constructor surface. +//! +//! ## License +//! +//! Copyright (c) Microsoft Corporation. +//! +//! SPDX-License-Identifier: MIT +//! +extern crate alloc; + +use patina::{ + boot_services::{BootServices, StandardBootServices}, + component::service::dxe_dispatch::DxeDispatch, + device_path::paths::DevicePathBuf, + error::EfiError, + runtime_services::StandardRuntimeServices, +}; +use patina_boot::{BootOrchestrator, helpers}; +use r_efi::efi; + +fn interleave_connect_and_dispatch( + boot_services: &B, + dxe_services: &D, +) -> patina::error::Result<()> { + const MAX_ROUNDS: usize = 10; + + for _round in 0..MAX_ROUNDS { + helpers::connect_all(boot_services)?; + if !dxe_services.dispatch()? { + return Ok(()); + } + } + + log::warn!("connect-dispatch interleaving did not converge after {MAX_ROUNDS} rounds"); + + Ok(()) +} + +/// SRE boot manager implementing [`BootOrchestrator`]. +/// +/// Skeleton — normal boot path only. The SRE-entry hotkey, WIM-to-RAM-disk boot, +/// and capsule-update pre-boot hook will land in subsequent issues and extend this +/// orchestrator without changing the public constructor surface. +pub struct SreBootManager { + boot_partition_path: DevicePathBuf, + main_os_path: DevicePathBuf, +} + +impl SreBootManager { + /// Construct an `SreBootManager` from the device paths of the boot partition + /// (to be write-locked before OS hand-off) and the main OS boot device. + pub fn new(boot_partition_path: DevicePathBuf, main_os_path: DevicePathBuf) -> Self { + Self { boot_partition_path, main_os_path } + } +} + +impl BootOrchestrator for SreBootManager { + #[coverage(off)] + fn execute( + &self, + boot_services: &StandardBootServices, + runtime_services: &StandardRuntimeServices, + dxe_dispatch: &dyn DxeDispatch, + image_handle: efi::Handle, + ) -> Result { + if let Err(e) = interleave_connect_and_dispatch(boot_services, dxe_dispatch) { + log::error!("interleave_connect_and_dispatch failed: {:?}", e); + } + + // One last connect pass before EndOfDxe so PartitionDxe and similar + // driver bindings can run during the open window. + if let Err(e) = helpers::connect_all(boot_services) { + log::error!("connect_all (pre-EndOfDxe) failed: {:?}", e); + } + + if let Err(e) = helpers::signal_bds_phase_entry(boot_services) { + log::error!("signal_bds_phase_entry failed: {:?}", e); + } + + if let Err(e) = helpers::discover_console_devices(boot_services, runtime_services) { + log::error!("discover_console_devices failed: {:?}", e); + } + + // TODO(odp-platform-common#61): boot-partition write-lock helper isn't in + // patina_boot yet (PR #1488 closed; reopening planned). Skipping the lock + // for now — the SRE integrity guarantee requires this before shipping. + log::warn!( + "boot-partition write-lock skipped (issue #61 pending); target path = {:?}", + self.boot_partition_path + ); + + // Try boot options discovered from the firmware's Boot#### EFI variables. + // The constructor's `main_os_path` is used as a fallback when discovery + // either fails OR yields no entries (both leave `tried_any == false`). + let mut tried_any = false; + match helpers::discover_boot_options(runtime_services) { + Ok(boot_config) => { + for device_path in boot_config.devices() { + tried_any = true; + if let Err(e) = helpers::signal_ready_to_boot(boot_services) { + log::error!("signal_ready_to_boot failed: {:?}", e); + } + match helpers::boot_from_device_path(boot_services, image_handle, device_path) { + Ok(()) => log::warn!("Boot option returned control (path={:?}), trying next...", device_path), + Err(e) => log::warn!("Boot option failed (path={:?}): {:?}", device_path, e), + } + } + } + Err(e) => log::error!("discover_boot_options failed: {:?}", e), + } + + if !tried_any { + // Discovery returned no entries or errored — fall back to the + // constructor-provided path. + if let Err(e) = helpers::signal_ready_to_boot(boot_services) { + log::error!("signal_ready_to_boot failed: {:?}", e); + } + match helpers::boot_from_device_path(boot_services, image_handle, &self.main_os_path) { + Ok(()) => log::warn!("Main OS fallback returned control (path={:?})", self.main_os_path), + Err(e) => log::warn!("Main OS fallback failed (path={:?}): {:?}", self.main_os_path, e), + } + } + + log::error!("SRE normal boot exhausted all boot options"); + Err(EfiError::NotFound) + } +} + +#[cfg(test)] +mod tests { + extern crate alloc; + + use super::*; + use alloc::{boxed::Box, sync::Arc, vec::Vec}; + use patina::{ + boot_services::{MockBootServices, boxed::BootServicesBox}, + device_path::{node_defs::EndEntire, paths::DevicePathBuf}, + }; + + fn test_device_path() -> DevicePathBuf { + DevicePathBuf::from_device_path_node_iter(core::iter::once(EndEntire)) + } + + struct MockDxeDispatcher { + results: spin::Mutex>>, + } + + impl MockDxeDispatcher { + fn new(results: &[patina::error::Result]) -> Self { + Self { results: spin::Mutex::new(results.iter().cloned().collect()) } + } + } + + impl DxeDispatch for MockDxeDispatcher { + fn dispatch(&self) -> patina::error::Result { + self.results.lock().pop_front().expect("MockDxeDispatcher: unexpected dispatch call") + } + } + + fn leaked_boot_services_for_box() -> &'static MockBootServices { + Box::leak(Box::new({ + let mut m = MockBootServices::new(); + m.expect_free_pool().returning(|_| Ok(())); + m + })) + } + + fn mock_handle_buffer( + handle_addrs: &[usize], + boot_services: &'static MockBootServices, + ) -> BootServicesBox<'static, [efi::Handle], MockBootServices> { + let handles: Vec = handle_addrs.iter().map(|&a| a as efi::Handle).collect(); + let leaked = handles.leak(); + // SAFETY: leaked is a valid pointer+length from Vec::leak. + unsafe { BootServicesBox::from_raw_parts_mut(leaked.as_mut_ptr(), leaked.len(), boot_services) } + } + + #[test] + fn test_new_constructs() { + let _ = SreBootManager::new(test_device_path(), test_device_path()); + } + + #[test] + fn test_interleave_single_round_no_drivers_dispatched() { + let box_mock = leaked_boot_services_for_box(); + let mut boot_mock = MockBootServices::new(); + + boot_mock.expect_locate_handle_buffer().returning(move |_| Ok(mock_handle_buffer(&[1], box_mock))); + boot_mock.expect_connect_controller().returning(|_, _, _, _| Ok(())); + + let dxe_mock = MockDxeDispatcher::new(&[Ok(false)]); + + let result = interleave_connect_and_dispatch(&boot_mock, &dxe_mock); + assert!(result.is_ok()); + } + + #[test] + fn test_interleave_dispatch_failure_propagates() { + let box_mock = leaked_boot_services_for_box(); + let mut boot_mock = MockBootServices::new(); + + boot_mock.expect_locate_handle_buffer().returning(move |_| Ok(mock_handle_buffer(&[1], box_mock))); + boot_mock.expect_connect_controller().returning(|_, _, _, _| Ok(())); + + let dxe_mock = MockDxeDispatcher::new(&[Err(EfiError::DeviceError)]); + + let result = interleave_connect_and_dispatch(&boot_mock, &dxe_mock); + assert!(result.is_err()); + } + + #[test] + fn test_interleave_stops_at_max_rounds() { + let box_mock = leaked_boot_services_for_box(); + let mut boot_mock = MockBootServices::new(); + + boot_mock.expect_locate_handle_buffer().returning(move |_| Ok(mock_handle_buffer(&[1], box_mock))); + boot_mock.expect_connect_controller().returning(|_, _, _, _| Ok(())); + + let dxe_mock = MockDxeDispatcher::new(&[Ok(true); 10]); + + let result = interleave_connect_and_dispatch(&boot_mock, &dxe_mock); + assert!(result.is_ok()); + } + + // Type-level confirmation that SreBootManager satisfies BootOrchestrator's + // Send + Sync + 'static bounds at compile time. + #[test] + fn test_implements_boot_orchestrator() { + fn assert_orchestrator() {} + assert_orchestrator::(); + } + + // Confirm the manager is constructible behind an Arc, + // matching the BootDispatcher consumption path. + #[test] + fn test_arc_dyn_construction() { + let _: Arc = Arc::new(SreBootManager::new(test_device_path(), test_device_path())); + } +}