mirror of
https://github.com/edera-dev/sprout.git
synced 2025-12-19 15:50:18 +00:00
Merge pull request #25 from edera-dev/azenla/shim-support
feat(boot): basic support for secure boot via shim
This commit is contained in:
@@ -18,7 +18,7 @@ existing UEFI bootloader or booted by the hardware directly.
|
|||||||
|
|
||||||
Sprout is licensed under Apache 2.0 and is open to modifications and contributions.
|
Sprout is licensed under Apache 2.0 and is open to modifications and contributions.
|
||||||
|
|
||||||
**IMPORTANT WARNING**: Sprout does not support UEFI Secure Boot yet.
|
**IMPORTANT WARNING**: Sprout does not support all of UEFI Secure Boot yet.
|
||||||
See [this issue](https://github.com/edera-dev/sprout/issues/20) for updates.
|
See [this issue](https://github.com/edera-dev/sprout/issues/20) for updates.
|
||||||
|
|
||||||
## Background
|
## Background
|
||||||
@@ -65,13 +65,13 @@ The boot menu mechanism is very rudimentary.
|
|||||||
- [x] Load Linux initrd from disk
|
- [x] Load Linux initrd from disk
|
||||||
- [x] Basic boot menu
|
- [x] Basic boot menu
|
||||||
- [x] BLS autoconfiguration support
|
- [x] BLS autoconfiguration support
|
||||||
|
- [x] [Secure Boot support](https://github.com/edera-dev/sprout/issues/20): partial
|
||||||
|
|
||||||
### Roadmap
|
### Roadmap
|
||||||
|
|
||||||
- [ ] [Bootloader interface support](https://github.com/edera-dev/sprout/issues/21)
|
- [ ] [Bootloader interface support](https://github.com/edera-dev/sprout/issues/21)
|
||||||
- [ ] [BLS specification conformance](https://github.com/edera-dev/sprout/issues/2)
|
- [ ] [BLS specification conformance](https://github.com/edera-dev/sprout/issues/2)
|
||||||
- [ ] [Full-featured boot menu](https://github.com/edera-dev/sprout/issues/1)
|
- [ ] [Full-featured boot menu](https://github.com/edera-dev/sprout/issues/1)
|
||||||
- [ ] [Secure Boot support](https://github.com/edera-dev/sprout/issues/20): work in progress
|
|
||||||
- [ ] [UKI support](https://github.com/edera-dev/sprout/issues/6): partial
|
- [ ] [UKI support](https://github.com/edera-dev/sprout/issues/6): partial
|
||||||
- [ ] [multiboot2 support](https://github.com/edera-dev/sprout/issues/7)
|
- [ ] [multiboot2 support](https://github.com/edera-dev/sprout/issues/7)
|
||||||
- [ ] [Linux boot protocol (boot without EFI stub)](https://github.com/edera-dev/sprout/issues/7)
|
- [ ] [Linux boot protocol (boot without EFI stub)](https://github.com/edera-dev/sprout/issues/7)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
- Secure Boot disabled
|
- Secure Boot is disabled or configured to allow Sprout
|
||||||
- UEFI Windows installation
|
- UEFI Windows installation
|
||||||
|
|
||||||
## Step 1: Base Installation
|
## Step 1: Base Installation
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -35,20 +36,14 @@ pub fn chainload(context: Rc<SproutContext>, configuration: &ChainloadConfigurat
|
|||||||
|
|
||||||
// Resolve the path to the image to chainload.
|
// Resolve the path to the image to chainload.
|
||||||
let resolved = utils::resolve_path(
|
let resolved = utils::resolve_path(
|
||||||
context.root().loaded_image_path()?,
|
Some(context.root().loaded_image_path()?),
|
||||||
&context.stamp(&configuration.path),
|
&context.stamp(&configuration.path),
|
||||||
)
|
)
|
||||||
.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)
|
||||||
@@ -95,7 +90,8 @@ pub fn chainload(context: Rc<SproutContext>, configuration: &ChainloadConfigurat
|
|||||||
// If an initrd is provided, register it with the EFI stack.
|
// If an initrd is provided, register it with the EFI stack.
|
||||||
let mut initrd_handle = None;
|
let mut initrd_handle = None;
|
||||||
if let Some(linux_initrd) = initrd {
|
if let Some(linux_initrd) = initrd {
|
||||||
let content = utils::read_file_contents(context.root().loaded_image_path()?, &linux_initrd)
|
let content =
|
||||||
|
utils::read_file_contents(Some(context.root().loaded_image_path()?), &linux_initrd)
|
||||||
.context("unable to read linux initrd")?;
|
.context("unable to read linux initrd")?;
|
||||||
let handle =
|
let handle =
|
||||||
MediaLoaderHandle::register(LINUX_EFI_INITRD_MEDIA_GUID, content.into_boxed_slice())
|
MediaLoaderHandle::register(LINUX_EFI_INITRD_MEDIA_GUID, content.into_boxed_slice())
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ fn register_media_loader_file(
|
|||||||
// Stamp the path to the file.
|
// Stamp the path to the file.
|
||||||
let path = context.stamp(path);
|
let path = context.stamp(path);
|
||||||
// Read the file contents.
|
// Read the file contents.
|
||||||
let content = utils::read_file_contents(context.root().loaded_image_path()?, &path)
|
let content = utils::read_file_contents(Some(context.root().loaded_image_path()?), &path)
|
||||||
.context(format!("unable to read {} file", what))?;
|
.context(format!("unable to read {} file", what))?;
|
||||||
// Register the media loader.
|
// Register the media loader.
|
||||||
let handle = MediaLoaderHandle::register(guid, content.into_boxed_slice())
|
let handle = MediaLoaderHandle::register(guid, content.into_boxed_slice())
|
||||||
|
|||||||
@@ -143,7 +143,7 @@ pub fn splash(context: Rc<SproutContext>, configuration: &SplashConfiguration) -
|
|||||||
// Stamp the image path value.
|
// Stamp the image path value.
|
||||||
let image = context.stamp(&configuration.image);
|
let image = context.stamp(&configuration.image);
|
||||||
// Read the image contents.
|
// Read the image contents.
|
||||||
let image = read_file_contents(context.root().loaded_image_path()?, &image)?;
|
let image = read_file_contents(Some(context.root().loaded_image_path()?), &image)?;
|
||||||
// Decode the image as a PNG.
|
// Decode the image as a PNG.
|
||||||
let image = ImageReader::with_format(Cursor::new(image), ImageFormat::Png)
|
let image = ImageReader::with_format(Cursor::new(image), ImageFormat::Png)
|
||||||
.decode()
|
.decode()
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ fn load_raw_config(options: &SproutOptions) -> Result<Vec<u8>> {
|
|||||||
info!("configuration file: {}", options.config);
|
info!("configuration file: {}", options.config);
|
||||||
|
|
||||||
// Read the contents of the sprout config file.
|
// Read the contents of the sprout config file.
|
||||||
let content = utils::read_file_contents(&path, &options.config)
|
let content = utils::read_file_contents(Some(&path), &options.config)
|
||||||
.context("unable to read sprout config file")?;
|
.context("unable to read sprout config file")?;
|
||||||
// Return the contents of the sprout config file.
|
// Return the contents of the sprout config file.
|
||||||
Ok(content)
|
Ok(content)
|
||||||
|
|||||||
@@ -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(
|
||||||
|
Some(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
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ pub fn generate(context: Rc<SproutContext>, bls: &BlsConfiguration) -> Result<Ve
|
|||||||
let path = context.stamp(&bls.path);
|
let path = context.stamp(&bls.path);
|
||||||
|
|
||||||
// Resolve the path to the BLS directory.
|
// Resolve the path to the BLS directory.
|
||||||
let bls_resolved = utils::resolve_path(context.root().loaded_image_path()?, &path)
|
let bls_resolved = utils::resolve_path(Some(context.root().loaded_image_path()?), &path)
|
||||||
.context("unable to resolve bls path")?;
|
.context("unable to resolve bls path")?;
|
||||||
|
|
||||||
// Construct a filesystem path to the BLS entries directory.
|
// Construct a filesystem path to the BLS entries directory.
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
272
src/integrations/shim.rs
Normal file
272
src/integrations/shim.rs
Normal file
@@ -0,0 +1,272 @@
|
|||||||
|
use crate::integrations::shim::hook::SecurityHook;
|
||||||
|
use crate::utils;
|
||||||
|
use crate::utils::ResolvedPath;
|
||||||
|
use crate::utils::variables::VariableController;
|
||||||
|
use anyhow::{Context, Result, anyhow, bail};
|
||||||
|
use std::ffi::c_void;
|
||||||
|
use uefi::Handle;
|
||||||
|
use uefi::boot::LoadImageSource;
|
||||||
|
use uefi::proto::device_path::text::{AllowShortcuts, DisplayOnly};
|
||||||
|
use uefi::proto::device_path::{DevicePath, FfiDevicePath};
|
||||||
|
use uefi::proto::unsafe_protocol;
|
||||||
|
use uefi_raw::table::runtime::VariableVendor;
|
||||||
|
use uefi_raw::{Guid, Status, guid};
|
||||||
|
|
||||||
|
/// Security hook support.
|
||||||
|
mod hook;
|
||||||
|
|
||||||
|
/// 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]),
|
||||||
|
/// Low-level data buffer provided by the security hook.
|
||||||
|
SecurityHookBuffer(Option<*const FfiDevicePath>, &'a [u8]),
|
||||||
|
/// Low-level owned data buffer provided by the security hook.
|
||||||
|
SecurityHookOwnedBuffer(Option<*const FfiDevicePath>, Vec<u8>),
|
||||||
|
/// Low-level path provided by the security hook.
|
||||||
|
SecurityHookPath(*const FfiDevicePath),
|
||||||
|
/// 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::SecurityHookOwnedBuffer(_, data) => Some(data),
|
||||||
|
ShimInput::SecurityHookBuffer(_, data) => Some(data),
|
||||||
|
ShimInput::SecurityHookPath(_) => None,
|
||||||
|
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::SecurityHookBuffer(path, _) => {
|
||||||
|
path.map(|it| unsafe { DevicePath::from_ffi_ptr(it) })
|
||||||
|
}
|
||||||
|
ShimInput::SecurityHookPath(path) => unsafe { Some(DevicePath::from_ffi_ptr(*path)) },
|
||||||
|
ShimInput::ResolvedPath(path) => Some(path.full_path.as_ref()),
|
||||||
|
ShimInput::SecurityHookOwnedBuffer(path, _) => {
|
||||||
|
path.map(|it| unsafe { DevicePath::from_ffi_ptr(it) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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::SecurityHookPath(ffi_path) => {
|
||||||
|
// Acquire the file path.
|
||||||
|
let Some(path) = self.file_path() else {
|
||||||
|
bail!("unable to convert security hook path to device path");
|
||||||
|
};
|
||||||
|
// Convert the underlying path to a string.
|
||||||
|
let path = path
|
||||||
|
.to_string(DisplayOnly(false), AllowShortcuts(false))
|
||||||
|
.context("unable to convert device path to string")?;
|
||||||
|
let path = utils::resolve_path(None, &path.to_string())
|
||||||
|
.context("unable to resolve path")?;
|
||||||
|
// Read the file path.
|
||||||
|
let data = path.read_file()?;
|
||||||
|
Ok(ShimInput::SecurityHookOwnedBuffer(Some(ffi_path), data))
|
||||||
|
}
|
||||||
|
|
||||||
|
ShimInput::SecurityHookBuffer(_, _) => {
|
||||||
|
bail!("unable to convert security hook buffer to owned data buffer")
|
||||||
|
}
|
||||||
|
|
||||||
|
ShimInput::ResolvedPath(path) => {
|
||||||
|
Ok(ShimInput::OwnedDataBuffer(Some(path), path.read_file()?))
|
||||||
|
}
|
||||||
|
|
||||||
|
ShimInput::SecurityHookOwnedBuffer(path, data) => {
|
||||||
|
Ok(ShimInput::SecurityHookOwnedBuffer(path, data))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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 {
|
||||||
|
/// Variable controller for the shim lock.
|
||||||
|
const SHIM_LOCK_VARIABLES: VariableController =
|
||||||
|
VariableController::new(VariableVendor(Self::SHIM_LOCK_GUID));
|
||||||
|
|
||||||
|
/// 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 verify(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::SecurityHookBuffer(_, _) => None,
|
||||||
|
ShimInput::SecurityHookOwnedBuffer(_, _) => None,
|
||||||
|
ShimInput::DataBuffer(_, _) => None,
|
||||||
|
ShimInput::ResolvedPath(path) => Some(path.read_file()?),
|
||||||
|
ShimInput::SecurityHookPath(_) => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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) => data,
|
||||||
|
ShimInput::DataBuffer(_root, data) => *data,
|
||||||
|
ShimInput::ResolvedPath(_path) => maybe_loaded_data
|
||||||
|
.as_deref()
|
||||||
|
.context("expected data buffer to be loaded already")?,
|
||||||
|
ShimInput::SecurityHookBuffer(_, data) => data,
|
||||||
|
ShimInput::SecurityHookOwnedBuffer(_, data) => data,
|
||||||
|
ShimInput::SecurityHookPath(_) => {
|
||||||
|
bail!("security hook path input not supported in the verification function")
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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 hook.
|
||||||
|
let requires_security_hook = shim_loaded && !shim_loader_available;
|
||||||
|
|
||||||
|
// If the security hook is required, we will bail for now.
|
||||||
|
if requires_security_hook {
|
||||||
|
// Install the security hook, if possible. If it's not, this is necessary to continue
|
||||||
|
// so we should bail.
|
||||||
|
let installed = SecurityHook::install().context("unable to install security hook")?;
|
||||||
|
if !installed {
|
||||||
|
bail!("unable to install security hook require for this platform");
|
||||||
|
}
|
||||||
|
// Retain the shim protocol after load.
|
||||||
|
Self::retain()?
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
let result = uefi::boot::load_image(current_image, source).context("unable to load image");
|
||||||
|
|
||||||
|
// If the security override is required, we will uninstall the security hook.
|
||||||
|
if requires_security_hook {
|
||||||
|
SecurityHook::uninstall().context("unable to uninstall security hook")?;
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the ShimRetainProtocol variable to indicate that shim should retain the protocols
|
||||||
|
/// for the full lifetime of boot services.
|
||||||
|
pub fn retain() -> Result<()> {
|
||||||
|
Self::SHIM_LOCK_VARIABLES
|
||||||
|
.set_bool("ShimRetainProtocol", true)
|
||||||
|
.context("unable to retain shim protocol")?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
214
src/integrations/shim/hook.rs
Normal file
214
src/integrations/shim/hook.rs
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
use crate::integrations::shim::{ShimInput, ShimSupport, ShimVerificationOutput};
|
||||||
|
use crate::utils;
|
||||||
|
use anyhow::{Context, Result, bail};
|
||||||
|
use log::warn;
|
||||||
|
use std::sync::{LazyLock, Mutex};
|
||||||
|
use uefi::proto::device_path::FfiDevicePath;
|
||||||
|
use uefi::proto::unsafe_protocol;
|
||||||
|
use uefi::{Guid, guid};
|
||||||
|
use uefi_raw::Status;
|
||||||
|
|
||||||
|
/// GUID for the EFI_SECURITY_ARCH protocol.
|
||||||
|
const SECURITY_ARCH_GUID: Guid = guid!("a46423e3-4617-49f1-b9ff-d1bfa9115839");
|
||||||
|
/// GUID for the EFI_SECURITY_ARCH2 protocol.
|
||||||
|
const SECURITY_ARCH2_GUID: Guid = guid!("94ab2f58-1438-4ef1-9152-18941a3a0e68");
|
||||||
|
|
||||||
|
/// EFI_SECURITY_ARCH protocol definition.
|
||||||
|
#[unsafe_protocol(SECURITY_ARCH_GUID)]
|
||||||
|
pub struct SecurityArchProtocol {
|
||||||
|
/// Determines the file authentication state.
|
||||||
|
pub file_authentication_state: unsafe extern "efiapi" fn(
|
||||||
|
this: *const SecurityArchProtocol,
|
||||||
|
status: u32,
|
||||||
|
path: *mut FfiDevicePath,
|
||||||
|
) -> Status,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// EFI_SECURITY_ARCH2 protocol definition.
|
||||||
|
#[unsafe_protocol(SECURITY_ARCH2_GUID)]
|
||||||
|
pub struct SecurityArch2Protocol {
|
||||||
|
/// Determines the file authentication.
|
||||||
|
pub file_authentication: unsafe extern "efiapi" fn(
|
||||||
|
this: *const SecurityArch2Protocol,
|
||||||
|
path: *mut FfiDevicePath,
|
||||||
|
file_buffer: *mut u8,
|
||||||
|
file_size: usize,
|
||||||
|
boot_policy: bool,
|
||||||
|
) -> Status,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Global state for the security hook.
|
||||||
|
struct SecurityHookState {
|
||||||
|
original_hook: SecurityArchProtocol,
|
||||||
|
original_hook2: SecurityArch2Protocol,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Global state for the security hook.
|
||||||
|
/// This is messy, but it is safe given the mutex.
|
||||||
|
static GLOBAL_HOOK_STATE: LazyLock<Mutex<Option<SecurityHookState>>> =
|
||||||
|
LazyLock::new(|| Mutex::new(None));
|
||||||
|
|
||||||
|
/// Security hook helper.
|
||||||
|
pub struct SecurityHook;
|
||||||
|
|
||||||
|
impl SecurityHook {
|
||||||
|
/// Shared verifier logic for both hook types.
|
||||||
|
fn verify(input: ShimInput) -> Status {
|
||||||
|
// Verify the input.
|
||||||
|
match ShimSupport::verify(input) {
|
||||||
|
Ok(output) => match output {
|
||||||
|
// If the verification failed, return the access-denied status.
|
||||||
|
ShimVerificationOutput::VerificationFailed => Status::ACCESS_DENIED,
|
||||||
|
// If the verification succeeded, return the success status.
|
||||||
|
ShimVerificationOutput::VerifiedDataNotLoaded => Status::SUCCESS,
|
||||||
|
ShimVerificationOutput::VerifiedDataBuffer(_) => Status::SUCCESS,
|
||||||
|
},
|
||||||
|
|
||||||
|
// If an error occurs, log the error since we can't return a better error.
|
||||||
|
// Then return the access-denied status.
|
||||||
|
Err(error) => {
|
||||||
|
warn!("unable to verify image: {}", error);
|
||||||
|
Status::ACCESS_DENIED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// File authentication state verifier for the EFI_SECURITY_ARCH protocol.
|
||||||
|
/// Takes the `path` and determines the verification.
|
||||||
|
unsafe extern "efiapi" fn arch_file_authentication_state(
|
||||||
|
_this: *const SecurityArchProtocol,
|
||||||
|
_status: u32,
|
||||||
|
path: *mut FfiDevicePath,
|
||||||
|
) -> Status {
|
||||||
|
// Verify the path is not null.
|
||||||
|
if path.is_null() {
|
||||||
|
return Status::INVALID_PARAMETER;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Construct a shim input from the path.
|
||||||
|
let input = ShimInput::SecurityHookPath(path);
|
||||||
|
|
||||||
|
// Verify the input.
|
||||||
|
Self::verify(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// File authentication verifier for the EFI_SECURITY_ARCH2 protocol.
|
||||||
|
/// Takes the `path` and a file buffer to determine the verification.
|
||||||
|
unsafe extern "efiapi" fn arch2_file_authentication(
|
||||||
|
_this: *const SecurityArch2Protocol,
|
||||||
|
path: *mut FfiDevicePath,
|
||||||
|
file_buffer: *mut u8,
|
||||||
|
file_size: usize,
|
||||||
|
boot_policy: bool,
|
||||||
|
) -> Status {
|
||||||
|
// Verify the path and file buffer are not null.
|
||||||
|
if path.is_null() || file_buffer.is_null() {
|
||||||
|
return Status::INVALID_PARAMETER;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the boot policy is true, we can't continue as we don't support that.
|
||||||
|
if boot_policy {
|
||||||
|
return Status::INVALID_PARAMETER;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Construct a slice out of the file buffer and size.
|
||||||
|
let buffer = unsafe { std::slice::from_raw_parts_mut(file_buffer, file_size) };
|
||||||
|
|
||||||
|
// Construct a shim input from the path.
|
||||||
|
let input = ShimInput::SecurityHookBuffer(Some(path), buffer);
|
||||||
|
|
||||||
|
// Verify the input.
|
||||||
|
Self::verify(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Install the security hook if needed.
|
||||||
|
pub fn install() -> Result<bool> {
|
||||||
|
// Find the security arch protocol. If we can't find it, we will return false.
|
||||||
|
let Some(hook_arch) = utils::find_handle(&SECURITY_ARCH_GUID)
|
||||||
|
.context("unable to check security arch existence")?
|
||||||
|
else {
|
||||||
|
return Ok(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Find the security arch2 protocol. If we can't find it, we will return false.
|
||||||
|
let Some(hook_arch2) = utils::find_handle(&SECURITY_ARCH2_GUID)
|
||||||
|
.context("unable to check security arch2 existence")?
|
||||||
|
else {
|
||||||
|
return Ok(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Open the security arch protocol.
|
||||||
|
let mut arch_protocol =
|
||||||
|
uefi::boot::open_protocol_exclusive::<SecurityArchProtocol>(hook_arch)
|
||||||
|
.context("unable to open security arch protocol")?;
|
||||||
|
|
||||||
|
// Open the security arch2 protocol.
|
||||||
|
let mut arch_protocol2 =
|
||||||
|
uefi::boot::open_protocol_exclusive::<SecurityArch2Protocol>(hook_arch2)
|
||||||
|
.context("unable to open security arch2 protocol")?;
|
||||||
|
|
||||||
|
// Construct the global state to store.
|
||||||
|
let state = SecurityHookState {
|
||||||
|
original_hook: SecurityArchProtocol {
|
||||||
|
file_authentication_state: arch_protocol.file_authentication_state,
|
||||||
|
},
|
||||||
|
original_hook2: SecurityArch2Protocol {
|
||||||
|
file_authentication: arch_protocol2.file_authentication,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Acquire the lock to the global state and replace it.
|
||||||
|
let Ok(mut global_state) = GLOBAL_HOOK_STATE.lock() else {
|
||||||
|
bail!("unable to acquire global hook state lock");
|
||||||
|
};
|
||||||
|
global_state.replace(state);
|
||||||
|
|
||||||
|
// Install the hooks into the UEFI stack.
|
||||||
|
arch_protocol.file_authentication_state = Self::arch_file_authentication_state;
|
||||||
|
arch_protocol2.file_authentication = Self::arch2_file_authentication;
|
||||||
|
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Uninstalls the global security hook, if installed.
|
||||||
|
pub fn uninstall() -> Result<()> {
|
||||||
|
// Find the security arch protocol. If we can't find it, we will do nothing.
|
||||||
|
let Some(hook_arch) = utils::find_handle(&SECURITY_ARCH_GUID)
|
||||||
|
.context("unable to check security arch existence")?
|
||||||
|
else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
|
||||||
|
// Find the security arch2 protocol. If we can't find it, we will do nothing.
|
||||||
|
let Some(hook_arch2) = utils::find_handle(&SECURITY_ARCH2_GUID)
|
||||||
|
.context("unable to check security arch2 existence")?
|
||||||
|
else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
|
||||||
|
// Open the security arch protocol.
|
||||||
|
let mut arch_protocol =
|
||||||
|
uefi::boot::open_protocol_exclusive::<SecurityArchProtocol>(hook_arch)
|
||||||
|
.context("unable to open security arch protocol")?;
|
||||||
|
|
||||||
|
// Open the security arch2 protocol.
|
||||||
|
let mut arch_protocol2 =
|
||||||
|
uefi::boot::open_protocol_exclusive::<SecurityArch2Protocol>(hook_arch2)
|
||||||
|
.context("unable to open security arch2 protocol")?;
|
||||||
|
|
||||||
|
// Acquire the lock to the global state.
|
||||||
|
let Ok(mut global_state) = GLOBAL_HOOK_STATE.lock() else {
|
||||||
|
bail!("unable to acquire global hook state lock");
|
||||||
|
};
|
||||||
|
|
||||||
|
// Take the state and replace the original functions.
|
||||||
|
let Some(state) = global_state.take() else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
|
||||||
|
// Reinstall the original functions.
|
||||||
|
arch_protocol.file_authentication_state = state.original_hook.file_authentication_state;
|
||||||
|
arch_protocol2.file_authentication = state.original_hook2.file_authentication;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
@@ -72,9 +72,9 @@ pub mod utils;
|
|||||||
|
|
||||||
/// Run Sprout, returning an error if one occurs.
|
/// Run Sprout, returning an error if one occurs.
|
||||||
fn run() -> Result<()> {
|
fn run() -> Result<()> {
|
||||||
// For safety reasons, we will bail early if Secure Boot is enabled.
|
// For safety reasons, we will note that Secure Boot is in beta on Sprout.
|
||||||
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 Secure Boot is in beta.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start the platform timer.
|
// Start the platform timer.
|
||||||
|
|||||||
53
src/utils.rs
53
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,10 +104,24 @@ 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.
|
||||||
pub fn resolve_path(default_root_path: &DevicePath, input: &str) -> Result<ResolvedPath> {
|
pub fn resolve_path(default_root_path: Option<&DevicePath>, input: &str) -> Result<ResolvedPath> {
|
||||||
let mut path = text_to_device_path(input).context("unable to convert text to path")?;
|
let mut path = text_to_device_path(input).context("unable to convert text to path")?;
|
||||||
let path_has_device = path
|
let path_has_device = path
|
||||||
.node_iter()
|
.node_iter()
|
||||||
@@ -122,6 +137,9 @@ pub fn resolve_path(default_root_path: &DevicePath, input: &str) -> Result<Resol
|
|||||||
if !input.starts_with('\\') {
|
if !input.starts_with('\\') {
|
||||||
input.insert(0, '\\');
|
input.insert(0, '\\');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let default_root_path = default_root_path.context("unable to get default root path")?;
|
||||||
|
|
||||||
input.insert_str(
|
input.insert_str(
|
||||||
0,
|
0,
|
||||||
device_path_root(default_root_path)
|
device_path_root(default_root_path)
|
||||||
@@ -155,16 +173,9 @@ pub fn resolve_path(default_root_path: &DevicePath, input: &str) -> Result<Resol
|
|||||||
/// This acquires exclusive protocol access to the [SimpleFileSystem] protocol of the resolved
|
/// This acquires exclusive protocol access to the [SimpleFileSystem] protocol of the resolved
|
||||||
/// filesystem handle, so care must be taken to call this function outside a scope with
|
/// filesystem handle, so care must be taken to call this function outside a scope with
|
||||||
/// 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: Option<&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 +243,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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -90,4 +90,10 @@ impl VariableController {
|
|||||||
.collect::<Vec<u8>>();
|
.collect::<Vec<u8>>();
|
||||||
self.set(key, &encoded, class)
|
self.set(key, &encoded, class)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set a boolean variable specified by `key` to `value`, converting the value.
|
||||||
|
/// The variable `class` controls the attributes for the variable.
|
||||||
|
pub fn set_bool(&self, key: &str, value: bool) -> Result<()> {
|
||||||
|
self.set(key, &[value as u8], VariableClass::BootAndRuntimeTemporary)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user