32 Commits

Author SHA1 Message Date
4129ae4c0e sprout: version 0.0.16 2025-11-01 02:28:51 -04:00
7a7fcc71c0 fix(variables): set bool should have variable class parameter 2025-11-01 02:26:53 -04:00
812036fada chore(doc): fix incorrect comment on list generator 2025-11-01 02:22:10 -04:00
7f122b088e chore(context): add documentation to the stamping algorithm 2025-11-01 02:20:59 -04:00
5217dd0538 chore(doc): update readme 2025-11-01 02:05:08 -04:00
b94c684d52 fix(autoconfigure/linux): remove debug line 2025-11-01 02:01:09 -04:00
fd2e9df3f1 fix(autoconfigure): detect kernel and initramfs case-insensitive, even at the root 2025-11-01 01:58:55 -04:00
f49bbed0d5 fix(utils): for safety, ensure that the root path is not modifiable by the uefi stack 2025-11-01 01:20:45 -04:00
b0081ef9f3 chore(options): fix incorrect comment about values 2025-11-01 01:11:02 -04:00
d9c0dc915d chore(sbat): add note about alignment of sbat.csv 2025-11-01 01:10:27 -04:00
0bee93b607 fix(shim): handle hook uninstallation more gracefully 2025-11-01 01:07:37 -04:00
eace74a6b0 fix(tpm): correct comment about the format of the description data 2025-11-01 00:54:51 -04:00
d1936f7db4 fix(sbat): add newline to template 2025-10-31 15:50:01 -04:00
4866961d2f feat(secure-boot): add support for SBAT section 2025-10-31 15:49:00 -04:00
bbc8f58352 fix(shim): retain the protocol if the shim is loaded at all 2025-10-31 14:56:26 -04:00
b3424fcd8f fix(tpm): correctly write the log name, and change the sprout configuration event name 2025-10-31 02:45:15 -04:00
afc650f944 feat(tpm): implement basic measurement of the bootloader configuration 2025-10-31 02:35:58 -04:00
81cf331158 feat(tpm): initial tpm support code, we just tell systemd about the pcr banks right now 2025-10-31 01:30:07 -04:00
6602e1d69e fix(bootloader-interface): use the correct uefi revision and firmware revision format 2025-10-30 23:58:07 -04:00
7bd93f5aa0 fix(platform/timer): ensure the x86_64 frequency measurement uses wrapping subtraction 2025-10-30 23:51:20 -04:00
f897addc3c fix(filesystem-device-match): has-partition-type-uuid should fetch the partition type guid 2025-10-30 23:48:48 -04:00
8241d6d774 fix(shim/hook): create an immutable slice for the buffer instead of a mutable one 2025-10-30 23:45:08 -04:00
c3e883c121 fix(utils): when retrieving the partition guid, if the guid is zero, return none 2025-10-30 23:42:47 -04:00
f69d4b942b fix(platform/timer): use wrapping subtraction to measure duration of a timer 2025-10-30 23:40:52 -04:00
c1a672afcb fix(bootloader-interface): report the correct firmware revision 2025-10-30 23:25:48 -04:00
a2f017ba30 fix(variables): add null terminator to the end of strings written into variables 2025-10-30 23:15:18 -04:00
0368a170a8 Merge pull request #25 from edera-dev/azenla/shim-support
feat(boot): basic support for secure boot via shim
2025-10-30 23:04:55 -04:00
f593f5a601 feat(boot): basic support for secure boot via shim protocol 2025-10-30 22:56:01 -04:00
92f611e9a8 feat(shim): initial shim support 2025-10-30 21:38:49 -04:00
20932695e3 feat(safety): bail if secure boot is enabled early 2025-10-30 18:57:26 -04:00
40e2d1baef fix(bootloader-interface): autoconfigure should produce auto-* entries to match spec 2025-10-30 15:31:26 -04:00
94caf123ae chore(main): add constant for delay on error 2025-10-30 15:26:44 -04:00
36 changed files with 1078 additions and 131 deletions

2
Cargo.lock generated
View File

@@ -116,7 +116,7 @@ dependencies = [
[[package]] [[package]]
name = "edera-sprout" name = "edera-sprout"
version = "0.0.15" version = "0.0.16"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"image", "image",

View File

@@ -2,7 +2,7 @@
name = "edera-sprout" name = "edera-sprout"
description = "Modern UEFI bootloader" description = "Modern UEFI bootloader"
license = "Apache-2.0" license = "Apache-2.0"
version = "0.0.15" version = "0.0.16"
homepage = "https://sprout.edera.dev" homepage = "https://sprout.edera.dev"
repository = "https://github.com/edera-dev/sprout" repository = "https://github.com/edera-dev/sprout"
edition = "2024" edition = "2024"

View File

@@ -6,11 +6,11 @@
</div> </div>
Sprout is an **EXPERIMENTAL** programmable UEFI bootloader written in Rust. Sprout is a programmable UEFI bootloader written in Rust.
Sprout is in use at Edera today in development environments and is intended to ship to production soon. It is in use at Edera today in development environments and is intended to ship to production soon.
The name "sprout" is derived from our company name "Edera" which means "ivy." The name "Sprout" is derived from our company name "Edera" which means "ivy."
Given that Sprout is the first thing intended to start on an Edera system, the name was apt. Given that Sprout is the first thing intended to start on an Edera system, the name was apt.
It supports `x86_64` and `ARM64` EFI-capable systems. It is designed to require UEFI and can be chainloaded from an It supports `x86_64` and `ARM64` EFI-capable systems. It is designed to require UEFI and can be chainloaded from an
@@ -18,16 +18,13 @@ 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.
See [this issue](https://github.com/edera-dev/sprout/issues/20) for updates.
## Background ## Background
At [Edera] we make compute isolation technology for a wide variety of environments, often ones we do not fully control. At [Edera] we make compute isolation technology for a wide variety of environments, often ones we do not fully control.
Our technology utilizes a hypervisor to boot the host system to provide a new isolation mechanism that works Our technology uses a hypervisor to boot the host system to provide a new isolation mechanism that works
with or without hardware virtualization support. To do this we need to inject our hypervisor at boot time. with or without hardware virtualization support. To do this, we need to inject our hypervisor at boot time.
Unfortunately, GRUB, the most common bootloader on Linux systems today, utilizes a shell-script like Unfortunately, GRUB, the most common bootloader on Linux systems today, uses a shell-script like
configuration system. Both the code that runs to generate a GRUB config and the GRUB config configuration system. Both the code that runs to generate a GRUB config and the GRUB config
itself is fully turing complete. This makes modifying boot configuration difficult and error-prone. itself is fully turing complete. This makes modifying boot configuration difficult and error-prone.
@@ -52,8 +49,7 @@ simplify installation and usage.
## Features ## Features
NOTE: Currently, Sprout is experimental and is not intended for production use. **NOTE**: Sprout is still in beta.
The boot menu mechanism is very rudimentary.
### Current ### Current
@@ -65,13 +61,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)

57
build.rs Normal file
View File

@@ -0,0 +1,57 @@
use std::path::PathBuf;
use std::{env, fs};
/// The size of the sbat.csv file.
const SBAT_SIZE: usize = 512;
/// Generate the sbat.csv for the .sbat link section.
///
/// We intake a sbat.template.tsv and output a sbat.csv which is included by src/sbat.rs
fn generate_sbat_csv() {
// Notify Cargo that if the Sprout version changes, we need to regenerate the sbat.csv.
println!("cargo:rerun-if-env-changed=CARGO_PKG_VERSION");
// The version of the sprout crate.
let sprout_version = env::var("CARGO_PKG_VERSION").expect("CARGO_PKG_VERSION not set");
// The output directory to place the sbat.csv into.
let output_dir = PathBuf::from(env::var("OUT_DIR").expect("OUT_DIR not set"));
// The output path to the sbat.csv.
let output_file = output_dir.join("sbat.csv");
// The path to the root of the sprout crate.
let sprout_root =
PathBuf::from(env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR not set"));
// The path to the sbat.template.tsv file is in the source directory of the sprout crate.
let template_path = sprout_root.join("src/sbat.template.csv");
// Read the sbat.csv template file.
let template = fs::read_to_string(&template_path).expect("unable to read template file");
// Replace the version placeholder in the template with the actual version.
let sbat = template.replace("{version}", &sprout_version);
// Encode the sbat.csv as bytes.
let mut encoded = sbat.as_bytes().to_vec();
if encoded.len() > SBAT_SIZE {
panic!("sbat.csv is too large");
}
// Pad the sbat.csv to the required size.
while encoded.len() < SBAT_SIZE {
encoded.push(0);
}
// Write the sbat.csv to the output directory.
fs::write(&output_file, encoded).expect("unable to write sbat.csv");
}
/// Build script entry point.
/// Right now, all we need to do is generate the sbat.csv file.
fn main() {
// Generate the sbat.csv file.
generate_sbat_csv();
}

View File

@@ -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

View File

@@ -7,7 +7,7 @@ RUN export DEBIAN_FRONTEND=noninteractive && apt-get update && apt-get install -
WORKDIR /work WORKDIR /work
COPY sprout.efi /work/${EFI_NAME}.EFI COPY sprout.efi /work/${EFI_NAME}.EFI
COPY sprout.toml /work/SPROUT.TOML COPY sprout.toml /work/SPROUT.TOML
COPY kernel.efi /work/KERNEL.EFI COPY kernel.efi /work/VMLINUZ
COPY shell.efi /work/SHELL.EFI COPY shell.efi /work/SHELL.EFI
COPY xen.efi /work/XEN.EFI COPY xen.efi /work/XEN.EFI
COPY xen.cfg /work/XEN.CFG COPY xen.cfg /work/XEN.CFG
@@ -24,7 +24,7 @@ RUN truncate -s128MiB sprout.img && \
mmd -i sprout.img ::/LOADER && \ mmd -i sprout.img ::/LOADER && \
mmd -i sprout.img ::/LOADER/ENTRIES && \ mmd -i sprout.img ::/LOADER/ENTRIES && \
mcopy -i sprout.img ${EFI_NAME}.EFI ::/EFI/BOOT/ && \ mcopy -i sprout.img ${EFI_NAME}.EFI ::/EFI/BOOT/ && \
mcopy -i sprout.img KERNEL.EFI ::/EFI/BOOT/ && \ mcopy -i sprout.img VMLINUZ ::/VMLINUZ && \
mcopy -i sprout.img SHELL.EFI ::/EFI/BOOT/ && \ mcopy -i sprout.img SHELL.EFI ::/EFI/BOOT/ && \
mcopy -i sprout.img XEN.EFI ::/EFI/BOOT/ && \ mcopy -i sprout.img XEN.EFI ::/EFI/BOOT/ && \
mcopy -i sprout.img XEN.CFG ::/EFI/BOOT/ && \ mcopy -i sprout.img XEN.CFG ::/EFI/BOOT/ && \

View File

@@ -4,10 +4,10 @@ version = 1
default-entry = "kernel" default-entry = "kernel"
[extractors.boot.filesystem-device-match] [extractors.boot.filesystem-device-match]
has-item = "\\EFI\\BOOT\\kernel.efi" has-item = "\\vmlinuz"
[actions.chainload-kernel] [actions.chainload-kernel]
chainload.path = "$boot\\EFI\\BOOT\\kernel.efi" chainload.path = "$boot\\vmlinuz"
chainload.options = ["console=hvc0"] chainload.options = ["console=hvc0"]
chainload.linux-initrd = "$boot\\initramfs" chainload.linux-initrd = "$boot\\initramfs"

View File

@@ -1,4 +1,4 @@
title Boot Linux title Boot Linux
linux /efi/boot/kernel.efi linux /vmlinuz
options console=hvc0 options console=hvc0
initrd /initramfs initrd /initramfs

View File

@@ -5,10 +5,10 @@ default-entry = "kernel"
menu-timeout = 0 menu-timeout = 0
[extractors.boot.filesystem-device-match] [extractors.boot.filesystem-device-match]
has-item = "\\EFI\\BOOT\\kernel.efi" has-item = "\\vmlinuz"
[actions.chainload-kernel] [actions.chainload-kernel]
chainload.path = "$boot\\EFI\\BOOT\\kernel.efi" chainload.path = "$boot\\vmlinuz"
chainload.options = ["console=hvc0"] chainload.options = ["console=hvc0"]
chainload.linux-initrd = "$boot\\initramfs" chainload.linux-initrd = "$boot\\initramfs"

View File

@@ -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,8 +90,9 @@ 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 =
.context("unable to read linux initrd")?; utils::read_file_contents(Some(context.root().loaded_image_path()?), &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())
.context("unable to register linux initrd")?; .context("unable to register linux initrd")?;

View File

@@ -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())

View File

@@ -12,6 +12,8 @@ use std::time::Duration;
use uefi::boot::ScopedProtocol; use uefi::boot::ScopedProtocol;
use uefi::proto::console::gop::GraphicsOutput; use uefi::proto::console::gop::GraphicsOutput;
/// We set the default splash time to zero, as this makes it so any logging shows up
/// on top of the splash and does not hold up the boot process.
const DEFAULT_SPLASH_TIME: u32 = 0; const DEFAULT_SPLASH_TIME: u32 = 0;
/// The configuration of the splash action. /// The configuration of the splash action.
@@ -143,7 +145,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()

View File

@@ -72,7 +72,7 @@ pub fn scan(
// Generate a unique name for the BLS generator and insert the generator into the configuration. // Generate a unique name for the BLS generator and insert the generator into the configuration.
config.generators.insert( config.generators.insert(
format!("autoconfigure-bls-{}", root_unique_hash), format!("auto-bls-{}", root_unique_hash),
GeneratorDeclaration { GeneratorDeclaration {
bls: Some(generator), bls: Some(generator),
..Default::default() ..Default::default()

View File

@@ -8,7 +8,7 @@ use crate::utils;
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use std::collections::BTreeMap; use std::collections::BTreeMap;
use uefi::CString16; use uefi::CString16;
use uefi::fs::{FileSystem, Path}; use uefi::fs::{FileSystem, Path, PathBuf};
use uefi::proto::device_path::DevicePath; use uefi::proto::device_path::DevicePath;
use uefi::proto::device_path::text::{AllowShortcuts, DisplayOnly}; use uefi::proto::device_path::text::{AllowShortcuts, DisplayOnly};
@@ -17,7 +17,8 @@ const LINUX_CHAINLOAD_ACTION_PREFIX: &str = "linux-chainload-";
/// The locations to scan for kernel pairs. /// The locations to scan for kernel pairs.
/// We will check for symlinks and if this directory is a symlink, we will skip it. /// We will check for symlinks and if this directory is a symlink, we will skip it.
const SCAN_LOCATIONS: &[&str] = &["/boot", "/"]; /// The empty string represents the root of the filesystem.
const SCAN_LOCATIONS: &[&str] = &["\\boot", "\\"];
/// Prefixes of kernel files to scan for. /// Prefixes of kernel files to scan for.
const KERNEL_PREFIXES: &[&str] = &["vmlinuz"]; const KERNEL_PREFIXES: &[&str] = &["vmlinuz"];
@@ -39,6 +40,9 @@ fn scan_directory(filesystem: &mut FileSystem, path: &str) -> Result<Vec<KernelP
// All the discovered kernel pairs. // All the discovered kernel pairs.
let mut pairs = Vec::new(); let mut pairs = Vec::new();
// We have to special-case the root directory due to path logic in the uefi crate.
let is_root = path.is_empty() || path == "\\";
// Construct a filesystem path from the path string. // Construct a filesystem path from the path string.
let path = CString16::try_from(path).context("unable to convert path to CString16")?; let path = CString16::try_from(path).context("unable to convert path to CString16")?;
let path = Path::new(&path); let path = Path::new(&path);
@@ -62,6 +66,16 @@ fn scan_directory(filesystem: &mut FileSystem, path: &str) -> Result<Vec<KernelP
return Ok(pairs); return Ok(pairs);
}; };
// Create a new path used for joining file names below.
// All attempts to derive paths for the files in the directory should use this instead.
// The uefi crate does not handle push correctly for the root directory.
// It will add a second slash, which will cause our path logic to fail.
let path_for_join = if is_root {
PathBuf::new()
} else {
path.clone()
};
// For each item in the directory, find a kernel. // For each item in the directory, find a kernel.
for item in directory { for item in directory {
let item = item.context("unable to read directory item")?; let item = item.context("unable to read directory item")?;
@@ -74,11 +88,14 @@ fn scan_directory(filesystem: &mut FileSystem, path: &str) -> Result<Vec<KernelP
// Convert the name from a CString16 to a String. // Convert the name from a CString16 to a String.
let name = item.file_name().to_string(); let name = item.file_name().to_string();
// Convert the name to lowercase to make all of this case-insensitive.
let name_for_match = name.to_lowercase();
// Find a kernel prefix that matches, if any. // Find a kernel prefix that matches, if any.
let Some(prefix) = KERNEL_PREFIXES // This is case-insensitive to ensure we pick up all possibilities.
.iter() let Some(prefix) = KERNEL_PREFIXES.iter().find(|prefix| {
.find(|prefix| name == **prefix || name.starts_with(&format!("{}-", prefix))) name_for_match == **prefix || name_for_match.starts_with(&format!("{}-", prefix))
else { }) else {
// Skip over anything that doesn't match a kernel prefix. // Skip over anything that doesn't match a kernel prefix.
continue; continue;
}; };
@@ -96,8 +113,9 @@ fn scan_directory(filesystem: &mut FileSystem, path: &str) -> Result<Vec<KernelP
let initramfs = format!("{}{}", prefix, suffix); let initramfs = format!("{}{}", prefix, suffix);
let initramfs = CString16::try_from(initramfs.as_str()) let initramfs = CString16::try_from(initramfs.as_str())
.context("unable to convert initramfs name to CString16")?; .context("unable to convert initramfs name to CString16")?;
let mut initramfs_path = path.clone(); let mut initramfs_path = path_for_join.clone();
initramfs_path.push(Path::new(&initramfs)); initramfs_path.push(Path::new(&initramfs));
// Check if the initramfs path exists, if it does, break out of the loop. // Check if the initramfs path exists, if it does, break out of the loop.
if filesystem if filesystem
.try_exists(&initramfs_path) .try_exists(&initramfs_path)
@@ -108,7 +126,7 @@ fn scan_directory(filesystem: &mut FileSystem, path: &str) -> Result<Vec<KernelP
}; };
// Construct a kernel path from the kernel name. // Construct a kernel path from the kernel name.
let mut kernel = path.clone(); let mut kernel = path_for_join.clone();
kernel.push(Path::new(&item.file_name())); kernel.push(Path::new(&item.file_name()));
let kernel = kernel.to_string(); let kernel = kernel.to_string();
let initramfs = matched_initramfs_path.map(|initramfs_path| initramfs_path.to_string()); let initramfs = matched_initramfs_path.map(|initramfs_path| initramfs_path.to_string());
@@ -181,7 +199,7 @@ pub fn scan(
// Generate a unique name for the Linux generator and insert the generator into the configuration. // Generate a unique name for the Linux generator and insert the generator into the configuration.
config.generators.insert( config.generators.insert(
format!("autoconfigure-linux-{}", root_unique_hash), format!("auto-linux-{}", root_unique_hash),
GeneratorDeclaration { GeneratorDeclaration {
list: Some(generator), list: Some(generator),
..Default::default() ..Default::default()

View File

@@ -49,7 +49,7 @@ pub fn scan(
let chainload_action_name = format!("{}{}", WINDOWS_CHAINLOAD_ACTION_PREFIX, root_unique_hash,); let chainload_action_name = format!("{}{}", WINDOWS_CHAINLOAD_ACTION_PREFIX, root_unique_hash,);
// Generate an entry name for Windows. // Generate an entry name for Windows.
let entry_name = format!("autoconfigure-windows-{}", root_unique_hash,); let entry_name = format!("auto-windows-{}", root_unique_hash,);
// Create an entry for Windows and insert it into the configuration. // Create an entry for Windows and insert it into the configuration.
let entry = EntryDeclaration { let entry = EntryDeclaration {

View File

@@ -1,5 +1,6 @@
use crate::config::{RootConfiguration, latest_version}; use crate::config::{RootConfiguration, latest_version};
use crate::options::SproutOptions; use crate::options::SproutOptions;
use crate::platform::tpm::PlatformTpm;
use crate::utils; use crate::utils;
use anyhow::{Context, Result, bail}; use anyhow::{Context, Result, bail};
use log::info; use log::info;
@@ -19,8 +20,17 @@ 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")?;
// Measure the sprout.toml into the TPM, if needed and possible.
PlatformTpm::log_event(
PlatformTpm::PCR_BOOT_LOADER_CONFIG,
&content,
"sprout: configuration file",
)
.context("unable to measure the sprout.toml file into the TPM")?;
// Return the contents of the sprout config file. // Return the contents of the sprout config file.
Ok(content) Ok(content)
} }

View File

@@ -219,6 +219,14 @@ impl SproutContext {
/// Stamps the `text` value with the specified `values` map. The returned value indicates /// Stamps the `text` value with the specified `values` map. The returned value indicates
/// whether the `text` has been changed and the value that was stamped and changed. /// whether the `text` has been changed and the value that was stamped and changed.
///
/// Stamping works like this:
/// - Start with the input text.
/// - Sort all the keys in reverse length order (longest keys first)
/// - For each key, if the key is not empty, replace $KEY in the text.
/// - Each follow-up iteration acts upon the last iterations result.
/// - We keep track if the text changes during the replacement.
/// - We return both whether the text changed during any iteration and the final result.
fn stamp_values(values: &BTreeMap<String, String>, text: impl AsRef<str>) -> (bool, String) { fn stamp_values(values: &BTreeMap<String, String>, text: impl AsRef<str>) -> (bool, String) {
let mut result = text.as_ref().to_string(); let mut result = text.as_ref().to_string();
let mut did_change = false; let mut did_change = false;

View File

@@ -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

View File

@@ -98,9 +98,9 @@ pub fn extract(
.deref() .deref()
.to_boxed(); .to_boxed();
// Fetch the partition uuid for this filesystem. // Fetch the partition type uuid for this filesystem.
let partition_type_uuid = let partition_type_uuid =
utils::partition_guid(&root, utils::PartitionGuidForm::Partition) utils::partition_guid(&root, utils::PartitionGuidForm::PartitionType)
.context("unable to fetch the partition uuid of the filesystem")?; .context("unable to fetch the partition uuid of the filesystem")?;
// Compare the partition type uuid to the parsed uuid. // Compare the partition type uuid to the parsed uuid.
// If it does not match, continue to the next filesystem. // If it does not match, continue to the next filesystem.

View File

@@ -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.

View File

@@ -18,7 +18,7 @@ pub struct ListConfiguration {
pub values: Vec<BTreeMap<String, String>>, pub values: Vec<BTreeMap<String, String>>,
} }
/// Generates a set of entries using the specified `matrix` configuration in the `context`. /// Generates a set of entries using the specified `list` configuration in the `context`.
pub fn generate( pub fn generate(
context: Rc<SproutContext>, context: Rc<SproutContext>,
list: &ListConfiguration, list: &ListConfiguration,

View File

@@ -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;

View File

@@ -1,9 +1,10 @@
use crate::platform::timer::PlatformTimer; use crate::platform::timer::PlatformTimer;
use crate::utils::device_path_subpath; use crate::utils::device_path_subpath;
use crate::utils::variables::{VariableClass, VariableController};
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use uefi::proto::device_path::DevicePath; use uefi::proto::device_path::DevicePath;
use uefi::{CString16, Guid, guid}; use uefi::{Guid, guid};
use uefi_raw::table::runtime::{VariableAttributes, VariableVendor}; use uefi_raw::table::runtime::VariableVendor;
/// The name of the bootloader to tell the system. /// The name of the bootloader to tell the system.
const LOADER_NAME: &str = "Sprout"; const LOADER_NAME: &str = "Sprout";
@@ -13,7 +14,9 @@ pub struct BootloaderInterface;
impl BootloaderInterface { impl BootloaderInterface {
/// Bootloader Interface GUID from https://systemd.io/BOOT_LOADER_INTERFACE /// Bootloader Interface GUID from https://systemd.io/BOOT_LOADER_INTERFACE
const VENDOR: VariableVendor = VariableVendor(guid!("4a67b082-0a4c-41cf-b6c7-440b29bb8c4f")); const VENDOR: VariableController = VariableController::new(VariableVendor(guid!(
"4a67b082-0a4c-41cf-b6c7-440b29bb8c4f"
)));
/// Tell the system that Sprout was initialized at the current time. /// Tell the system that Sprout was initialized at the current time.
pub fn mark_init(timer: &PlatformTimer) -> Result<()> { pub fn mark_init(timer: &PlatformTimer) -> Result<()> {
@@ -35,23 +38,39 @@ impl BootloaderInterface {
fn mark_time(key: &str, timer: &PlatformTimer) -> Result<()> { fn mark_time(key: &str, timer: &PlatformTimer) -> Result<()> {
// Measure the elapsed time since the hardware timer was started. // Measure the elapsed time since the hardware timer was started.
let elapsed = timer.elapsed_since_lifetime(); let elapsed = timer.elapsed_since_lifetime();
Self::set_cstr16(key, &elapsed.as_micros().to_string()) Self::VENDOR.set_cstr16(
key,
&elapsed.as_micros().to_string(),
VariableClass::BootAndRuntimeTemporary,
)
} }
/// Tell the system what loader is being used. /// Tell the system what loader is being used.
pub fn set_loader_info() -> Result<()> { pub fn set_loader_info() -> Result<()> {
Self::set_cstr16("LoaderInfo", LOADER_NAME) Self::VENDOR.set_cstr16(
"LoaderInfo",
LOADER_NAME,
VariableClass::BootAndRuntimeTemporary,
)
} }
/// Tell the system the relative path to the partition root of the current bootloader. /// Tell the system the relative path to the partition root of the current bootloader.
pub fn set_loader_path(path: &DevicePath) -> Result<()> { pub fn set_loader_path(path: &DevicePath) -> Result<()> {
let subpath = device_path_subpath(path).context("unable to get loader path subpath")?; let subpath = device_path_subpath(path).context("unable to get loader path subpath")?;
Self::set_cstr16("LoaderImageIdentifier", &subpath) Self::VENDOR.set_cstr16(
"LoaderImageIdentifier",
&subpath,
VariableClass::BootAndRuntimeTemporary,
)
} }
/// Tell the system what the partition GUID of the ESP Sprout was booted from is. /// Tell the system what the partition GUID of the ESP Sprout was booted from is.
pub fn set_partition_guid(guid: &Guid) -> Result<()> { pub fn set_partition_guid(guid: &Guid) -> Result<()> {
Self::set_cstr16("LoaderDevicePartUUID", &guid.to_string()) Self::VENDOR.set_cstr16(
"LoaderDevicePartUUID",
&guid.to_string(),
VariableClass::BootAndRuntimeTemporary,
)
} }
/// Tell the system what boot entries are available. /// Tell the system what boot entries are available.
@@ -66,60 +85,79 @@ impl BootloaderInterface {
.encode_utf16() .encode_utf16()
.flat_map(|c| c.to_le_bytes()) .flat_map(|c| c.to_le_bytes())
.collect::<Vec<u8>>(); .collect::<Vec<u8>>();
// Write the bytes (including the null terminator) into the data buffer. // Write the bytes into the data buffer.
data.extend_from_slice(&encoded); data.extend_from_slice(&encoded);
// Add a null terminator to the end of the entry.
data.extend_from_slice(&[0, 0]);
} }
Self::set("LoaderEntries", &data) Self::VENDOR.set(
"LoaderEntries",
&data,
VariableClass::BootAndRuntimeTemporary,
)
} }
/// Tell the system what the default boot entry is. /// Tell the system what the default boot entry is.
pub fn set_default_entry(entry: String) -> Result<()> { pub fn set_default_entry(entry: String) -> Result<()> {
Self::set_cstr16("LoaderEntryDefault", &entry) Self::VENDOR.set_cstr16(
"LoaderEntryDefault",
&entry,
VariableClass::BootAndRuntimeTemporary,
)
} }
/// Tell the system what the selected boot entry is. /// Tell the system what the selected boot entry is.
pub fn set_selected_entry(entry: String) -> Result<()> { pub fn set_selected_entry(entry: String) -> Result<()> {
Self::set_cstr16("LoaderEntrySelected", &entry) Self::VENDOR.set_cstr16(
"LoaderEntrySelected",
&entry,
VariableClass::BootAndRuntimeTemporary,
)
} }
/// Tell the system about the UEFI firmware we are running on. /// Tell the system about the UEFI firmware we are running on.
pub fn set_firmware_info() -> Result<()> { pub fn set_firmware_info() -> Result<()> {
// Access the firmware revision.
let firmware_revision = uefi::system::firmware_revision();
// Access the UEFI revision.
let uefi_revision = uefi::system::uefi_revision();
// Format the firmware information string into something human-readable. // Format the firmware information string into something human-readable.
let firmware_info = format!( let firmware_info = format!(
"{} {}.{:02}", "{} {}.{:02}",
uefi::system::firmware_vendor(), uefi::system::firmware_vendor(),
uefi::system::firmware_revision() >> 16, firmware_revision >> 16,
uefi::system::firmware_revision() & 0xFFFFF, firmware_revision & 0xffff,
); );
Self::set_cstr16("LoaderFirmwareInfo", &firmware_info)?; Self::VENDOR.set_cstr16(
"LoaderFirmwareInfo",
&firmware_info,
VariableClass::BootAndRuntimeTemporary,
)?;
// Format the firmware revision into something human-readable. // Format the firmware revision into something human-readable.
let firmware_type = format!("UEFI {:02}", uefi::system::firmware_revision()); let firmware_type = format!(
Self::set_cstr16("LoaderFirmwareType", &firmware_type) "UEFI {}.{:02}",
uefi_revision.major(),
uefi_revision.minor()
);
Self::VENDOR.set_cstr16(
"LoaderFirmwareType",
&firmware_type,
VariableClass::BootAndRuntimeTemporary,
)
} }
/// The [VariableAttributes] for bootloader interface variables. /// Tell the system what the number of active PCR banks is.
fn attributes() -> VariableAttributes { /// If this is zero, that is okay.
VariableAttributes::BOOTSERVICE_ACCESS | VariableAttributes::RUNTIME_ACCESS pub fn set_tpm2_active_pcr_banks(value: u32) -> Result<()> {
} // Format the value into the specification format.
let value = format!("0x{:08x}", value);
/// Set a bootloader interface variable specified by `key` to `value`. Self::VENDOR.set_cstr16(
fn set(key: &str, value: &[u8]) -> Result<()> { "LoaderTpm2ActivePcrBanks",
let name = &value,
CString16::try_from(key).context("unable to convert variable name to CString16")?; VariableClass::BootAndRuntimeTemporary,
uefi::runtime::set_variable(&name, &Self::VENDOR, Self::attributes(), value) )
.with_context(|| format!("unable to set efi variable {}", key))?;
Ok(())
}
/// Set a bootloader interface variable specified by `key` to `value`, converting the value to
/// a [CString16].
fn set_cstr16(key: &str, value: &str) -> Result<()> {
// Encode the value as a CString16 little endian.
let encoded = value
.encode_utf16()
.flat_map(|c| c.to_le_bytes())
.collect::<Vec<u8>>();
Self::set(key, &encoded)
} }
} }

293
src/integrations/shim.rs Normal file
View File

@@ -0,0 +1,293 @@
use crate::integrations::shim::hook::SecurityHook;
use crate::utils;
use crate::utils::ResolvedPath;
use crate::utils::variables::{VariableClass, VariableController};
use anyhow::{Context, Result, anyhow, bail};
use log::warn;
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 the shim verify function is
// guaranteed to be defined by the environment if we are able to acquire the protocol.
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 required for this platform");
}
}
// If the shim is loaded, we will need to retain the shim protocol to allow
// loading multiple images.
if shim_loaded {
// Retain the shim protocol after loading the image.
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 {
let uninstall_result = SecurityHook::uninstall();
// Ensure we don't mask load image errors if uninstalling fails.
if result.is_err()
&& let Err(uninstall_error) = &uninstall_result
{
// Warn on the error since the load image error is more important.
warn!("unable to uninstall security hook: {}", uninstall_error);
} else {
// Otherwise, ensure we handle the original uninstallation result.
uninstall_result?;
}
}
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,
VariableClass::BootAndRuntimeTemporary,
)
.context("unable to retain shim protocol")?;
Ok(())
}
}

View 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(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(())
}
}

View File

@@ -2,6 +2,9 @@
#![feature(uefi_std)] #![feature(uefi_std)]
extern crate core; extern crate core;
/// The delay to wait for when an error occurs in Sprout.
const DELAY_ON_ERROR: Duration = Duration::from_secs(10);
use crate::config::RootConfiguration; use crate::config::RootConfiguration;
use crate::context::{RootContext, SproutContext}; use crate::context::{RootContext, SproutContext};
use crate::entries::BootableEntry; use crate::entries::BootableEntry;
@@ -10,9 +13,11 @@ use crate::options::SproutOptions;
use crate::options::parser::OptionsRepresentable; use crate::options::parser::OptionsRepresentable;
use crate::phases::phase; use crate::phases::phase;
use crate::platform::timer::PlatformTimer; use crate::platform::timer::PlatformTimer;
use crate::platform::tpm::PlatformTpm;
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;
@@ -54,6 +59,12 @@ pub mod integrations;
/// phases: Hooks into specific parts of the boot process. /// phases: Hooks into specific parts of the boot process.
pub mod phases; pub mod phases;
/// sbat: Secure Boot Attestation section.
pub mod sbat;
/// secure: Secure Boot support.
pub mod secure;
/// setup: Code that initializes the UEFI environment for Sprout. /// setup: Code that initializes the UEFI environment for Sprout.
pub mod setup; pub mod setup;
@@ -65,6 +76,11 @@ 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 note that Secure Boot is in beta on Sprout.
if SecureBoot::enabled().context("unable to determine Secure Boot status")? {
warn!("Secure Boot is enabled. Sprout Secure Boot is in beta.");
}
// Start the platform timer. // Start the platform timer.
let timer = PlatformTimer::start(); let timer = PlatformTimer::start();
@@ -80,6 +96,13 @@ fn run() -> Result<()> {
BootloaderInterface::set_loader_info() BootloaderInterface::set_loader_info()
.context("unable to set loader info in bootloader interface")?; .context("unable to set loader info in bootloader interface")?;
// Acquire the number of active PCR banks on the TPM.
// If no TPM is available, this will return zero.
let active_pcr_banks = PlatformTpm::active_pcr_banks()?;
// Tell the bootloader interface what the number of active PCR banks is.
BootloaderInterface::set_tpm2_active_pcr_banks(active_pcr_banks)
.context("unable to set tpm2 active PCR banks in bootloader interface")?;
// Parse the options to the sprout executable. // Parse the options to the sprout executable.
let options = SproutOptions::parse().context("unable to parse options")?; let options = SproutOptions::parse().context("unable to parse options")?;
@@ -318,8 +341,8 @@ fn main() -> Result<()> {
for (index, stack) in error.chain().enumerate() { for (index, stack) in error.chain().enumerate() {
error!("[{}]: {}", index, stack); error!("[{}]: {}", index, stack);
} }
// Sleep for 10 seconds to allow the user to read the error. // Sleep to allow the user to read the error.
uefi::boot::stall(Duration::from_secs(10)); uefi::boot::stall(DELAY_ON_ERROR);
} }
// Sprout doesn't necessarily guarantee anything was booted. // Sprout doesn't necessarily guarantee anything was booted.

View File

@@ -96,7 +96,7 @@ pub trait OptionsRepresentable {
let maybe_next = iterator.peek(); let maybe_next = iterator.peek();
// If the next value isn't another option, set the value to the next value. // If the next value isn't another option, set the value to the next value.
// Otherwise, it is an empty string. // Otherwise, it is None.
value = if let Some(next) = maybe_next value = if let Some(next) = maybe_next
&& !next.starts_with("--") && !next.starts_with("--")
{ {

View File

@@ -1,2 +1,4 @@
/// timer: Platform timer support. /// timer: Platform timer support.
pub mod timer; pub mod timer;
/// tpm: Platform TPM support.
pub mod tpm;

View File

@@ -83,7 +83,7 @@ impl PlatformTimer {
/// Measure the elapsed duration since the timer was started. /// Measure the elapsed duration since the timer was started.
pub fn elapsed_since_start(&self) -> Duration { pub fn elapsed_since_start(&self) -> Duration {
let duration = arch_ticks() - self.start; let duration = arch_ticks().wrapping_sub(self.start);
self.frequency.duration(duration) self.frequency.duration(duration)
} }
} }

View File

@@ -54,7 +54,7 @@ fn measure_frequency(duration: &Duration) -> u64 {
let start = start(); let start = start();
uefi::boot::stall(*duration); uefi::boot::stall(*duration);
let stop = stop(); let stop = stop();
let elapsed = (stop - start) as f64; let elapsed = stop.wrapping_sub(start) as f64;
(elapsed / duration.as_secs_f64()) as u64 (elapsed / duration.as_secs_f64()) as u64
} }

128
src/platform/tpm.rs Normal file
View File

@@ -0,0 +1,128 @@
use crate::utils;
use anyhow::{Context, Result};
use uefi::ResultExt;
use uefi::boot::ScopedProtocol;
use uefi::proto::tcg::PcrIndex;
use uefi::proto::tcg::v2::{PcrEventInputs, Tcg};
use uefi_raw::protocol::tcg::EventType;
use uefi_raw::protocol::tcg::v2::{Tcg2HashLogExtendEventFlags, Tcg2Protocol, Tcg2Version};
/// Represents the platform TPM.
pub struct PlatformTpm;
/// Represents an open TPM handle.
pub struct TpmProtocolHandle {
/// The version of the TPM protocol.
version: Tcg2Version,
/// The protocol itself.
protocol: ScopedProtocol<Tcg>,
}
impl TpmProtocolHandle {
/// Construct a new [TpmProtocolHandle] from the `version` and `protocol`.
pub fn new(version: Tcg2Version, protocol: ScopedProtocol<Tcg>) -> Self {
Self { version, protocol }
}
/// Access the version provided by the tcg2 protocol.
pub fn version(&self) -> Tcg2Version {
self.version
}
/// Access the protocol interface for tcg2.
pub fn protocol(&mut self) -> &mut ScopedProtocol<Tcg> {
&mut self.protocol
}
}
impl PlatformTpm {
/// The PCR for measuring the bootloader configuration into.
pub const PCR_BOOT_LOADER_CONFIG: PcrIndex = PcrIndex(5);
/// Acquire access to the TPM protocol handle, if possible.
/// Returns None if TPM is not available.
fn protocol() -> Result<Option<TpmProtocolHandle>> {
// Attempt to acquire the TCG2 protocol handle. If it's not available, return None.
let Some(handle) =
utils::find_handle(&Tcg2Protocol::GUID).context("unable to determine tpm presence")?
else {
return Ok(None);
};
// If we reach here, we've already validated that the handle
// implements the TCG2 protocol.
let mut protocol = uefi::boot::open_protocol_exclusive::<Tcg>(handle)
.context("unable to open tcg2 protocol")?;
// Acquire the capabilities of the TPM.
let capability = protocol
.get_capability()
.context("unable to get tcg2 boot service capability")?;
// If the TPM is not present, return None.
if !capability.tpm_present() {
return Ok(None);
}
// If the TPM is present, we need to determine the version of the TPM.
let version = capability.protocol_version;
// We have a TPM, so return the protocol version and the protocol handle.
Ok(Some(TpmProtocolHandle::new(version, protocol)))
}
/// Determines whether the platform TPM is present.
pub fn present() -> Result<bool> {
Ok(PlatformTpm::protocol()?.is_some())
}
/// Determine the number of active PCR banks on the TPM.
/// If no TPM is available, this will return zero.
pub fn active_pcr_banks() -> Result<u32> {
// Acquire access to the TPM protocol handle.
let Some(mut handle) = PlatformTpm::protocol()? else {
return Ok(0);
};
// Check if the TPM supports `GetActivePcrBanks`, and if it doesn't return zero.
if handle.version().major < 1 || handle.version().major == 1 && handle.version().minor < 1 {
return Ok(0);
}
// The safe wrapper for this function will decode the bitmap.
// Strictly speaking, it's not future-proof to re-encode that, but in practice it will work.
let banks = handle
.protocol()
.get_active_pcr_banks()
.context("unable to get active pcr banks")?;
// Return the number of active PCR banks.
Ok(banks.bits())
}
/// Log an event into the TPM pcr `pcr_index` with `buffer` as data. The `description`
/// is used to describe what the event is.
///
/// If a TPM is not available, this will do nothing.
pub fn log_event(pcr_index: PcrIndex, buffer: &[u8], description: &str) -> Result<()> {
// Acquire access to the TPM protocol handle.
let Some(mut handle) = PlatformTpm::protocol()? else {
return Ok(());
};
// Encode the description as UTF-8.
let description = description.as_bytes().to_vec();
// Construct an event input for the TPM.
let event = PcrEventInputs::new_in_box(pcr_index, EventType::IPL, &description)
.discard_errdata()
.context("unable to construct pcr event inputs")?;
// Log the event into the TPM.
handle
.protocol()
.hash_log_extend_event(Tcg2HashLogExtendEventFlags::empty(), buffer, &event)
.context("unable to log event to tpm")?;
Ok(())
}
}

11
src/sbat.rs Normal file
View File

@@ -0,0 +1,11 @@
/// SBAT must be aligned by 512 bytes.
const SBAT_SIZE: usize = 512;
/// Define the SBAT attestation by including the sbat.csv file.
/// See this document for more details: https://github.com/rhboot/shim/blob/main/SBAT.md
/// NOTE: Alignment can't be enforced by an attribute, so instead the alignment is currently
/// enforced by the SBAT_SIZE being 512. The build.rs will ensure that the sbat.csv is padded.
/// This code will not compile if the sbat.csv is a different size than SBAT_SIZE.
#[used]
#[unsafe(link_section = ".sbat")]
static SBAT: [u8; SBAT_SIZE] = *include_bytes!(concat!(env!("OUT_DIR"), "/sbat.csv"));

2
src/sbat.template.csv Normal file
View File

@@ -0,0 +1,2 @@
sbat,1,SBAT Version,sbat,1,https://github.com/rhboot/shim/blob/main/SBAT.md
sprout,1,Edera,sprout,{version},https://sprout.edera.dev
1 sbat 1 SBAT Version sbat 1 https://github.com/rhboot/shim/blob/main/SBAT.md
2 sprout 1 Edera sprout {version} https://sprout.edera.dev

14
src/secure.rs Normal file
View File

@@ -0,0 +1,14 @@
use crate::utils::variables::VariableController;
use anyhow::Result;
/// Secure boot services.
pub struct SecureBoot;
impl SecureBoot {
/// Checks if Secure Boot is enabled on the system.
/// This might fail if retrieving the variable fails in an irrecoverable way.
pub fn enabled() -> Result<bool> {
// The SecureBoot variable will tell us whether Secure Boot is enabled at all.
VariableController::GLOBAL.get_bool("SecureBoot")
}
}

View File

@@ -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};
@@ -14,6 +15,9 @@ pub mod framebuffer;
/// Support code for the media loader protocol. /// Support code for the media loader protocol.
pub mod media_loader; pub mod media_loader;
/// Support code for EFI variables.
pub mod variables;
/// Parses the input `path` as a [DevicePath]. /// Parses the input `path` as a [DevicePath].
/// Uses the [DevicePathFromText] protocol exclusively, and will fail if it cannot acquire the protocol. /// Uses the [DevicePathFromText] protocol exclusively, and will fail if it cannot acquire the protocol.
pub fn text_to_device_path(path: &str) -> Result<PoolDevicePath> { pub fn text_to_device_path(path: &str) -> Result<PoolDevicePath> {
@@ -100,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()
@@ -119,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)
@@ -133,8 +154,11 @@ pub fn resolve_path(default_root_path: &DevicePath, input: &str) -> Result<Resol
let root_path = text_to_device_path(root.as_str()) let root_path = text_to_device_path(root.as_str())
.context("unable to convert root to path")? .context("unable to convert root to path")?
.to_boxed(); .to_boxed();
let mut root_path = root_path.as_ref(); let root_path = root_path.as_ref();
let handle = uefi::boot::locate_device_path::<SimpleFileSystem>(&mut root_path)
// locate_device_path modifies the path, so we need to clone it.
let root_path_modifiable = root_path.to_owned();
let handle = uefi::boot::locate_device_path::<SimpleFileSystem>(&mut &*root_path_modifiable)
.context("unable to locate filesystem device path")?; .context("unable to locate filesystem device path")?;
let subpath = device_path_subpath(path.deref()).context("unable to get device subpath")?; let subpath = device_path_subpath(path.deref()).context("unable to get device subpath")?;
Ok(ResolvedPath { Ok(ResolvedPath {
@@ -152,16 +176,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].
@@ -187,12 +204,15 @@ pub fn unique_hash(input: &str) -> String {
/// Represents the type of partition GUID that can be retrieved. /// Represents the type of partition GUID that can be retrieved.
#[derive(PartialEq, Eq)] #[derive(PartialEq, Eq)]
pub enum PartitionGuidForm { pub enum PartitionGuidForm {
/// The partition GUID is the unique partition GUID.
Partition, Partition,
/// The partition GUID is the partition type GUID.
PartitionType, PartitionType,
} }
/// Retrieve the partition / partition type GUID of the device root `path`. /// Retrieve the partition / partition type GUID of the device root `path`.
/// This only works on GPT partitions. If the root is not a GPT partition, None is returned. /// This only works on GPT partitions. If the root is not a GPT partition, None is returned.
/// If the GUID is all zeros, this will return None.
pub fn partition_guid(path: &DevicePath, form: PartitionGuidForm) -> Result<Option<Guid>> { pub fn partition_guid(path: &DevicePath, form: PartitionGuidForm) -> Result<Option<Guid>> {
// Clone the path so we can pass it to the UEFI stack. // Clone the path so we can pass it to the UEFI stack.
let path = path.to_boxed(); let path = path.to_boxed();
@@ -224,8 +244,31 @@ pub fn partition_guid(path: &DevicePath, form: PartitionGuidForm) -> Result<Opti
// Match the form of the partition GUID. // Match the form of the partition GUID.
PartitionGuidForm::Partition => entry.unique_partition_guid, PartitionGuidForm::Partition => entry.unique_partition_guid,
PartitionGuidForm::PartitionType => entry.partition_type_guid.0, PartitionGuidForm::PartitionType => entry.partition_type_guid.0,
})) })
.filter(|guid| !guid.is_zero()))
} else { } else {
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")
}
}
}
}

101
src/utils/variables.rs Normal file
View File

@@ -0,0 +1,101 @@
use anyhow::{Context, Result};
use uefi::{CString16, guid};
use uefi_raw::Status;
use uefi_raw::table::runtime::{VariableAttributes, VariableVendor};
/// The classification of a variable.
/// This is an abstraction over various variable attributes.
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum VariableClass {
/// The variable is available in Boot Services and Runtime Services and is not persistent.
BootAndRuntimeTemporary,
}
impl VariableClass {
/// The [VariableAttributes] for this classification.
fn attributes(&self) -> VariableAttributes {
match self {
VariableClass::BootAndRuntimeTemporary => {
VariableAttributes::BOOTSERVICE_ACCESS | VariableAttributes::RUNTIME_ACCESS
}
}
}
}
/// Provides access to a particular set of vendor variables.
pub struct VariableController {
/// The GUID of the vendor.
vendor: VariableVendor,
}
impl VariableController {
/// Global variables.
pub const GLOBAL: VariableController = VariableController::new(VariableVendor(guid!(
"8be4df61-93ca-11d2-aa0d-00e098032b8c"
)));
/// Create a new [VariableController] for the `vendor`.
pub const fn new(vendor: VariableVendor) -> Self {
Self { vendor }
}
/// Convert `key` to a variable name as a CString16.
fn name(key: &str) -> Result<CString16> {
CString16::try_from(key).context("unable to convert variable name to CString16")
}
/// Retrieve a boolean value specified by the `key`.
pub fn get_bool(&self, key: &str) -> Result<bool> {
let name = Self::name(key)?;
// Retrieve the variable data, handling variable not existing as false.
match uefi::runtime::get_variable_boxed(&name, &self.vendor) {
Ok((data, _)) => {
// If the variable is zero-length, we treat it as false.
if data.is_empty() {
Ok(false)
} else {
// We treat the variable as true if the first byte is non-zero.
Ok(data[0] > 0)
}
}
Err(error) => {
// If the variable does not exist, we treat it as false.
if error.status() == Status::NOT_FOUND {
Ok(false)
} else {
Err(error).with_context(|| format!("unable to get efi variable {}", key))
}
}
}
}
/// Set a variable specified by `key` to `value`.
/// The variable `class` controls the attributes for the variable.
pub fn set(&self, key: &str, value: &[u8], class: VariableClass) -> Result<()> {
let name = Self::name(key)?;
uefi::runtime::set_variable(&name, &self.vendor, class.attributes(), value)
.with_context(|| format!("unable to set efi variable {}", key))?;
Ok(())
}
/// Set a variable specified by `key` to `value`, converting the value to
/// a [CString16]. The variable `class` controls the attributes for the variable.
pub fn set_cstr16(&self, key: &str, value: &str, class: VariableClass) -> Result<()> {
// Encode the value as a CString16 little endian.
let mut encoded = value
.encode_utf16()
.flat_map(|c| c.to_le_bytes())
.collect::<Vec<u8>>();
// Add a null terminator to the end of the value.
encoded.extend_from_slice(&[0, 0]);
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, class: VariableClass) -> Result<()> {
self.set(key, &[value as u8], class)
}
}