mirror of
https://github.com/edera-dev/sprout.git
synced 2025-12-19 19:00:18 +00:00
feat(shim): initial shim support
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
use crate::context::SproutContext;
|
use crate::context::SproutContext;
|
||||||
use crate::integrations::bootloader_interface::BootloaderInterface;
|
use crate::integrations::bootloader_interface::BootloaderInterface;
|
||||||
|
use crate::integrations::shim::{ShimInput, ShimSupport};
|
||||||
use crate::utils;
|
use crate::utils;
|
||||||
use crate::utils::media_loader::MediaLoaderHandle;
|
use crate::utils::media_loader::MediaLoaderHandle;
|
||||||
use crate::utils::media_loader::constants::linux::LINUX_EFI_INITRD_MEDIA_GUID;
|
use crate::utils::media_loader::constants::linux::LINUX_EFI_INITRD_MEDIA_GUID;
|
||||||
@@ -40,15 +41,9 @@ pub fn chainload(context: Rc<SproutContext>, configuration: &ChainloadConfigurat
|
|||||||
)
|
)
|
||||||
.context("unable to resolve chainload path")?;
|
.context("unable to resolve chainload path")?;
|
||||||
|
|
||||||
// Load the image to chainload.
|
// Load the image to chainload using the shim support integration.
|
||||||
let image = uefi::boot::load_image(
|
// It will determine if the image needs to be loaded via the shim or can be loaded directly.
|
||||||
sprout_image,
|
let image = ShimSupport::load(sprout_image, ShimInput::ResolvedPath(&resolved))?;
|
||||||
uefi::boot::LoadImageSource::FromDevicePath {
|
|
||||||
device_path: &resolved.full_path,
|
|
||||||
boot_policy: uefi::proto::BootPolicy::ExactMatch,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.context("unable to load image")?;
|
|
||||||
|
|
||||||
// Open the LoadedImage protocol of the image to chainload.
|
// Open the LoadedImage protocol of the image to chainload.
|
||||||
let mut loaded_image_protocol = uefi::boot::open_protocol_exclusive::<LoadedImage>(image)
|
let mut loaded_image_protocol = uefi::boot::open_protocol_exclusive::<LoadedImage>(image)
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use crate::context::SproutContext;
|
use crate::context::SproutContext;
|
||||||
|
use crate::integrations::shim::{ShimInput, ShimSupport};
|
||||||
use crate::utils;
|
use crate::utils;
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use log::info;
|
use log::info;
|
||||||
@@ -6,7 +7,6 @@ use serde::{Deserialize, Serialize};
|
|||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use uefi::boot::SearchType;
|
use uefi::boot::SearchType;
|
||||||
use uefi::proto::device_path::LoadedImageDevicePath;
|
|
||||||
|
|
||||||
/// Declares a driver configuration.
|
/// Declares a driver configuration.
|
||||||
/// Drivers allow extending the functionality of Sprout.
|
/// Drivers allow extending the functionality of Sprout.
|
||||||
@@ -23,28 +23,17 @@ pub struct DriverDeclaration {
|
|||||||
fn load_driver(context: Rc<SproutContext>, driver: &DriverDeclaration) -> Result<()> {
|
fn load_driver(context: Rc<SproutContext>, driver: &DriverDeclaration) -> Result<()> {
|
||||||
// Acquire the handle and device path of the loaded image.
|
// Acquire the handle and device path of the loaded image.
|
||||||
let sprout_image = uefi::boot::image_handle();
|
let sprout_image = uefi::boot::image_handle();
|
||||||
let image_device_path_protocol =
|
|
||||||
uefi::boot::open_protocol_exclusive::<LoadedImageDevicePath>(sprout_image)
|
|
||||||
.context("unable to open loaded image device path protocol")?;
|
|
||||||
|
|
||||||
// Get the device path root of the sprout image.
|
// Resolve the path to the driver image.
|
||||||
let mut full_path = utils::device_path_root(&image_device_path_protocol)?;
|
let resolved = utils::resolve_path(
|
||||||
|
context.root().loaded_image_path()?,
|
||||||
// Push the path of the driver from the root.
|
&context.stamp(&driver.path),
|
||||||
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,
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
.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.
|
// 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
|
// There is no guarantee that the driver will actually return control as it is
|
||||||
|
|||||||
@@ -1,2 +1,4 @@
|
|||||||
/// Implements support for the bootloader interface specification.
|
/// Implements support for the bootloader interface specification.
|
||||||
pub mod bootloader_interface;
|
pub mod bootloader_interface;
|
||||||
|
/// Implements support for the shim loader application for Secure Boot.
|
||||||
|
pub mod shim;
|
||||||
|
|||||||
193
src/integrations/shim.rs
Normal file
193
src/integrations/shim.rs
Normal file
@@ -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<u8>),
|
||||||
|
/// 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<ShimInput<'a>> {
|
||||||
|
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<u8>),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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<bool> {
|
||||||
|
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<bool> {
|
||||||
|
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<ShimVerificationOutput> {
|
||||||
|
// 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::<ShimLockProtocol>(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<Handle> {
|
||||||
|
// 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")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,7 +16,7 @@ use crate::platform::timer::PlatformTimer;
|
|||||||
use crate::secure::SecureBoot;
|
use crate::secure::SecureBoot;
|
||||||
use crate::utils::PartitionGuidForm;
|
use crate::utils::PartitionGuidForm;
|
||||||
use anyhow::{Context, Result, bail};
|
use anyhow::{Context, Result, bail};
|
||||||
use log::{error, info};
|
use log::{error, info, warn};
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
@@ -74,7 +74,7 @@ pub mod utils;
|
|||||||
fn run() -> Result<()> {
|
fn run() -> Result<()> {
|
||||||
// For safety reasons, we will bail early if Secure Boot is enabled.
|
// For safety reasons, we will bail early if Secure Boot is enabled.
|
||||||
if SecureBoot::enabled().context("unable to determine Secure Boot status")? {
|
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.
|
// Start the platform timer.
|
||||||
|
|||||||
46
src/utils.rs
46
src/utils.rs
@@ -1,5 +1,6 @@
|
|||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
|
use uefi::boot::SearchType;
|
||||||
use uefi::fs::{FileSystem, Path};
|
use uefi::fs::{FileSystem, Path};
|
||||||
use uefi::proto::device_path::text::{AllowShortcuts, DevicePathFromText, DisplayOnly};
|
use uefi::proto::device_path::text::{AllowShortcuts, DevicePathFromText, DisplayOnly};
|
||||||
use uefi::proto::device_path::{DevicePath, PoolDevicePath};
|
use uefi::proto::device_path::{DevicePath, PoolDevicePath};
|
||||||
@@ -103,6 +104,20 @@ pub struct ResolvedPath {
|
|||||||
pub filesystem_handle: Handle,
|
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<Vec<u8>> {
|
||||||
|
let fs = uefi::boot::open_protocol_exclusive::<SimpleFileSystem>(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.
|
/// 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.
|
/// Uses `default_root_path` as the base root if one is not specified in the path.
|
||||||
/// Returns [ResolvedPath] which contains the resolved components.
|
/// Returns [ResolvedPath] which contains the resolved components.
|
||||||
@@ -157,14 +172,7 @@ pub fn resolve_path(default_root_path: &DevicePath, input: &str) -> Result<Resol
|
|||||||
/// the filesystem handle protocol acquired.
|
/// the filesystem handle protocol acquired.
|
||||||
pub fn read_file_contents(default_root_path: &DevicePath, input: &str) -> Result<Vec<u8>> {
|
pub fn read_file_contents(default_root_path: &DevicePath, input: &str) -> Result<Vec<u8>> {
|
||||||
let resolved = resolve_path(default_root_path, input)?;
|
let resolved = resolve_path(default_root_path, input)?;
|
||||||
let fs = uefi::boot::open_protocol_exclusive::<SimpleFileSystem>(resolved.filesystem_handle)
|
resolved.read_file()
|
||||||
.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")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Filter a string-like Option `input` such that an empty string is [None].
|
/// 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<Opti
|
|||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Find a handle that provides the specified `protocol`.
|
||||||
|
pub fn find_handle(protocol: &Guid) -> Result<Option<Handle>> {
|
||||||
|
// 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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user