69 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
b9b34394cd sprout: version 0.0.15 2025-10-30 15:01:49 -04:00
3c467fef65 Merge pull request #22 from edera-dev/dependabot/docker/docker-updates-d0b0844295
chore(deps): bump rustlang/rust from `7cba2ed` to `3453212` in the docker-updates group
2025-10-30 14:58:36 -04:00
cfe7088970 Merge pull request #23 from edera-dev/azenla/bootloader-interface
feat(integrations): basic bootloader interface support
2025-10-30 14:58:04 -04:00
9d3a022e08 feat(bootloader-interface): add support for marking when the menu is being display 2025-10-30 13:27:58 -04:00
cc90199d61 feat(bootloader-interface): identify ourselves as sprout 2025-10-30 12:50:36 -04:00
cff55322fc feat(bootloader-interface): implement support for LoaderImageIdentifier 2025-10-30 12:44:07 -04:00
d2bef03437 fix(platform/timer): add back note of platform timer reference 2025-10-30 12:30:08 -04:00
37ab0406bb feat(bootloader-interface): implement support for UEFI firmware information 2025-10-30 11:47:35 -04:00
a77be3c282 feat(bootloader-interface): measure time in firmware as well 2025-10-30 02:51:52 -04:00
87d608366f feat(bootloader-interface): add support for loader boot times 2025-10-30 02:36:14 -04:00
e7d2438e5f feat(bls): basic support for boot loader interface 2025-10-28 23:23:12 -04:00
f82d24a206 feat(integrations): implement initial bootloader interface touchpoints 2025-10-28 21:05:22 -04:00
fe714cc411 fix(filesystem-device-match): bail early if no criteria is provided 2025-10-28 17:10:28 -04:00
000e3ea6c7 chore(github): disable blank issues 2025-10-28 16:56:56 -04:00
44de1bebd3 chore(github): add a feature request template 2025-10-28 16:56:06 -04:00
4d62ac2ce2 chore(github): show an ask a question link when creating an issue 2025-10-28 16:52:22 -04:00
dependabot[bot]
8d87fcfd2d chore(deps): bump rustlang/rust in the docker-updates group
Bumps the docker-updates group with 1 update: rustlang/rust.


Updates `rustlang/rust` from `7cba2ed` to `3453212`

---
updated-dependencies:
- dependency-name: rustlang/rust
  dependency-version: nightly-alpine
  dependency-type: direct:production
  dependency-group: docker-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-28 20:50:21 +00:00
1a2b103675 chore(github): make all workflows (except release) concurrent by sha 2025-10-28 16:49:26 -04:00
5e1bc4659b chore(github): rename all files from yaml => yml 2025-10-28 16:47:25 -04:00
27c82d24d0 chore(github): update bug report issue template 2025-10-28 16:44:54 -04:00
e2aa19174e chore(github): update bug report template to reorder items 2025-10-28 16:43:41 -04:00
fb342e12cb chore(github): initial issue templates 2025-10-28 16:35:12 -04:00
6f60a279c3 sprout: version 0.0.14 2025-10-28 01:47:15 -04:00
2e66d8c72e chore(docs): update readme with secure boot notes and roadmap items 2025-10-28 01:43:07 -04:00
86e08c2400 fix(doc/extractors/filesystem-device-match): the extractor will error if no criteria is provided 2025-10-28 00:19:38 -04:00
852823e2eb chore(doc/bls/entry): clarify why char::is_whitespace is used despite newline matching 2025-10-28 00:12:16 -04:00
734ab84054 chore(doc/context): clarify context finalization limit error message 2025-10-28 00:10:22 -04:00
c8a3408fdd fix(extractors/filesystem-device-match): clarify when to use fallback for empty criteria 2025-10-28 00:09:11 -04:00
deeda650a7 fix(autoconfigure/linux): remove debug line 2025-10-28 00:06:02 -04:00
268a2cb28b fix(media-loader): improve safety in the event protocol interface install fails 2025-10-27 23:56:12 -04:00
0b6523906d fix(doc): filesystem-device-match will not return a filesystem when criteria is not provided 2025-10-27 23:39:55 -04:00
3acd0ec7d8 chore(doc): document media loader safety 2025-10-27 23:24:35 -04:00
fe593efa8c chore(autoconfigure/docs): clarify why we append / to a device root 2025-10-27 23:15:14 -04:00
3058abab23 fix(menu): check for timeout duration overflow 2025-10-27 23:10:05 -04:00
5df717de6d chore(filesystem-device-match): extract partition uuid fetch to function 2025-10-27 23:05:57 -04:00
011e133455 chore(autoconfigure-linux): clarify variable shadowing for initramfs matching 2025-10-27 23:00:55 -04:00
ccd1a8f498 chore(menu): clarify that we do not need to free the key event 2025-10-27 22:59:00 -04:00
51 changed files with 1732 additions and 178 deletions

42
.github/ISSUE_TEMPLATE/bug-report.yml vendored Normal file
View File

@@ -0,0 +1,42 @@
name: Report a bug
description: File a bug report.
title: "bug: "
labels: ["bug", "triage"]
type: bug
assignees:
- edera-dev/engineering
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this bug report!
- type: input
id: version
attributes:
label: Version / Commit
description: What version of Sprout are you running?
validations:
required: true
- type: textarea
id: what-happened
attributes:
label: What happened?
description: Tell us what you expected to happen.
placeholder: Tell us what you see!
value: "A bug happened!"
validations:
required: true
- type: textarea
id: logs
attributes:
label: Log output
description: Please provide any relevant log output.
render: log
- type: checkboxes
id: terms
attributes:
label: Code of Conduct
description: By submitting this report, you agree to follow our [Code of Conduct](https://github.com/edera-dev/sprout/blob/main/CODE_OF_CONDUCT.md).
options:
- label: I agree to follow the Sprout Code of Conduct.
required: true

6
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@@ -0,0 +1,6 @@
blank_issues_enabled: false
contact_links:
- name: Ask a question
url: https://github.com/edera-dev/sprout/discussions
about: Please ask and answer questions here.
# Note that GitHub will automatically display our security policy in the new issue form.

View File

@@ -0,0 +1,29 @@
name: Request a feature
description: Request a feature.
title: "want: "
labels: ["enhancement", "triage"]
type: feature
assignees:
- edera-dev/engineering
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out a feature request!
- type: textarea
id: description
attributes:
label: Description
description:
placeholder: Tell us what you want to see!
value: "Your hopes and dreams here!"
validations:
required: true
- type: checkboxes
id: terms
attributes:
label: Code of Conduct
description: By submitting this report, you agree to follow our [Code of Conduct](https://github.com/edera-dev/sprout/blob/main/CODE_OF_CONDUCT.md).
options:
- label: I agree to follow the Sprout Code of Conduct.
required: true

View File

@@ -10,7 +10,7 @@ permissions:
contents: read # Needed to checkout the repository.
concurrency:
group: "${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}"
group: "${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}-${{ github.sha }}"
cancel-in-progress: true
jobs:

View File

@@ -12,7 +12,7 @@ permissions:
contents: read # Needed to checkout the repository.
concurrency:
group: "${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}"
group: "${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}-${{ github.sha }}"
cancel-in-progress: true
jobs:

View File

@@ -12,7 +12,7 @@ permissions:
contents: read # Needed to checkout the repository.
concurrency:
group: "${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}"
group: "${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}-${{ github.sha }}"
cancel-in-progress: true
jobs:

View File

@@ -19,7 +19,7 @@ permissions:
contents: read # Needed to checkout the repository.
concurrency:
group: "${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}"
group: "${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}-${{ github.sha }}"
cancel-in-progress: true
jobs:

2
Cargo.lock generated
View File

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

View File

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

View File

@@ -2,7 +2,7 @@
ARG RUST_PROFILE=release
ARG RUST_TARGET_SUBDIR=release
FROM --platform=$BUILDPLATFORM rustlang/rust:nightly-alpine@sha256:7cba2edabb6ba0e92cd806cd1e0acae99d50f63e5b9c9ad842766d13c896d68c AS build
FROM --platform=$BUILDPLATFORM rustlang/rust:nightly-alpine@sha256:34532121803db17008af0cdc4e2e1210466cb257cc9d3840dac42d706640fee5 AS build
RUN apk --no-cache add musl-dev busybox-static
ARG RUST_PROFILE
RUN adduser -S -s /bin/sh build

View File

@@ -6,11 +6,11 @@
</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.
It supports `x86_64` and `ARM64` EFI-capable systems. It is designed to require UEFI and can be chainloaded from an
@@ -21,10 +21,10 @@ Sprout is licensed under Apache 2.0 and is open to modifications and contributio
## Background
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
with or without hardware virtualization support. To do this we need to inject our hypervisor at boot time.
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.
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
itself is fully turing complete. This makes modifying boot configuration difficult and error-prone.
@@ -49,31 +49,33 @@ simplify installation and usage.
## Features
NOTE: Currently, Sprout is experimental and is not intended for production use.
The boot menu mechanism is very rudimentary.
**NOTE**: Sprout is still in beta.
### Current
- [x] Loadable driver support
- [x] [Bootloader specification (BLS)](https://uapi-group.org/specifications/specs/boot_loader_specification/) support
- [x] Basic [Bootloader specification (BLS)](https://uapi-group.org/specifications/specs/boot_loader_specification/) support
- [x] Chainload support
- [x] Linux boot support via EFI stub
- [x] Windows boot support via chainload
- [x] Load Linux initrd from disk
- [x] Basic boot menu
- [x] BLS autoconfiguration support
- [x] [Secure Boot support](https://github.com/edera-dev/sprout/issues/20): partial
### Roadmap
- [ ] Full-featured boot menu
- [ ] Secure Boot support: work in progress
- [ ] UKI support: partial
- [ ] multiboot2 support
- [ ] Linux boot protocol (boot without EFI stub)
- [ ] [Bootloader interface support](https://github.com/edera-dev/sprout/issues/21)
- [ ] [BLS specification conformance](https://github.com/edera-dev/sprout/issues/2)
- [ ] [Full-featured boot menu](https://github.com/edera-dev/sprout/issues/1)
- [ ] [UKI support](https://github.com/edera-dev/sprout/issues/6): partial
- [ ] [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)
## Concepts
- drivers: loadable EFI modules that can add functionality to the EFI system.
- autoconfiguration: code that can automatically generate sprout.toml based on the EFI environment.
- actions: executable code with a configuration that can be run by various other sprout concepts.
- generators: code that can generate boot entries based on inputs or runtime code.
- extractors: code that can extract values from the EFI environment.

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
- Secure Boot disabled
- Secure Boot is disabled or configured to allow Sprout
- UEFI Windows installation
## Step 1: Base Installation

View File

@@ -7,7 +7,7 @@ RUN export DEBIAN_FRONTEND=noninteractive && apt-get update && apt-get install -
WORKDIR /work
COPY sprout.efi /work/${EFI_NAME}.EFI
COPY sprout.toml /work/SPROUT.TOML
COPY kernel.efi /work/KERNEL.EFI
COPY kernel.efi /work/VMLINUZ
COPY shell.efi /work/SHELL.EFI
COPY xen.efi /work/XEN.EFI
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/ENTRIES && \
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 XEN.EFI ::/EFI/BOOT/ && \
mcopy -i sprout.img XEN.CFG ::/EFI/BOOT/ && \

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,6 @@
use crate::context::SproutContext;
use crate::integrations::bootloader_interface::BootloaderInterface;
use crate::integrations::shim::{ShimInput, ShimSupport};
use crate::utils;
use crate::utils::media_loader::MediaLoaderHandle;
use crate::utils::media_loader::constants::linux::LINUX_EFI_INITRD_MEDIA_GUID;
@@ -34,20 +36,14 @@ pub fn chainload(context: Rc<SproutContext>, configuration: &ChainloadConfigurat
// Resolve the path to the image to chainload.
let resolved = utils::resolve_path(
context.root().loaded_image_path()?,
Some(context.root().loaded_image_path()?),
&context.stamp(&configuration.path),
)
.context("unable to resolve chainload path")?;
// Load the image to chainload.
let image = uefi::boot::load_image(
sprout_image,
uefi::boot::LoadImageSource::FromDevicePath {
device_path: &resolved.full_path,
boot_policy: uefi::proto::BootPolicy::ExactMatch,
},
)
.context("unable to load image")?;
// Load the image to chainload using the shim support integration.
// It will determine if the image needs to be loaded via the shim or can be loaded directly.
let image = ShimSupport::load(sprout_image, ShimInput::ResolvedPath(&resolved))?;
// Open the LoadedImage protocol of the image to chainload.
let mut loaded_image_protocol = uefi::boot::open_protocol_exclusive::<LoadedImage>(image)
@@ -94,7 +90,8 @@ pub fn chainload(context: Rc<SproutContext>, configuration: &ChainloadConfigurat
// If an initrd is provided, register it with the EFI stack.
let mut initrd_handle = None;
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")?;
let handle =
MediaLoaderHandle::register(LINUX_EFI_INITRD_MEDIA_GUID, content.into_boxed_slice())
@@ -102,6 +99,10 @@ pub fn chainload(context: Rc<SproutContext>, configuration: &ChainloadConfigurat
initrd_handle = Some(handle);
}
// Mark execution of an entry in the bootloader interface.
BootloaderInterface::mark_exec(context.root().timer())
.context("unable to mark execution of boot entry in bootloader interface")?;
// Start the loaded image.
// This call might return, or it may pass full control to another image that will never return.
// Capture the result to ensure we can return an error if the image fails to start, but only

View File

@@ -98,7 +98,7 @@ fn register_media_loader_file(
// Stamp the path to the file.
let path = context.stamp(path);
// 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))?;
// Register the media loader.
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::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;
/// The configuration of the splash action.
@@ -143,7 +145,7 @@ pub fn splash(context: Rc<SproutContext>, configuration: &SplashConfiguration) -
// Stamp the image path value.
let image = context.stamp(&configuration.image);
// 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.
let image = ImageReader::with_format(Cursor::new(image), ImageFormat::Png)
.decode()

View File

@@ -31,7 +31,7 @@ pub fn scan(
.to_string(DisplayOnly(false), AllowShortcuts(false))
.context("unable to convert device root to string")?
.to_string();
// Add a trailing slash to the root to ensure the path is valid.
// Add a trailing forward-slash to the root to ensure the device root is completed.
root.push('/');
// Generate a unique hash of the root path.
@@ -72,7 +72,7 @@ pub fn scan(
// Generate a unique name for the BLS generator and insert the generator into the configuration.
config.generators.insert(
format!("autoconfigure-bls-{}", root_unique_hash),
format!("auto-bls-{}", root_unique_hash),
GeneratorDeclaration {
bls: Some(generator),
..Default::default()

View File

@@ -6,10 +6,9 @@ use crate::generators::GeneratorDeclaration;
use crate::generators::list::ListConfiguration;
use crate::utils;
use anyhow::{Context, Result};
use log::info;
use std::collections::BTreeMap;
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::text::{AllowShortcuts, DisplayOnly};
@@ -18,7 +17,8 @@ const LINUX_CHAINLOAD_ACTION_PREFIX: &str = "linux-chainload-";
/// The locations to scan for kernel pairs.
/// 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.
const KERNEL_PREFIXES: &[&str] = &["vmlinuz"];
@@ -40,6 +40,9 @@ fn scan_directory(filesystem: &mut FileSystem, path: &str) -> Result<Vec<KernelP
// All the discovered kernel pairs.
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.
let path = CString16::try_from(path).context("unable to convert path to CString16")?;
let path = Path::new(&path);
@@ -63,6 +66,16 @@ fn scan_directory(filesystem: &mut FileSystem, path: &str) -> Result<Vec<KernelP
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 item in directory {
let item = item.context("unable to read directory item")?;
@@ -75,11 +88,14 @@ fn scan_directory(filesystem: &mut FileSystem, path: &str) -> Result<Vec<KernelP
// Convert the name from a CString16 to a 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.
let Some(prefix) = KERNEL_PREFIXES
.iter()
.find(|prefix| name == **prefix || name.starts_with(&format!("{}-", prefix)))
else {
// This is case-insensitive to ensure we pick up all possibilities.
let Some(prefix) = KERNEL_PREFIXES.iter().find(|prefix| {
name_for_match == **prefix || name_for_match.starts_with(&format!("{}-", prefix))
}) else {
// Skip over anything that doesn't match a kernel prefix.
continue;
};
@@ -89,7 +105,7 @@ fn scan_directory(filesystem: &mut FileSystem, path: &str) -> Result<Vec<KernelP
// Find a matching initramfs, if any.
let mut initramfs_prefix_iter = INITRAMFS_PREFIXES.iter();
let initramfs = loop {
let matched_initramfs_path = loop {
let Some(prefix) = initramfs_prefix_iter.next() else {
break None;
};
@@ -97,8 +113,9 @@ fn scan_directory(filesystem: &mut FileSystem, path: &str) -> Result<Vec<KernelP
let initramfs = format!("{}{}", prefix, suffix);
let initramfs = CString16::try_from(initramfs.as_str())
.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));
// Check if the initramfs path exists, if it does, break out of the loop.
if filesystem
.try_exists(&initramfs_path)
@@ -109,10 +126,10 @@ fn scan_directory(filesystem: &mut FileSystem, path: &str) -> Result<Vec<KernelP
};
// 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()));
let kernel = kernel.to_string();
let initramfs = initramfs.map(|initramfs| initramfs.to_string());
let initramfs = matched_initramfs_path.map(|initramfs_path| initramfs_path.to_string());
// Produce a kernel pair.
let pair = KernelPair { kernel, initramfs };
@@ -135,7 +152,7 @@ pub fn scan(
.to_string(DisplayOnly(false), AllowShortcuts(false))
.context("unable to convert device root to string")?
.to_string();
// Add a trailing slash to the root to ensure the path is valid.
// Add a trailing forward-slash to the root to ensure the device root is completed.
root.push('/');
// Generate a unique hash of the root path.
@@ -182,7 +199,7 @@ pub fn scan(
// Generate a unique name for the Linux generator and insert the generator into the configuration.
config.generators.insert(
format!("autoconfigure-linux-{}", root_unique_hash),
format!("auto-linux-{}", root_unique_hash),
GeneratorDeclaration {
list: Some(generator),
..Default::default()
@@ -215,8 +232,6 @@ pub fn scan(
},
);
info!("{:?}", config);
// We had a Linux kernel, so return true to indicate something was found.
Ok(true)
}

View File

@@ -39,7 +39,7 @@ pub fn scan(
.to_string(DisplayOnly(false), AllowShortcuts(false))
.context("unable to convert device root to string")?
.to_string();
// Add a trailing slash to the root to ensure the path is valid.
// Add a trailing forward-slash to the root to ensure the device root is completed.
root.push('/');
// Generate a unique hash of the root path.
@@ -49,7 +49,7 @@ pub fn scan(
let chainload_action_name = format!("{}{}", WINDOWS_CHAINLOAD_ACTION_PREFIX, root_unique_hash,);
// 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.
let entry = EntryDeclaration {

View File

@@ -1,5 +1,6 @@
use crate::config::{RootConfiguration, latest_version};
use crate::options::SproutOptions;
use crate::platform::tpm::PlatformTpm;
use crate::utils;
use anyhow::{Context, Result, bail};
use log::info;
@@ -19,8 +20,17 @@ fn load_raw_config(options: &SproutOptions) -> Result<Vec<u8>> {
info!("configuration file: {}", options.config);
// 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")?;
// 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.
Ok(content)
}

View File

@@ -1,5 +1,6 @@
use crate::actions::ActionDeclaration;
use crate::options::SproutOptions;
use crate::platform::timer::PlatformTimer;
use anyhow::anyhow;
use anyhow::{Result, bail};
use std::cmp::Reverse;
@@ -12,22 +13,29 @@ const CONTEXT_FINALIZE_ITERATION_LIMIT: usize = 100;
/// Declares a root context for Sprout.
/// This contains data that needs to be shared across Sprout.
#[derive(Default)]
pub struct RootContext {
/// The actions that are available in Sprout.
actions: BTreeMap<String, ActionDeclaration>,
/// The device path of the loaded Sprout image.
loaded_image_path: Option<Box<DevicePath>>,
/// Platform timer started at the beginning of the boot process.
timer: PlatformTimer,
/// The global options of Sprout.
options: SproutOptions,
}
impl RootContext {
/// Creates a new root context with the `loaded_image_device_path` which will be stored
/// in the context for easy access.
pub fn new(loaded_image_device_path: Box<DevicePath>, options: SproutOptions) -> Self {
/// in the context for easy access. We also provide a `timer` which is used to measure elapsed
/// time for the bootloader.
pub fn new(
loaded_image_device_path: Box<DevicePath>,
timer: PlatformTimer,
options: SproutOptions,
) -> Self {
Self {
actions: BTreeMap::new(),
timer,
loaded_image_path: Some(loaded_image_device_path),
options,
}
@@ -43,6 +51,11 @@ impl RootContext {
&mut self.actions
}
/// Access the platform timer that is started at the beginning of the boot process.
pub fn timer(&self) -> &PlatformTimer {
&self.timer
}
/// Access the device path of the loaded Sprout image.
pub fn loaded_image_path(&self) -> Result<&DevicePath> {
self.loaded_image_path
@@ -168,13 +181,13 @@ impl SproutContext {
let mut current_values = self.all_values();
// To ensure that there is no possible infinite loop, we need to check
// the number of iterations. If it exceeds 100, we bail.
// the number of iterations. If it exceeds CONTEXT_FINALIZE_ITERATION_LIMIT, we bail.
let mut iterations: usize = 0;
loop {
iterations += 1;
if iterations > CONTEXT_FINALIZE_ITERATION_LIMIT {
bail!("infinite loop detected in context finalization");
bail!("maximum number of replacement iterations reached while finalizing context");
}
let mut did_change = false;
@@ -206,6 +219,14 @@ impl SproutContext {
/// 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.
///
/// 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) {
let mut result = text.as_ref().to_string();
let mut did_change = false;

View File

@@ -1,4 +1,5 @@
use crate::context::SproutContext;
use crate::integrations::shim::{ShimInput, ShimSupport};
use crate::utils;
use anyhow::{Context, Result};
use log::info;
@@ -6,7 +7,6 @@ use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::rc::Rc;
use uefi::boot::SearchType;
use uefi::proto::device_path::LoadedImageDevicePath;
/// Declares a driver configuration.
/// Drivers allow extending the functionality of Sprout.
@@ -23,28 +23,17 @@ pub struct DriverDeclaration {
fn load_driver(context: Rc<SproutContext>, driver: &DriverDeclaration) -> Result<()> {
// Acquire the handle and device path of the loaded image.
let sprout_image = uefi::boot::image_handle();
let image_device_path_protocol =
uefi::boot::open_protocol_exclusive::<LoadedImageDevicePath>(sprout_image)
.context("unable to open loaded image device path protocol")?;
// Get the device path root of the sprout image.
let mut full_path = utils::device_path_root(&image_device_path_protocol)?;
// Push the path of the driver from the root.
full_path.push_str(&context.stamp(&driver.path));
// Convert the path to a device path.
let device_path = utils::text_to_device_path(&full_path)?;
// Load the driver image.
let image = uefi::boot::load_image(
sprout_image,
uefi::boot::LoadImageSource::FromDevicePath {
device_path: &device_path,
boot_policy: uefi::proto::BootPolicy::ExactMatch,
},
// Resolve the path to the driver image.
let resolved = utils::resolve_path(
Some(context.root().loaded_image_path()?),
&context.stamp(&driver.path),
)
.context("unable to load image")?;
.context("unable to resolve path to driver")?;
// Load the driver image using the shim support integration.
// It will determine if the image needs to be loaded via the shim or can be loaded directly.
let image = ShimSupport::load(sprout_image, ShimInput::ResolvedPath(&resolved))?;
// Start the driver image, this is expected to return control to sprout.
// There is no guarantee that the driver will actually return control as it is

View File

@@ -28,6 +28,7 @@ pub struct BootableEntry {
context: Rc<SproutContext>,
declaration: EntryDeclaration,
default: bool,
pin_name: bool,
}
impl BootableEntry {
@@ -44,6 +45,7 @@ impl BootableEntry {
context,
declaration,
default: false,
pin_name: false,
}
}
@@ -72,6 +74,11 @@ impl BootableEntry {
self.default
}
/// Fetch whether the entry is pinned, which prevents prefixing.
pub fn is_pin_name(&self) -> bool {
self.pin_name
}
/// Swap out the context of the entry.
pub fn swap_context(&mut self, context: Rc<SproutContext>) {
self.context = context;
@@ -87,6 +94,11 @@ impl BootableEntry {
self.default = true;
}
/// Mark this entry as being pinned, which prevents prefixing.
pub fn mark_pin_name(&mut self) {
self.pin_name = true;
}
/// Prepend the name of the entry with `prefix`.
pub fn prepend_name_prefix(&mut self, prefix: &str) {
self.name.insert_str(0, prefix);

View File

@@ -9,17 +9,16 @@ use uefi::fs::{FileSystem, Path};
use uefi::proto::device_path::DevicePath;
use uefi::proto::media::file::{File, FileSystemVolumeLabel};
use uefi::proto::media::fs::SimpleFileSystem;
use uefi::proto::media::partition::PartitionInfo;
use uefi::{CString16, Guid};
use uefi_raw::Status;
/// The filesystem device match extractor.
/// This extractor finds a filesystem using some search criteria and returns
/// the device root path that can concatenated with subpaths to access files
/// on a particular filesystem.
/// The fallback value can be used to provide a value if no match is found.
///
/// This function only requires all the criteria to match.
/// The fallback value can be used to provide a value if none is found.
/// This extractor requires all the criteria to match. If no criteria is provided,
/// an error is returned.
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
pub struct FilesystemDeviceMatchExtractor {
/// Matches a filesystem that has the specified label.
@@ -45,6 +44,15 @@ pub fn extract(
context: Rc<SproutContext>,
extractor: &FilesystemDeviceMatchExtractor,
) -> Result<String> {
// If no criteria are provided, bail with an error.
if extractor.has_label.is_none()
&& extractor.has_item.is_none()
&& extractor.has_partition_uuid.is_none()
&& extractor.has_partition_type_uuid.is_none()
{
bail!("at least one criteria is required for filesystem-device-match");
}
// Find all the filesystems inside the UEFI stack.
let handles = uefi::boot::find_handles::<SimpleFileSystem>()
.context("unable to find filesystem handles")?;
@@ -54,58 +62,49 @@ pub fn extract(
// This defines whether a match has been found.
let mut has_match = false;
// Extract the partition info for this filesystem.
// There is no guarantee that the filesystem has a partition.
let partition_info = {
// Open the partition info protocol for this handle.
let partition_info = uefi::boot::open_protocol_exclusive::<PartitionInfo>(handle);
match partition_info {
Ok(partition_info) => {
// GPT partitions have a unique partition GUID.
// MBR does not.
if let Some(gpt) = partition_info.gpt_partition_entry() {
let uuid = gpt.unique_partition_guid;
let type_uuid = gpt.partition_type_guid;
Some((uuid, type_uuid.0))
} else {
None
}
}
Err(error) => {
// If the filesystem does not have a partition, that is okay.
if error.status() == Status::NOT_FOUND || error.status() == Status::UNSUPPORTED
{
None
} else {
// We should still handle other errors gracefully.
Err(error).context("unable to open filesystem partition info")?;
unreachable!()
}
}
}
};
// Check if the partition info matches partition uuid criteria.
if let Some((partition_uuid, _partition_type_guid)) = partition_info
&& let Some(ref has_partition_uuid) = extractor.has_partition_uuid
{
if let Some(ref has_partition_uuid) = extractor.has_partition_uuid {
// Parse the partition uuid from the extractor.
let parsed_uuid = Guid::from_str(has_partition_uuid)
.map_err(|e| anyhow!("unable to parse has-partition-uuid: {}", e))?;
if partition_uuid != parsed_uuid {
// Fetch the root of the device.
let root = uefi::boot::open_protocol_exclusive::<DevicePath>(handle)
.context("unable to fetch the device path of the filesystem")?
.deref()
.to_boxed();
// Fetch the partition uuid for this filesystem.
let partition_uuid = utils::partition_guid(&root, utils::PartitionGuidForm::Partition)
.context("unable to fetch the partition uuid of the filesystem")?;
// Compare the partition uuid to the parsed uuid.
// If it does not match, continue to the next filesystem.
if partition_uuid != Some(parsed_uuid) {
continue;
}
has_match = true;
}
// Check if the partition info matches partition type uuid criteria.
if let Some((_partition_uuid, partition_type_guid)) = partition_info
&& let Some(ref has_partition_type_uuid) = extractor.has_partition_type_uuid
{
if let Some(ref has_partition_type_uuid) = extractor.has_partition_type_uuid {
// Parse the partition type uuid from the extractor.
let parsed_uuid = Guid::from_str(has_partition_type_uuid)
.map_err(|e| anyhow!("unable to parse has-partition-type-uuid: {}", e))?;
if partition_type_guid != parsed_uuid {
// Fetch the root of the device.
let root = uefi::boot::open_protocol_exclusive::<DevicePath>(handle)
.context("unable to fetch the device path of the filesystem")?
.deref()
.to_boxed();
// Fetch the partition type uuid for this filesystem.
let partition_type_uuid =
utils::partition_guid(&root, utils::PartitionGuidForm::PartitionType)
.context("unable to fetch the partition uuid of the filesystem")?;
// Compare the partition type uuid to the parsed uuid.
// If it does not match, continue to the next filesystem.
if partition_type_uuid != Some(parsed_uuid) {
continue;
}
has_match = true;

View File

@@ -49,7 +49,7 @@ pub fn generate(context: Rc<SproutContext>, bls: &BlsConfiguration) -> Result<Ve
let path = context.stamp(&bls.path);
// 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")?;
// Construct a filesystem path to the BLS entries directory.
@@ -83,13 +83,16 @@ pub fn generate(context: Rc<SproutContext>, bls: &BlsConfiguration) -> Result<Ve
}
// Get the file name of the filesystem item.
let name = entry.file_name().to_string();
let mut name = entry.file_name().to_string();
// Ignore files that are not .conf files.
if !name.to_lowercase().ends_with(".conf") {
continue;
}
// Remove the .conf extension.
name.truncate(name.len() - 5);
// Create a mutable path so we can append the file name to produce the full path.
let mut full_entry_path = entries_path.to_path_buf();
full_entry_path.push(entry.file_name());
@@ -125,13 +128,21 @@ pub fn generate(context: Rc<SproutContext>, bls: &BlsConfiguration) -> Result<Ve
context.set("options", options);
context.set("initrd", initrd);
// Add the entry to the list with a frozen context.
entries.push(BootableEntry::new(
// Produce a new bootable entry.
let mut entry = BootableEntry::new(
name,
bls.entry.title.clone(),
context.freeze(),
bls.entry.clone(),
));
);
// Pin the entry name to prevent prefixing.
// This is needed as the bootloader interface requires the name to be
// the same as the entry file name, minus the .conf extension.
entry.mark_pin_name();
// Add the entry to the list with a frozen context.
entries.push(entry);
}
Ok(entries)

View File

@@ -41,7 +41,8 @@ impl FromStr for BlsEntry {
continue;
}
// Split the line once by whitespace.
// Split the line once by whitespace. This technically includes newlines but since
// the lines iterator is used, there should never be a newline here.
let Some((key, value)) = line.split_once(char::is_whitespace) else {
continue;
};

View File

@@ -18,7 +18,7 @@ pub struct ListConfiguration {
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(
context: Rc<SproutContext>,
list: &ListConfiguration,

4
src/integrations.rs Normal file
View File

@@ -0,0 +1,4 @@
/// Implements support for the bootloader interface specification.
pub mod bootloader_interface;
/// Implements support for the shim loader application for Secure Boot.
pub mod shim;

View File

@@ -0,0 +1,163 @@
use crate::platform::timer::PlatformTimer;
use crate::utils::device_path_subpath;
use crate::utils::variables::{VariableClass, VariableController};
use anyhow::{Context, Result};
use uefi::proto::device_path::DevicePath;
use uefi::{Guid, guid};
use uefi_raw::table::runtime::VariableVendor;
/// The name of the bootloader to tell the system.
const LOADER_NAME: &str = "Sprout";
/// Bootloader Interface support.
pub struct BootloaderInterface;
impl BootloaderInterface {
/// Bootloader Interface GUID from https://systemd.io/BOOT_LOADER_INTERFACE
const VENDOR: VariableController = VariableController::new(VariableVendor(guid!(
"4a67b082-0a4c-41cf-b6c7-440b29bb8c4f"
)));
/// Tell the system that Sprout was initialized at the current time.
pub fn mark_init(timer: &PlatformTimer) -> Result<()> {
Self::mark_time("LoaderTimeInitUSec", timer)
}
/// Tell the system that Sprout is about to execute the boot entry.
pub fn mark_exec(timer: &PlatformTimer) -> Result<()> {
Self::mark_time("LoaderTimeExecUSec", timer)
}
/// Tell the system that Sprout is about to display the menu.
pub fn mark_menu(timer: &PlatformTimer) -> Result<()> {
Self::mark_time("LoaderTimeMenuUsec", timer)
}
/// Tell the system about the current time as measured by the platform timer.
/// Sets the variable specified by `key` to the number of microseconds.
fn mark_time(key: &str, timer: &PlatformTimer) -> Result<()> {
// Measure the elapsed time since the hardware timer was started.
let elapsed = timer.elapsed_since_lifetime();
Self::VENDOR.set_cstr16(
key,
&elapsed.as_micros().to_string(),
VariableClass::BootAndRuntimeTemporary,
)
}
/// Tell the system what loader is being used.
pub fn set_loader_info() -> Result<()> {
Self::VENDOR.set_cstr16(
"LoaderInfo",
LOADER_NAME,
VariableClass::BootAndRuntimeTemporary,
)
}
/// Tell the system the relative path to the partition root of the current bootloader.
pub fn set_loader_path(path: &DevicePath) -> Result<()> {
let subpath = device_path_subpath(path).context("unable to get loader path subpath")?;
Self::VENDOR.set_cstr16(
"LoaderImageIdentifier",
&subpath,
VariableClass::BootAndRuntimeTemporary,
)
}
/// Tell the system what the partition GUID of the ESP Sprout was booted from is.
pub fn set_partition_guid(guid: &Guid) -> Result<()> {
Self::VENDOR.set_cstr16(
"LoaderDevicePartUUID",
&guid.to_string(),
VariableClass::BootAndRuntimeTemporary,
)
}
/// Tell the system what boot entries are available.
pub fn set_entries<N: AsRef<str>>(entries: impl Iterator<Item = N>) -> Result<()> {
// Entries are stored as a null-terminated list of CString16 strings back to back.
// Iterate over the entries and convert them to CString16 placing them into data.
let mut data = Vec::new();
for entry in entries {
// Convert the entry to CString16 little endian.
let encoded = entry
.as_ref()
.encode_utf16()
.flat_map(|c| c.to_le_bytes())
.collect::<Vec<u8>>();
// Write the bytes into the data buffer.
data.extend_from_slice(&encoded);
// Add a null terminator to the end of the entry.
data.extend_from_slice(&[0, 0]);
}
Self::VENDOR.set(
"LoaderEntries",
&data,
VariableClass::BootAndRuntimeTemporary,
)
}
/// Tell the system what the default boot entry is.
pub fn set_default_entry(entry: String) -> Result<()> {
Self::VENDOR.set_cstr16(
"LoaderEntryDefault",
&entry,
VariableClass::BootAndRuntimeTemporary,
)
}
/// Tell the system what the selected boot entry is.
pub fn set_selected_entry(entry: String) -> Result<()> {
Self::VENDOR.set_cstr16(
"LoaderEntrySelected",
&entry,
VariableClass::BootAndRuntimeTemporary,
)
}
/// Tell the system about the UEFI firmware we are running on.
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.
let firmware_info = format!(
"{} {}.{:02}",
uefi::system::firmware_vendor(),
firmware_revision >> 16,
firmware_revision & 0xffff,
);
Self::VENDOR.set_cstr16(
"LoaderFirmwareInfo",
&firmware_info,
VariableClass::BootAndRuntimeTemporary,
)?;
// Format the firmware revision into something human-readable.
let firmware_type = format!(
"UEFI {}.{:02}",
uefi_revision.major(),
uefi_revision.minor()
);
Self::VENDOR.set_cstr16(
"LoaderFirmwareType",
&firmware_type,
VariableClass::BootAndRuntimeTemporary,
)
}
/// Tell the system what the number of active PCR banks is.
/// If this is zero, that is okay.
pub fn set_tpm2_active_pcr_banks(value: u32) -> Result<()> {
// Format the value into the specification format.
let value = format!("0x{:08x}", value);
Self::VENDOR.set_cstr16(
"LoaderTpm2ActivePcrBanks",
&value,
VariableClass::BootAndRuntimeTemporary,
)
}
}

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

@@ -1,14 +1,23 @@
#![doc = include_str!("../README.md")]
#![feature(uefi_std)]
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::context::{RootContext, SproutContext};
use crate::entries::BootableEntry;
use crate::integrations::bootloader_interface::BootloaderInterface;
use crate::options::SproutOptions;
use crate::options::parser::OptionsRepresentable;
use crate::phases::phase;
use crate::platform::timer::PlatformTimer;
use crate::platform::tpm::PlatformTpm;
use crate::secure::SecureBoot;
use crate::utils::PartitionGuidForm;
use anyhow::{Context, Result, bail};
use log::{error, info};
use log::{error, info, warn};
use std::collections::BTreeMap;
use std::ops::Deref;
use std::time::Duration;
@@ -38,12 +47,24 @@ pub mod extractors;
/// generators: Runtime code that can generate entries with specific values.
pub mod generators;
/// platform: Integration or support code for specific hardware platforms.
pub mod platform;
/// menu: Display a boot menu to select an entry to boot.
pub mod menu;
/// integrations: Code that interacts with other systems.
pub mod integrations;
/// phases: Hooks into specific parts of the boot process.
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.
pub mod setup;
@@ -55,6 +76,33 @@ pub mod utils;
/// Run Sprout, returning an error if one occurs.
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.
let timer = PlatformTimer::start();
// Mark the initialization of Sprout in the bootloader interface.
BootloaderInterface::mark_init(&timer)
.context("unable to mark initialization in bootloader interface")?;
// Tell the bootloader interface what firmware we are running on.
BootloaderInterface::set_firmware_info()
.context("unable to set firmware info in bootloader interface")?;
// Tell the bootloader interface what loader is being used.
BootloaderInterface::set_loader_info()
.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.
let options = SproutOptions::parse().context("unable to parse options")?;
@@ -69,17 +117,35 @@ fn run() -> Result<()> {
config::loader::load(&options)?
};
// Load the root context.
// Grab the sprout.efi loaded image path.
// This is done in a block to ensure the release of the LoadedImageDevicePath protocol.
let mut root = {
let loaded_image_path = {
let current_image_device_path_protocol = uefi::boot::open_protocol_exclusive::<
LoadedImageDevicePath,
>(uefi::boot::image_handle())
.context("unable to get loaded image device path")?;
let loaded_image_path = current_image_device_path_protocol.deref().to_boxed();
RootContext::new(loaded_image_path, options)
current_image_device_path_protocol.deref().to_boxed()
};
// Grab the partition GUID of the ESP that sprout was loaded from.
let loaded_image_partition_guid =
utils::partition_guid(&loaded_image_path, PartitionGuidForm::Partition)
.context("unable to retrieve loaded image partition guid")?;
// Set the partition GUID of the ESP that sprout was loaded from in the bootloader interface.
if let Some(loaded_image_partition_guid) = loaded_image_partition_guid {
// Tell the system about the partition GUID.
BootloaderInterface::set_partition_guid(&loaded_image_partition_guid)
.context("unable to set partition guid in bootloader interface")?;
}
// Tell the bootloader interface what the loaded image path is.
BootloaderInterface::set_loader_path(&loaded_image_path)
.context("unable to set loader path in bootloader interface")?;
// Create the root context.
let mut root = RootContext::new(loaded_image_path, timer, options);
// Insert the configuration actions into the root context.
root.actions_mut().extend(config.actions.clone());
@@ -159,13 +225,17 @@ fn run() -> Result<()> {
for (name, generator) in config.generators {
let context = context.fork().freeze();
// We will prefix all entries with [name]-.
// We will prefix all entries with [name]-, provided the name is not pinned.
let prefix = format!("{}-", name);
// Add all the entries generated by the generator to the entry list.
// The generator specifies the context associated with the entry.
for mut entry in generators::generate(context.clone(), &generator)? {
// If the entry name is not pinned, prepend the name prefix.
if !entry.is_pin_name() {
entry.prepend_name_prefix(&prefix);
}
entries.push(entry);
}
}
@@ -200,6 +270,21 @@ fn run() -> Result<()> {
entry.mark_default();
}
// Iterate over all the entries and tell the bootloader interface what the entries are.
for entry in &entries {
// If the entry is the default entry, tell the bootloader interface it is the default.
if entry.is_default() {
// Tell the bootloader interface what the default entry is.
BootloaderInterface::set_default_entry(entry.name().to_string())
.context("unable to set default entry in bootloader interface")?;
break;
}
}
// Tell the bootloader interface what entries are available.
BootloaderInterface::set_entries(entries.iter().map(|entry| entry.name()))
.context("unable to set entries in bootloader interface")?;
// Execute the late phase.
phase(context.clone(), &config.phases.late).context("unable to execute late phase")?;
@@ -223,9 +308,14 @@ fn run() -> Result<()> {
.context(format!("unable to find entry: {force_boot_entry}"))?
} else {
// Delegate to the menu to select an entry to boot.
menu::select(menu_timeout, &entries).context("unable to select entry via boot menu")?
menu::select(&timer, menu_timeout, &entries)
.context("unable to select entry via boot menu")?
};
// Tell the bootloader interface what the selected entry is.
BootloaderInterface::set_selected_entry(entry.name().to_string())
.context("unable to set selected entry in bootloader interface")?;
// Execute all the actions for the selected entry.
for action in &entry.declaration().actions {
let action = entry.context().stamp(action);
@@ -251,8 +341,8 @@ fn main() -> Result<()> {
for (index, stack) in error.chain().enumerate() {
error!("[{}]: {}", index, stack);
}
// Sleep for 10 seconds to allow the user to read the error.
uefi::boot::stall(Duration::from_secs(10));
// Sleep to allow the user to read the error.
uefi::boot::stall(DELAY_ON_ERROR);
}
// Sprout doesn't necessarily guarantee anything was booted.

View File

@@ -1,4 +1,6 @@
use crate::entries::BootableEntry;
use crate::integrations::bootloader_interface::BootloaderInterface;
use crate::platform::timer::PlatformTimer;
use anyhow::{Context, Result, bail};
use log::info;
use std::time::Duration;
@@ -40,8 +42,17 @@ fn read(input: &mut Input, timeout: &Duration) -> Result<MenuOperation> {
uefi::boot::create_event_ex(EventType::TIMER, Tpl::CALLBACK, None, None, None)
.context("unable to create timer event")?
};
// The timeout is in increments of 100 nanoseconds.
let trigger = TimerTrigger::Relative(timeout.as_nanos() as u64 / 100);
let timeout_hundred_nanos = timeout.as_nanos() / 100;
// Check if the timeout is too large to fit into an u64.
if timeout_hundred_nanos > u64::MAX as u128 {
bail!("timeout duration overflow");
}
// Set a timer to trigger after the specified duration.
let trigger = TimerTrigger::Relative(timeout_hundred_nanos as u64);
uefi::boot::set_timer(&timer_event, trigger).context("unable to set timeout timer")?;
let mut events = vec![timer_event, key_event];
@@ -50,6 +61,7 @@ fn read(input: &mut Input, timeout: &Duration) -> Result<MenuOperation> {
.context("unable to wait for event")?;
// Close the timer event that we acquired.
// We don't need to close the key event because it is owned globally.
if let Some(timer_event) = events.into_iter().next() {
uefi::boot::close_event(timer_event).context("unable to close timer event")?;
}
@@ -152,7 +164,15 @@ fn select_with_input<'a>(
/// Shows a boot menu to select a bootable entry to boot.
/// The actual work is done internally in [select_with_input] which is called
/// within the context of the standard input device.
pub fn select(timeout: Duration, entries: &[BootableEntry]) -> Result<&BootableEntry> {
pub fn select<'live>(
timer: &'live PlatformTimer,
timeout: Duration,
entries: &'live [BootableEntry],
) -> Result<&'live BootableEntry> {
// Notify the bootloader interface that we are about to display the menu.
BootloaderInterface::mark_menu(timer)
.context("unable to mark menu display in bootloader interface")?;
// Acquire the standard input device and run the boot menu.
uefi::system::with_stdin(move |input| select_with_input(input, timeout, entries))
}

View File

@@ -96,7 +96,7 @@ pub trait OptionsRepresentable {
let maybe_next = iterator.peek();
// 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
&& !next.starts_with("--")
{

4
src/platform.rs Normal file
View File

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

89
src/platform/timer.rs Normal file
View File

@@ -0,0 +1,89 @@
// Referenced https://github.com/sheroz/tick_counter (MIT license) as a baseline.
// Architecturally modified to support UEFI and remove x86 (32-bit) support.
use std::time::Duration;
/// Support for aarch64 timers.
#[cfg(target_arch = "aarch64")]
pub mod aarch64;
/// Support for x86_64 timers.
#[cfg(target_arch = "x86_64")]
pub mod x86_64;
/// The tick frequency of the platform.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum TickFrequency {
/// The platform provides the tick frequency.
Hardware(u64),
/// The tick frequency is measured internally.
Measured(u64, Duration),
}
impl TickFrequency {
/// Acquire the tick frequency reported by the platform.
fn ticks(&self) -> u64 {
match self {
TickFrequency::Hardware(frequency) => *frequency,
TickFrequency::Measured(frequency, _) => *frequency,
}
}
/// Calculate the nanoseconds represented by a tick.
fn nanos(&self) -> f64 {
1.0e9_f64 / (self.ticks() as f64)
}
/// Produce a duration from the provided elapsed `ticks` value.
fn duration(&self, ticks: u64) -> Duration {
let accuracy = self.nanos();
let nanos = ticks as f64 * accuracy;
Duration::from_nanos(nanos as u64)
}
}
/// Acquire the tick value reported by the platform.
fn arch_ticks() -> u64 {
#[cfg(target_arch = "aarch64")]
return aarch64::ticks();
#[cfg(target_arch = "x86_64")]
return x86_64::ticks();
}
/// Acquire the tick frequency reported by the platform.
fn arch_frequency() -> TickFrequency {
#[cfg(target_arch = "aarch64")]
return aarch64::frequency();
#[cfg(target_arch = "x86_64")]
return x86_64::frequency();
}
/// Platform timer that allows measurement of the elapsed time.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct PlatformTimer {
/// The start tick value.
start: u64,
/// The tick frequency of the platform.
frequency: TickFrequency,
}
impl PlatformTimer {
/// Start a platform timer at the current instant.
pub fn start() -> Self {
Self {
start: arch_ticks(),
frequency: arch_frequency(),
}
}
/// Measure the elapsed duration since the hardware started ticking upwards.
pub fn elapsed_since_lifetime(&self) -> Duration {
self.frequency.duration(arch_ticks())
}
/// Measure the elapsed duration since the timer was started.
pub fn elapsed_since_start(&self) -> Duration {
let duration = arch_ticks().wrapping_sub(self.start);
self.frequency.duration(duration)
}
}

View File

@@ -0,0 +1,33 @@
use crate::platform::timer::TickFrequency;
use std::arch::asm;
/// Reads the cntvct_el0 counter and returns the value.
pub fn ticks() -> u64 {
let counter: u64;
unsafe {
asm!("mrs x0, cntvct_el0", out("x0") counter);
}
counter
}
/// We can use the actual ticks value as our start value.
pub fn start() -> u64 {
ticks()
}
/// We can use the actual ticks value as our stop value.
pub fn stop() -> u64 {
ticks()
}
/// Our frequency is provided by cntfrq_el0 on the platform.
pub fn frequency() -> TickFrequency {
let frequency: u64;
unsafe {
asm!(
"mrs x0, cntfrq_el0",
out("x0") frequency
);
}
TickFrequency::Hardware(frequency)
}

View File

@@ -0,0 +1,66 @@
use crate::platform::timer::TickFrequency;
use core::arch::asm;
use std::time::Duration;
/// We will measure the frequency of the timer based on 1000 microseconds.
/// This will result in a call to BS->Stall(1000) in the end.
const MEASURE_FREQUENCY_DURATION: Duration = Duration::from_micros(1000);
/// Read the number of ticks from the platform timer.
pub fn ticks() -> u64 {
let mut eax: u32;
let mut edx: u32;
unsafe {
asm!("rdtsc", out("eax") eax, out("edx") edx);
}
(edx as u64) << 32 | eax as u64
}
/// Read the starting number of ticks from the platform timer.
pub fn start() -> u64 {
let rax: u64;
unsafe {
asm!(
"mfence",
"lfence",
"rdtsc",
"shl rdx, 32",
"or rax, rdx",
out("rax") rax
);
}
rax
}
/// Read the ending number of ticks from the platform timer.
pub fn stop() -> u64 {
let rax: u64;
unsafe {
asm!(
"rdtsc",
"lfence",
"shl rdx, 32",
"or rax, rdx",
out("rax") rax
);
}
rax
}
/// Measure the frequency of the platform timer.
fn measure_frequency(duration: &Duration) -> u64 {
let start = start();
uefi::boot::stall(*duration);
let stop = stop();
let elapsed = stop.wrapping_sub(start) as f64;
(elapsed / duration.as_secs_f64()) as u64
}
/// Acquire the platform timer frequency.
/// On x86_64, this is slightly expensive, so it should be done once.
pub fn frequency() -> TickFrequency {
let frequency = measure_frequency(&MEASURE_FREQUENCY_DURATION);
TickFrequency::Measured(frequency, MEASURE_FREQUENCY_DURATION)
}

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,10 +1,13 @@
use anyhow::{Context, Result};
use std::ops::Deref;
use uefi::boot::SearchType;
use uefi::fs::{FileSystem, Path};
use uefi::proto::device_path::text::{AllowShortcuts, DevicePathFromText, DisplayOnly};
use uefi::proto::device_path::{DevicePath, PoolDevicePath};
use uefi::proto::media::fs::SimpleFileSystem;
use uefi::{CString16, Handle};
use uefi::proto::media::partition::PartitionInfo;
use uefi::{CString16, Guid, Handle};
use uefi_raw::Status;
/// Support code for the EFI framebuffer.
pub mod framebuffer;
@@ -12,6 +15,9 @@ pub mod framebuffer;
/// Support code for the media loader protocol.
pub mod media_loader;
/// Support code for EFI variables.
pub mod variables;
/// Parses the input `path` as a [DevicePath].
/// Uses the [DevicePathFromText] protocol exclusively, and will fail if it cannot acquire the protocol.
pub fn text_to_device_path(path: &str) -> Result<PoolDevicePath> {
@@ -98,10 +104,24 @@ pub struct ResolvedPath {
pub filesystem_handle: Handle,
}
impl ResolvedPath {
/// Read the file specified by this path into a buffer and return it.
pub fn read_file(&self) -> Result<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.
/// Uses `default_root_path` as the base root if one is not specified in the path.
/// 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 path_has_device = path
.node_iter()
@@ -117,6 +137,9 @@ pub fn resolve_path(default_root_path: &DevicePath, input: &str) -> Result<Resol
if !input.starts_with('\\') {
input.insert(0, '\\');
}
let default_root_path = default_root_path.context("unable to get default root path")?;
input.insert_str(
0,
device_path_root(default_root_path)
@@ -131,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())
.context("unable to convert root to path")?
.to_boxed();
let mut root_path = root_path.as_ref();
let handle = uefi::boot::locate_device_path::<SimpleFileSystem>(&mut root_path)
let root_path = root_path.as_ref();
// 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")?;
let subpath = device_path_subpath(path.deref()).context("unable to get device subpath")?;
Ok(ResolvedPath {
@@ -150,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
/// filesystem handle, so care must be taken to call this function outside a scope with
/// 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 fs = uefi::boot::open_protocol_exclusive::<SimpleFileSystem>(resolved.filesystem_handle)
.context("unable to open filesystem protocol")?;
let mut fs = FileSystem::new(fs);
let path = resolved
.sub_path
.to_string(DisplayOnly(false), AllowShortcuts(false))?;
let content = fs.read(Path::new(&path));
content.context("unable to read file contents")
resolved.read_file()
}
/// Filter a string-like Option `input` such that an empty string is [None].
@@ -181,3 +200,75 @@ pub fn combine_options<T: AsRef<str>>(options: impl Iterator<Item = T>) -> Strin
pub fn unique_hash(input: &str) -> String {
sha256::digest(input.as_bytes())
}
/// Represents the type of partition GUID that can be retrieved.
#[derive(PartialEq, Eq)]
pub enum PartitionGuidForm {
/// The partition GUID is the unique partition GUID.
Partition,
/// The partition GUID is the partition type GUID.
PartitionType,
}
/// 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.
/// If the GUID is all zeros, this will return None.
pub fn partition_guid(path: &DevicePath, form: PartitionGuidForm) -> Result<Option<Guid>> {
// Clone the path so we can pass it to the UEFI stack.
let path = path.to_boxed();
let result = uefi::boot::locate_device_path::<PartitionInfo>(&mut &*path);
let handle = match result {
Ok(handle) => Ok(Some(handle)),
Err(error) => {
// If the error is NOT_FOUND or UNSUPPORTED, we can return None.
// These are non-fatal errors.
if error.status() == Status::NOT_FOUND || error.status() == Status::UNSUPPORTED {
Ok(None)
} else {
Err(error)
}
}
}
.context("unable to locate device path")?;
// If we have the handle, we can try to open the partition info protocol.
if let Some(handle) = handle {
// Open the partition info protocol.
let partition_info = uefi::boot::open_protocol_exclusive::<PartitionInfo>(handle)
.context("unable to open partition info protocol")?;
// Find the unique partition GUID.
// If this is not a GPT partition, this will produce None.
Ok(partition_info
.gpt_partition_entry()
.map(|entry| match form {
// Match the form of the partition GUID.
PartitionGuidForm::Partition => entry.unique_partition_guid,
PartitionGuidForm::PartitionType => entry.partition_type_guid.0,
})
.filter(|guid| !guid.is_zero()))
} else {
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")
}
}
}
}

View File

@@ -51,6 +51,11 @@ impl MediaLoaderHandle {
/// The next call will pass a buffer of the right size, and we should copy
/// data into that buffer, checking whether it is safe to copy based on
/// the buffer size.
///
/// SAFETY: `this.address` and `this.length` are set by leaking a Box<[u8]>, so we can
/// be sure their pointers are valid when this is called. The caller must call this function
/// while inside UEFI boot services to ensure pointers are valid. Copying to `buffer` is
/// assumed valid because the caller must ensure `buffer` is valid by function contract.
unsafe extern "efiapi" fn load_file(
this: *mut MediaLoaderProtocol,
file_path: *const DevicePathProtocol,
@@ -155,7 +160,7 @@ impl MediaLoaderHandle {
// Install a protocol interface for the device path.
// This ensures it can be located by other EFI programs.
let mut handle = unsafe {
let primary_handle = unsafe {
uefi::boot::install_protocol_interface(
None,
&DevicePathProtocol::GUID,
@@ -178,25 +183,54 @@ impl MediaLoaderHandle {
let protocol = Box::leak(protocol);
// Install a protocol interface for the load file protocol for the media loader protocol.
handle = unsafe {
let secondary_handle = unsafe {
uefi::boot::install_protocol_interface(
Some(handle),
Some(primary_handle),
&LoadFile2Protocol::GUID,
protocol as *mut _ as *mut c_void,
// The UEFI API expects an opaque pointer here.
protocol as *mut MediaLoaderProtocol as *mut c_void,
)
}
.context("unable to install media loader load file handle")?;
};
// Check if the media loader is registered.
// If it is not, we can't continue safely because something went wrong.
if !Self::already_registered(guid)? {
bail!("media loader not registered when expected to be registered");
// If installing the second protocol interface failed, we need to clean up after ourselves.
if secondary_handle.is_err() {
// Uninstall the protocol interface for the device path protocol.
// SAFETY: If we have reached this point, we know that the protocol is registered.
// If this fails, we have no choice but to leak memory. The error will be shown
// to the user, so at least they can see it. In most cases, catching this error
// will exit, so leaking is safe.
unsafe {
uefi::boot::uninstall_protocol_interface(
primary_handle,
&DevicePathProtocol::GUID,
path.as_ffi_ptr() as *mut c_void,
)
.context(
"unable to uninstall media loader device path handle, this will leak memory",
)?;
}
// SAFETY: We know that the protocol is leaked, so we can safely take a reference to it.
let protocol = unsafe { Box::from_raw(protocol) };
// SAFETY: We know that the data is leaked, so we can safely take a reference to it.
let data = unsafe { Box::from_raw(data) };
// SAFETY: We know that the path is leaked, so we can safely take a reference to it.
let path = unsafe { Box::from_raw(path) };
// Drop all the allocations explicitly to clarify the lifetime.
drop(protocol);
drop(data);
drop(path);
}
// If installing the second protocol interface failed, this will return the error.
// We should have already cleaned up after ourselves, so this is safe.
secondary_handle.context("unable to install media loader load file handle")?;
// Return a handle to the media loader.
Ok(Self {
guid,
handle,
handle: primary_handle,
protocol,
path,
})

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