From 92f611e9a8646e31e67352c3f310902fba7a5393 Mon Sep 17 00:00:00 2001 From: Alex Zenla Date: Thu, 30 Oct 2025 21:38:49 -0400 Subject: [PATCH] feat(shim): initial shim support --- src/actions/chainload.rs | 13 +-- src/drivers.rs | 31 ++----- src/integrations.rs | 2 + src/integrations/shim.rs | 193 +++++++++++++++++++++++++++++++++++++++ src/main.rs | 4 +- src/utils.rs | 46 ++++++++-- 6 files changed, 249 insertions(+), 40 deletions(-) create mode 100644 src/integrations/shim.rs diff --git a/src/actions/chainload.rs b/src/actions/chainload.rs index 5341c4c..62dd664 100644 --- a/src/actions/chainload.rs +++ b/src/actions/chainload.rs @@ -1,5 +1,6 @@ use crate::context::SproutContext; use crate::integrations::bootloader_interface::BootloaderInterface; +use crate::integrations::shim::{ShimInput, ShimSupport}; use crate::utils; use crate::utils::media_loader::MediaLoaderHandle; use crate::utils::media_loader::constants::linux::LINUX_EFI_INITRD_MEDIA_GUID; @@ -40,15 +41,9 @@ pub fn chainload(context: Rc, configuration: &ChainloadConfigurat ) .context("unable to resolve chainload path")?; - // Load the image to chainload. - let image = uefi::boot::load_image( - sprout_image, - uefi::boot::LoadImageSource::FromDevicePath { - device_path: &resolved.full_path, - boot_policy: uefi::proto::BootPolicy::ExactMatch, - }, - ) - .context("unable to load image")?; + // Load the image to chainload using the shim support integration. + // It will determine if the image needs to be loaded via the shim or can be loaded directly. + let image = ShimSupport::load(sprout_image, ShimInput::ResolvedPath(&resolved))?; // Open the LoadedImage protocol of the image to chainload. let mut loaded_image_protocol = uefi::boot::open_protocol_exclusive::(image) diff --git a/src/drivers.rs b/src/drivers.rs index e58952d..0d387b1 100644 --- a/src/drivers.rs +++ b/src/drivers.rs @@ -1,4 +1,5 @@ use crate::context::SproutContext; +use crate::integrations::shim::{ShimInput, ShimSupport}; use crate::utils; use anyhow::{Context, Result}; use log::info; @@ -6,7 +7,6 @@ use serde::{Deserialize, Serialize}; use std::collections::BTreeMap; use std::rc::Rc; use uefi::boot::SearchType; -use uefi::proto::device_path::LoadedImageDevicePath; /// Declares a driver configuration. /// Drivers allow extending the functionality of Sprout. @@ -23,28 +23,17 @@ pub struct DriverDeclaration { fn load_driver(context: Rc, driver: &DriverDeclaration) -> Result<()> { // Acquire the handle and device path of the loaded image. let sprout_image = uefi::boot::image_handle(); - let image_device_path_protocol = - uefi::boot::open_protocol_exclusive::(sprout_image) - .context("unable to open loaded image device path protocol")?; - // Get the device path root of the sprout image. - let mut full_path = utils::device_path_root(&image_device_path_protocol)?; - - // Push the path of the driver from the root. - full_path.push_str(&context.stamp(&driver.path)); - - // Convert the path to a device path. - let device_path = utils::text_to_device_path(&full_path)?; - - // Load the driver image. - let image = uefi::boot::load_image( - sprout_image, - uefi::boot::LoadImageSource::FromDevicePath { - device_path: &device_path, - boot_policy: uefi::proto::BootPolicy::ExactMatch, - }, + // Resolve the path to the driver image. + let resolved = utils::resolve_path( + context.root().loaded_image_path()?, + &context.stamp(&driver.path), ) - .context("unable to load image")?; + .context("unable to resolve path to driver")?; + + // Load the driver image using the shim support integration. + // It will determine if the image needs to be loaded via the shim or can be loaded directly. + let image = ShimSupport::load(sprout_image, ShimInput::ResolvedPath(&resolved))?; // Start the driver image, this is expected to return control to sprout. // There is no guarantee that the driver will actually return control as it is diff --git a/src/integrations.rs b/src/integrations.rs index 286bf58..e05eaae 100644 --- a/src/integrations.rs +++ b/src/integrations.rs @@ -1,2 +1,4 @@ /// Implements support for the bootloader interface specification. pub mod bootloader_interface; +/// Implements support for the shim loader application for Secure Boot. +pub mod shim; diff --git a/src/integrations/shim.rs b/src/integrations/shim.rs new file mode 100644 index 0000000..2d40f41 --- /dev/null +++ b/src/integrations/shim.rs @@ -0,0 +1,193 @@ +use crate::utils; +use crate::utils::ResolvedPath; +use anyhow::{Context, Result, anyhow, bail}; +use std::ffi::c_void; +use uefi::Handle; +use uefi::boot::LoadImageSource; +use uefi::proto::device_path::DevicePath; +use uefi::proto::unsafe_protocol; +use uefi_raw::{Guid, Status, guid}; + +/// Support for the shim loader application for Secure Boot. +pub struct ShimSupport; + +/// Input to the shim mechanisms. +pub enum ShimInput<'a> { + /// Data loaded into a buffer and ready to be verified, owned. + OwnedDataBuffer(Option<&'a ResolvedPath>, Vec), + /// Data loaded into a buffer and ready to be verified. + DataBuffer(Option<&'a ResolvedPath>, &'a [u8]), + /// Data is provided as a resolved path. We will need to load the data to verify it. + /// The output will them return the loaded data. + ResolvedPath(&'a ResolvedPath), +} + +impl<'a> ShimInput<'a> { + /// Accesses the buffer behind the shim input, if available. + pub fn buffer(&self) -> Option<&[u8]> { + match self { + ShimInput::OwnedDataBuffer(_, data) => Some(data), + ShimInput::DataBuffer(_, data) => Some(data), + ShimInput::ResolvedPath(_) => None, + } + } + + /// Accesses the full device path to the input. + pub fn file_path(&self) -> Option<&DevicePath> { + match self { + ShimInput::OwnedDataBuffer(path, _) => path.as_ref().map(|it| it.full_path.as_ref()), + ShimInput::DataBuffer(path, _) => path.as_ref().map(|it| it.full_path.as_ref()), + ShimInput::ResolvedPath(path) => Some(path.full_path.as_ref()), + } + } + + /// Converts this input into an owned data buffer, where the data is loaded. + /// For ResolvedPath, this will read the file. + pub fn into_owned_data_buffer(self) -> Result> { + match self { + ShimInput::OwnedDataBuffer(root, data) => Ok(ShimInput::OwnedDataBuffer(root, data)), + + ShimInput::DataBuffer(root, data) => { + Ok(ShimInput::OwnedDataBuffer(root, data.to_vec())) + } + + ShimInput::ResolvedPath(path) => { + Ok(ShimInput::OwnedDataBuffer(Some(path), path.read_file()?)) + } + } + } +} + +/// Output of the shim verification function. +/// Since the shim needs to load the data from disk, we will optimize by using that as the data +/// to actually boot. +pub enum ShimVerificationOutput { + /// The verification failed. + VerificationFailed, + /// The data provided to the verifier was already a buffer. + VerifiedDataNotLoaded, + /// Verifying the data resulted in loading the data from the source. + /// This contains the data that was loaded, so it won't need to be loaded again. + VerifiedDataBuffer(Vec), +} + +/// The shim lock protocol as defined by the shim loader application. +#[unsafe_protocol(ShimSupport::SHIM_LOCK_GUID)] +struct ShimLockProtocol { + /// Verify the data in `buffer` with the size `buffer_size` to determine if it is valid. + pub shim_verify: unsafe extern "efiapi" fn(buffer: *mut c_void, buffer_size: u32) -> Status, + /// Unused function that is defined by the shim. + _generate_header: *mut c_void, + /// Unused function that is defined by the shim. + _read_header: *mut c_void, +} + +impl ShimSupport { + /// GUID for the shim lock protocol. + const SHIM_LOCK_GUID: Guid = guid!("605dab50-e046-4300-abb6-3dd810dd8b23"); + /// GUID for the shim image loader protocol. + const SHIM_IMAGE_LOADER_GUID: Guid = guid!("1f492041-fadb-4e59-9e57-7cafe73a55ab"); + + /// Determines whether the shim is loaded. + pub fn loaded() -> Result { + Ok(utils::find_handle(&Self::SHIM_LOCK_GUID) + .context("unable to find shim lock protocol")? + .is_some()) + } + + /// Determines whether the shim loader is available. + pub fn loader_available() -> Result { + Ok(utils::find_handle(&Self::SHIM_IMAGE_LOADER_GUID) + .context("unable to find shim image loader protocol")? + .is_some()) + } + + /// Use the shim to validate the `input`, returning [ShimVerificationOutput] when complete. + pub fn validate(input: ShimInput) -> Result { + // Acquire the handle to the shim lock protocol. + let handle = utils::find_handle(&Self::SHIM_LOCK_GUID) + .context("unable to find shim lock protocol")? + .ok_or_else(|| anyhow!("unable to find shim lock protocol"))?; + // Acquire the protocol exclusively to the shim lock. + let protocol = uefi::boot::open_protocol_exclusive::(handle) + .context("unable to open shim lock protocol")?; + + // If the input type is a device path, we need to load the data. + let maybe_loaded_data = match input { + ShimInput::OwnedDataBuffer(_, _data) => { + bail!("owned data buffer is not supported in the verification function"); + } + ShimInput::DataBuffer(_, _) => None, + ShimInput::ResolvedPath(path) => Some(path.read_file()?), + }; + + // Convert the input to a buffer. + // If the input provides the data buffer, we will use that. + // Otherwise, we will use the data loaded by this function. + let buffer = match input { + ShimInput::OwnedDataBuffer(_root, _data) => { + bail!("owned data buffer is not supported in the verification function"); + } + ShimInput::DataBuffer(_root, data) => data, + ShimInput::ResolvedPath(_path) => maybe_loaded_data + .as_deref() + .context("expected data buffer to be loaded already")?, + }; + + // Check if the buffer is too large to verify. + if buffer.len() > u32::MAX as usize { + bail!("buffer is too large to verify with shim lock protocol"); + } + + // Call the shim verify function. + // SAFETY: The shim verify function is specified by the shim lock protocol. + // Calling this function is considered safe because + let status = + unsafe { (protocol.shim_verify)(buffer.as_ptr() as *mut c_void, buffer.len() as u32) }; + + // If the verification failed, return the verification failure output. + if !status.is_success() { + return Ok(ShimVerificationOutput::VerificationFailed); + } + + // If verification succeeded, return the validation output, + // which might include the loaded data. + Ok(maybe_loaded_data + .map(ShimVerificationOutput::VerifiedDataBuffer) + .unwrap_or(ShimVerificationOutput::VerifiedDataNotLoaded)) + } + + /// Load the image specified by the `input` and returns an image handle. + pub fn load(current_image: Handle, input: ShimInput) -> Result { + // Determine whether the shim is loaded. + let shim_loaded = Self::loaded().context("unable to determine if shim is loaded")?; + + // Determine whether the shim loader is available. + let shim_loader_available = + Self::loader_available().context("unable to determine if shim loader is available")?; + + // Determines whether LoadImage in Boot Services must be patched. + // Version 16 of the shim doesn't require extra effort to load Secure Boot binaries. + // If the image loader is installed, we can skip over the security override. + let requires_security_override = shim_loaded && !shim_loader_available; + + // If the security override is required, we will bail for now. + if requires_security_override { + bail!("shim image loader protocol is not available, please upgrade to shim version 16"); + } + + // Converts the shim input to an owned data buffer. + let input = input + .into_owned_data_buffer() + .context("unable to convert input to loaded data buffer")?; + + // Constructs a LoadImageSource from the input. + let source = LoadImageSource::FromBuffer { + buffer: input.buffer().context("unable to get buffer from input")?, + file_path: input.file_path(), + }; + + // Loads the image using Boot Services LoadImage function. + uefi::boot::load_image(current_image, source).context("unable to load image") + } +} diff --git a/src/main.rs b/src/main.rs index 0aa27ad..5028efa 100644 --- a/src/main.rs +++ b/src/main.rs @@ -16,7 +16,7 @@ use crate::platform::timer::PlatformTimer; use crate::secure::SecureBoot; use crate::utils::PartitionGuidForm; use anyhow::{Context, Result, bail}; -use log::{error, info}; +use log::{error, info, warn}; use std::collections::BTreeMap; use std::ops::Deref; use std::time::Duration; @@ -74,7 +74,7 @@ pub mod utils; fn run() -> Result<()> { // For safety reasons, we will bail early if Secure Boot is enabled. if SecureBoot::enabled().context("unable to determine Secure Boot status")? { - bail!("Secure Boot is enabled. Sprout does not currently support Secure Boot."); + warn!("Secure Boot is enabled. Sprout does not currently support Secure Boot."); } // Start the platform timer. diff --git a/src/utils.rs b/src/utils.rs index 526d61c..6b999ff 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,5 +1,6 @@ use anyhow::{Context, Result}; use std::ops::Deref; +use uefi::boot::SearchType; use uefi::fs::{FileSystem, Path}; use uefi::proto::device_path::text::{AllowShortcuts, DevicePathFromText, DisplayOnly}; use uefi::proto::device_path::{DevicePath, PoolDevicePath}; @@ -103,6 +104,20 @@ pub struct ResolvedPath { pub filesystem_handle: Handle, } +impl ResolvedPath { + /// Read the file specified by this path into a buffer and return it. + pub fn read_file(&self) -> Result> { + let fs = uefi::boot::open_protocol_exclusive::(self.filesystem_handle) + .context("unable to open filesystem protocol")?; + let mut fs = FileSystem::new(fs); + let path = self + .sub_path + .to_string(DisplayOnly(false), AllowShortcuts(false))?; + let content = fs.read(Path::new(&path)); + content.context("unable to read file contents") + } +} + /// Resolve a path specified by `input` to its various components. /// Uses `default_root_path` as the base root if one is not specified in the path. /// Returns [ResolvedPath] which contains the resolved components. @@ -157,14 +172,7 @@ pub fn resolve_path(default_root_path: &DevicePath, input: &str) -> Result Result> { let resolved = resolve_path(default_root_path, input)?; - let fs = uefi::boot::open_protocol_exclusive::(resolved.filesystem_handle) - .context("unable to open filesystem protocol")?; - let mut fs = FileSystem::new(fs); - let path = resolved - .sub_path - .to_string(DisplayOnly(false), AllowShortcuts(false))?; - let content = fs.read(Path::new(&path)); - content.context("unable to read file contents") + resolved.read_file() } /// Filter a string-like Option `input` such that an empty string is [None]. @@ -232,3 +240,25 @@ pub fn partition_guid(path: &DevicePath, form: PartitionGuidForm) -> Result Result> { + // Locate the requested protocol handle. + match uefi::boot::locate_handle_buffer(SearchType::ByProtocol(protocol)) { + // If a handle is found, the protocol is available. + Ok(handles) => Ok(if handles.is_empty() { + None + } else { + Some(handles[0]) + }), + // If an error occurs, check if it is because the protocol is not available. + // If so, return false. Otherwise, return the error. + Err(error) => { + if error.status() == Status::NOT_FOUND { + Ok(None) + } else { + Err(error).context("unable to determine if the protocol is available") + } + } + } +}