40 Commits

Author SHA1 Message Date
2a9c9f6907 Merge pull request #34 from edera-dev/chore/release-0.0.25
chore(release): sprout: version 0.0.25
2025-11-14 10:49:09 -05:00
16755acdfe chore(release): sprout: version 0.0.25 2025-11-14 10:45:16 -05:00
ebb1f0bb44 Merge pull request #33 from edera-dev/chore/upgrade-deps
chore(deps): upgrade rust to 1.91.1 and bump cargo and docker deps
2025-11-14 00:30:48 -05:00
ecba8a5e02 chore(deps): upgrade rust to 1.91.1 and bump cargo and docker deps 2025-11-14 00:24:09 -05:00
0028e3eefc Merge pull request #32 from rkratky/typo 2025-11-12 10:15:49 -05:00
Robert Krátký
8603794c44 Fix a typo: grubaa64.efi -> grubx64.efi 2025-11-12 16:11:13 +01:00
4fae4080a2 Merge pull request #29 from edera-dev/dependabot/github_actions/actions-updates-1be5372544
chore(deps): bump the actions-updates group with 2 updates
2025-11-10 10:09:58 -05:00
dependabot[bot]
da5e0daa51 chore(deps): bump the actions-updates group with 2 updates
Bumps the actions-updates group with 2 updates: [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv) and [github/codeql-action](https://github.com/github/codeql-action).


Updates `astral-sh/setup-uv` from 7.1.1 to 7.1.2
- [Release notes](https://github.com/astral-sh/setup-uv/releases)
- [Commits](2ddd2b9cb3...85856786d1)

Updates `github/codeql-action` from 4.31.0 to 4.31.2
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](4e94bd11f7...0499de31b9)

---
updated-dependencies:
- dependency-name: astral-sh/setup-uv
  dependency-version: 7.1.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions-updates
- dependency-name: github/codeql-action
  dependency-version: 4.31.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-10 12:27:22 +00:00
0fb54a948b chore(doc): add fedora setup guide and tweak existing guides 2025-11-06 23:51:57 -05:00
c4475ad42d fix(boot): ensure top-level error is printed 2025-11-06 13:04:05 -05:00
5e4e86857c sprout: version 0.0.24 2025-11-06 12:03:46 -05:00
d3cad25749 chore(deps): upgrade uefi dependencies 2025-11-06 12:03:19 -05:00
22780e6102 chore(eficore): decouple the shim support from the image load callsites 2025-11-06 11:52:00 -05:00
c52d61b07f fix(eficore): handle possible leak when the first install protocol interface fails 2025-11-04 14:32:32 -05:00
0ce6ffa3da sprout: version 0.0.23 2025-11-04 12:16:53 -05:00
a1028c629d fix(eficore/env): improve quirk handling for dell systems 2025-11-03 23:58:48 -05:00
503a9cba0a chore(code): move load options parsing to crates/eficore 2025-11-03 23:45:35 -05:00
532fb38d5a chore(code): move crates/sprout to crates/boot and name it edera-sprout-boot 2025-11-03 22:52:54 -05:00
9a803ad355 chore(code): sbat section generator build tool 2025-11-03 22:37:06 -05:00
632781abbf chore(code): split much of the efi support code to crates/eficore 2025-11-03 20:47:21 -05:00
48497700d8 fix(sprout): make secure boot warning more specific 2025-11-03 15:31:44 -05:00
34ac57d291 sprout: version 0.0.22 2025-11-03 14:47:59 -05:00
37abe49347 feat(sprout): implement custom logger which shortens output 2025-11-03 14:45:48 -05:00
79615f7436 chore(docs): add openSUSE Secure Boot guide 2025-11-03 04:49:55 -05:00
7a7d92ef70 chore(hack): add keyboard and mouse to dev qemu 2025-11-03 03:41:03 -05:00
b34c171ccb fix(hack): remove xen images during clean 2025-11-03 03:18:23 -05:00
384c1e7eaf chore(docker): swap rust for docker builds to 1.91.0 2025-11-03 03:08:09 -05:00
0b7b5066e4 chore(workflows): align on push/pull_request events across workflows 2025-11-03 03:01:57 -05:00
ba634ed68a fix(platform/timer): on x86_64, elide usage of asm!() and use _rdtsc() intrinsic 2025-11-03 02:57:22 -05:00
be63c5171b chore(doc): add clarifying comments in vercmp 2025-11-03 02:46:41 -05:00
f740c35568 fix(tpm): add clarifying parentheses to version check 2025-11-03 02:37:52 -05:00
8a0b70a99b chore(doc): add documentation to VariableController::remove 2025-11-03 02:35:57 -05:00
223a00563e chore(menu): add note as to why we match on the timer event 2025-11-03 02:35:01 -05:00
029e59b209 sprout: version 0.0.21 2025-11-03 02:15:23 -05:00
bde1cd01c8 Merge pull request #28 from edera-dev/experiment/no-std
feat(sprout): introduce no_std sprout which uses stable rust
2025-11-02 23:11:52 -08:00
0017d7874d feat(sprout): introduce no_std sprout which uses stable rust 2025-11-03 02:04:21 -05:00
1c2acdc568 chore(build): pin rust-toolchain to nightly-2025-11-03 2025-11-03 00:37:04 -05:00
1f322ff4bf chore(workflows): publish should upload and attest all artifacts in a single zip 2025-11-03 00:32:54 -05:00
0bb7d7ccb1 sprout: version 0.0.20 2025-11-03 00:10:20 -05:00
74b6a8deb3 chore(workflows): release workflow should attest all artifacts together 2025-11-03 00:06:53 -05:00
101 changed files with 1503 additions and 782 deletions

View File

@@ -1,10 +1,12 @@
name: zizmor
on:
push:
branches: ["main"]
pull_request:
branches: ["**"]
branches:
- main
push:
branches:
- main
permissions:
contents: read # Needed to checkout the repository.
@@ -33,7 +35,7 @@ jobs:
persist-credentials: false
- name: setup uv
uses: astral-sh/setup-uv@2ddd2b9cb38ad8efd50337e8ab201519a34c9f24 # v7.1.1
uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2
- name: zizmor
run: uvx zizmor --pedantic --format sarif . > results.sarif
@@ -41,7 +43,7 @@ jobs:
GH_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
- name: upload
uses: github/codeql-action/upload-sarif@4e94bd11f71e507f7f87df81788dff88d1dacbfb # v4.31.0
uses: github/codeql-action/upload-sarif@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2
with:
sarif_file: results.sarif
category: zizmor

View File

@@ -1,10 +1,12 @@
name: codeql
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
branches:
- main
push:
branches:
- main
schedule:
- cron: '33 16 * * 0'
@@ -45,13 +47,13 @@ jobs:
persist-credentials: false
- name: initialize codeql
uses: github/codeql-action/init@4e94bd11f71e507f7f87df81788dff88d1dacbfb # v4.31.0
uses: github/codeql-action/init@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
config-file: ./.github/codeql/codeql-config.yaml
- name: perform codeql analysis
uses: github/codeql-action/analyze@4e94bd11f71e507f7f87df81788dff88d1dacbfb # v4.31.0
uses: github/codeql-action/analyze@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2
with:
category: "/language:${{matrix.language}}"

View File

@@ -1,19 +1,12 @@
name: publish
on:
push:
branches:
- main
pull_request:
branches:
- main
paths:
- bin/**
- src/**
- Cargo.*
- rust-toolchain.toml
- .github/workflows/publish.yaml
push:
branches:
- main
permissions:
contents: read # Needed to checkout the repository.
@@ -48,28 +41,15 @@ jobs:
- name: 'assemble artifacts'
run: ./hack/assemble.sh
- name: 'upload sprout-x86_64.efi.zip artifact'
id: upload-sprout-x86_64-efi
- name: 'upload artifacts'
id: upload
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with:
name: sprout-x86_64.efi.zip
path: target/assemble/sprout-x86_64.efi
name: artifacts
path: target/assemble/*
- name: 'upload sprout-aarch64.efi.zip artifact'
id: upload-sprout-aarch64-efi
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with:
name: sprout-aarch64.efi.zip
path: target/assemble/sprout-aarch64.efi
- name: 'attest sprout-x86_64.efi.zip artifact'
- name: 'attest artifacts'
uses: actions/attest-build-provenance@977bb373ede98d70efdf65b84cb5f73e068dcc2a # v3.0.0
with:
subject-name: sprout-x86_64.efi.zip
subject-digest: "sha256:${{ steps.upload-sprout-x86_64-efi.outputs.artifact-digest }}"
- name: 'attest sprout-aarch64.efi.zip artifact'
uses: actions/attest-build-provenance@977bb373ede98d70efdf65b84cb5f73e068dcc2a # v3.0.0
with:
subject-name: sprout-aarch64.efi.zip
subject-digest: "sha256:${{ steps.upload-sprout-aarch64-efi.outputs.artifact-digest }}"
subject-name: artifacts.zip
subject-digest: "sha256:${{ steps.upload.outputs.artifact-digest }}"

View File

@@ -38,18 +38,13 @@ jobs:
run: |
cargo version
- name: 'assemble artifacts'
- name: 'assemble release artifacts'
run: ./hack/assemble.sh
- name: 'attest sprout-x86_64.efi artifact'
- name: 'attest release artifacts'
uses: actions/attest-build-provenance@977bb373ede98d70efdf65b84cb5f73e068dcc2a # v3.0.0
with:
subject-path: target/assemble/sprout-x86_64.efi
- name: 'attest sprout-aarch64.efi artifact'
uses: actions/attest-build-provenance@977bb373ede98d70efdf65b84cb5f73e068dcc2a # v3.0.0
with:
subject-path: target/assemble/sprout-aarch64.efi
subject-path: target/assemble/*
- name: 'generate cultivator token'
uses: actions/create-github-app-token@bf559f85448f9380bcfa2899dbdc01eb5b37be3a # v3.0.0-beta.2

108
Cargo.lock generated
View File

@@ -46,9 +46,9 @@ dependencies = [
[[package]]
name = "crypto-common"
version = "0.1.6"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
dependencies = [
"generic-array",
"typenum",
@@ -65,12 +65,13 @@ dependencies = [
]
[[package]]
name = "edera-sprout"
version = "0.0.19"
name = "edera-sprout-boot"
version = "0.0.25"
dependencies = [
"anyhow",
"bitflags",
"edera-sprout-build",
"edera-sprout-config",
"edera-sprout-eficore",
"hex",
"log",
"sha2",
@@ -79,57 +80,61 @@ dependencies = [
"uefi-raw",
]
[[package]]
name = "edera-sprout-build"
version = "0.0.25"
[[package]]
name = "edera-sprout-config"
version = "0.0.19"
version = "0.0.25"
dependencies = [
"serde",
]
[[package]]
name = "equivalent"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
name = "edera-sprout-eficore"
version = "0.0.25"
dependencies = [
"anyhow",
"bitflags",
"log",
"shlex",
"spin",
"uefi",
"uefi-raw",
]
[[package]]
name = "generic-array"
version = "0.14.9"
version = "0.14.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2"
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
dependencies = [
"typenum",
"version_check",
]
[[package]]
name = "hashbrown"
version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d"
[[package]]
name = "hex"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "indexmap"
version = "2.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f"
dependencies = [
"equivalent",
"hashbrown",
]
[[package]]
name = "libc"
version = "0.2.177"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976"
[[package]]
name = "lock_api"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
dependencies = [
"scopeguard",
]
[[package]]
name = "log"
version = "0.4.28"
@@ -167,13 +172,19 @@ dependencies = [
[[package]]
name = "quote"
version = "1.0.41"
version = "1.0.42"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1"
checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f"
dependencies = [
"proc-macro2",
]
[[package]]
name = "scopeguard"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "serde"
version = "1.0.228"
@@ -225,10 +236,25 @@ dependencies = [
]
[[package]]
name = "syn"
version = "2.0.108"
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da58917d35242480a05c2897064da0a80589a2a0476c9a3f2fdc83b53502e917"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "spin"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591"
dependencies = [
"lock_api",
]
[[package]]
name = "syn"
version = "2.0.110"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a99801b5bd34ede4cf3fc688c5919368fea4e4814a4664359503e6015b280aea"
dependencies = [
"proc-macro2",
"quote",
@@ -241,12 +267,10 @@ version = "0.9.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0dc8b1fb61449e27716ec0e1bdf0f6b8f3e8f6b05391e8497b8b6d7804ea6d8"
dependencies = [
"indexmap",
"serde_core",
"serde_spanned",
"toml_datetime",
"toml_parser",
"toml_writer",
"winnow",
]
@@ -268,12 +292,6 @@ dependencies = [
"winnow",
]
[[package]]
name = "toml_writer"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df8b2b54733674ad286d16267dcfc7a71ed5c776e4ac7aa3c3e2561f7c637bf2"
[[package]]
name = "typenum"
version = "1.19.0"
@@ -291,9 +309,9 @@ dependencies = [
[[package]]
name = "uefi"
version = "0.36.0"
version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f123e69767fc287c44d70ee19af3b39d1bfb735dbaff5090e95b5b13cd656d16"
checksum = "71fe9058b73ee2b6559524af9e33199c13b2485ddbf3ad1181b68051cdc50c17"
dependencies = [
"bitflags",
"cfg-if",
@@ -318,9 +336,9 @@ dependencies = [
[[package]]
name = "uefi-raw"
version = "0.12.0"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8aff2f4f2b556a36a201d335a1e0a57754967a96857b1f47a52d5a23825cac84"
checksum = "7f64fe59e11af447d12fd60a403c74106eb104309f34b4c6dbce6e927d97da9d"
dependencies = [
"bitflags",
"uguid",

View File

@@ -1,27 +1,56 @@
[workspace]
members = [
"crates/boot",
"crates/build",
"crates/config",
"crates/sprout",
"crates/eficore",
]
resolver = "3"
[workspace.package]
license = "Apache-2.0"
version = "0.0.19"
version = "0.0.25"
homepage = "https://sprout.edera.dev"
repository = "https://github.com/edera-dev/sprout"
edition = "2024"
[workspace.dependencies]
anyhow = "1.0.100"
bitflags = "2.10.0"
hex = "0.4.3"
log = "0.4.28"
serde = "1.0.228"
sha2 = "0.10.9"
toml = "0.9.8"
uefi = "0.36.0"
uefi-raw = "0.12.0"
spin = "0.10.0"
uefi-raw = "0.13.0"
[workspace.dependencies.anyhow]
version = "1.0.100"
default-features = false
[workspace.dependencies.hex]
version = "0.4.3"
default-features = false
features = ["alloc"]
[workspace.dependencies.serde]
version = "1.0.228"
default-features = false
features = ["alloc", "derive"]
[workspace.dependencies.sha2]
version = "0.10.9"
default-features = false
[workspace.dependencies.shlex]
version = "1.3.0"
default-features = false
[workspace.dependencies.toml]
version = "0.9.8"
default-features = false
features = ["serde", "parse"]
[workspace.dependencies.uefi]
version = "0.36.1"
default-features = false
features = ["alloc", "global_allocator", "panic_handler"]
# Common build profiles
# NOTE: We have to compile everything for opt-level = 2 due to optimization passes

View File

@@ -5,16 +5,23 @@ This guide is a work in progress.
## Development Setup
You can use any Rust development environment to develop Sprout.
Rustup is recommended as the Rust toolchain manager to manage Rust versions and targets.
Sprout currently requires Rust nightly to support uefi_std. See [uefi_std](https://doc.rust-lang.org/beta/rustc/platform-support/unknown-uefi.html) for more details.
We currently only support `x86_64-unknown-uefi` and `aarch64-unknown-uefi` targets.
To test your changes in QEMU, please run `./hack/dev/boot.sh`, you can specify `x86_64` or `aarch64`
as an argument to boot.sh to boot the specified architecture.
## Crate Structure
Sprout is split into multiple crates:
- `edera-sprout-boot` as `crates/boot`: Bootloader entrypoint for Sprout.
- `edera-sprout-build` at `crates/build`: Build logic for Sprout.
- `edera-sprout-config` at `crates/config`: Serialization structures for the Sprout configuration file.
- `edera-sprout-eficore` at `crates/eficore`: Core library for Sprout EFI code.
It is intended that overtime Sprout will be split into even more crates.
## Hack Scripts
You can use the `./hack` scripts to run common development tasks:

View File

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

View File

@@ -47,8 +47,10 @@ We recommend running Sprout without Secure Boot for development, and with Secure
| Operating System | Secure Boot Enabled | Link |
|------------------|---------------------|-------------------------------------------------------|
| Ubuntu | ✅ | [Setup Guide](./docs/setup/signed/ubuntu.md) |
| Fedora | ✅ | [Setup Guide](./docs/setup/signed/fedora.md) |
| Debian | ✅ | [Setup Guide](./docs/setup/signed/debian.md) |
| Ubuntu | ✅ | [Setup Guide](./docs/setup/signed/ubuntu.md) |
| openSUSE | ✅ | [Setup Guide](./docs/setup/signed/opensuse.md) |
| Fedora | ❌ | [Setup Guide](./docs/setup/unsigned/fedora.md) |
| Alpine Edge | ❌ | [Setup Guide](./docs/setup/unsigned/alpine-edge.md) |
| Generic Linux | ❌ | [Setup Guide](./docs/setup/unsigned/generic-linux.md) |

View File

@@ -1,6 +1,6 @@
[package]
name = "edera-sprout"
description = "Modern UEFI bootloader"
name = "edera-sprout-boot"
description = "Sprout: Modern UEFI Bootloader"
license.workspace = true
version.workspace = true
homepage.workspace = true
@@ -9,19 +9,17 @@ edition.workspace = true
[dependencies]
anyhow.workspace = true
bitflags.workspace = true
edera-sprout-config.path = "../config"
edera-sprout-eficore.path = "../eficore"
hex.workspace = true
sha2.workspace = true
toml.workspace = true
log.workspace = true
uefi.workspace = true
uefi-raw.workspace = true
[dependencies.uefi]
workspace = true
features = ["alloc", "logger"]
[dependencies.uefi-raw]
workspace = true
[build-dependencies]
edera-sprout-build.path = "../build"
[[bin]]
name = "sprout"

7
crates/boot/build.rs Normal file
View File

@@ -0,0 +1,7 @@
use edera_sprout_build::generate_sbat_module;
/// Build script entry point for Sprout.
fn main() {
// Generate the sbat.generated.rs file.
generate_sbat_module();
}

View File

@@ -1,6 +1,6 @@
use crate::context::SproutContext;
use alloc::rc::Rc;
use anyhow::{Context, Result, bail};
use std::rc::Rc;
/// EFI chainloader action.
pub mod chainload;

View File

@@ -1,13 +1,15 @@
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;
use alloc::boxed::Box;
use alloc::rc::Rc;
use anyhow::{Context, Result, bail};
use edera_sprout_config::actions::chainload::ChainloadConfiguration;
use eficore::bootloader_interface::BootloaderInterface;
use eficore::loader::source::ImageSource;
use eficore::loader::{ImageLoadRequest, ImageLoader};
use eficore::media_loader::MediaLoaderHandle;
use eficore::media_loader::constants::linux::LINUX_EFI_INITRD_MEDIA_GUID;
use log::error;
use std::rc::Rc;
use uefi::CString16;
use uefi::proto::loaded_image::LoadedImage;
@@ -17,18 +19,22 @@ pub fn chainload(context: Rc<SproutContext>, configuration: &ChainloadConfigurat
let sprout_image = uefi::boot::image_handle();
// Resolve the path to the image to chainload.
let resolved = utils::resolve_path(
let resolved = eficore::path::resolve_path(
Some(context.root().loaded_image_path()?),
&context.stamp(&configuration.path),
)
.context("unable to resolve chainload path")?;
// Load the image to chainload using the shim support integration.
// Create a new image load request with the current image and the resolved path.
let request = ImageLoadRequest::new(sprout_image, ImageSource::ResolvedPath(&resolved));
// Load the image to chainload using the image loader support module.
// 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))?;
let image = ImageLoader::load(request)?;
// Open the LoadedImage protocol of the image to chainload.
let mut loaded_image_protocol = uefi::boot::open_protocol_exclusive::<LoadedImage>(image)
let mut loaded_image_protocol =
uefi::boot::open_protocol_exclusive::<LoadedImage>(*image.handle())
.context("unable to open loaded image protocol")?;
// Stamp and combine the options to pass to the image.
@@ -67,8 +73,10 @@ 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(Some(context.root().loaded_image_path()?), &linux_initrd)
let content = eficore::path::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())
@@ -84,7 +92,7 @@ pub fn chainload(context: Rc<SproutContext>, configuration: &ChainloadConfigurat
// 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
// after the optional initrd has been unregistered.
let result = uefi::boot::start_image(image);
let result = uefi::boot::start_image(*image.handle());
// Unregister the initrd if it was registered.
if let Some(initrd_handle) = initrd_handle

View File

@@ -1,21 +1,20 @@
use std::rc::Rc;
use crate::{
actions,
context::SproutContext,
utils::{
self,
media_loader::{
utils::{self},
};
use alloc::rc::Rc;
use alloc::string::{String, ToString};
use alloc::{format, vec};
use anyhow::{Context, Result};
use edera_sprout_config::actions::chainload::ChainloadConfiguration;
use edera_sprout_config::actions::edera::EderaConfiguration;
use eficore::media_loader::{
MediaLoaderHandle,
constants::xen::{
XEN_EFI_CONFIG_MEDIA_GUID, XEN_EFI_KERNEL_MEDIA_GUID, XEN_EFI_RAMDISK_MEDIA_GUID,
},
},
},
};
use anyhow::{Context, Result};
use edera_sprout_config::actions::chainload::ChainloadConfiguration;
use edera_sprout_config::actions::edera::EderaConfiguration;
use log::error;
use uefi::Guid;
@@ -78,7 +77,8 @@ 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(Some(context.root().loaded_image_path()?), &path)
let content =
eficore::path::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

@@ -1,8 +1,8 @@
use crate::context::SproutContext;
use alloc::rc::Rc;
use anyhow::Result;
use edera_sprout_config::actions::print::PrintConfiguration;
use log::info;
use std::rc::Rc;
/// Executes the print action with the specified `configuration` inside the provided `context`.
pub fn print(context: Rc<SproutContext>, configuration: &PrintConfiguration) -> Result<()> {

View File

@@ -1,4 +1,6 @@
use crate::utils;
use alloc::string::ToString;
use alloc::{format, vec};
use anyhow::{Context, Result};
use edera_sprout_config::RootConfiguration;
use edera_sprout_config::actions::ActionDeclaration;

View File

@@ -1,5 +1,9 @@
use crate::utils;
use crate::utils::vercmp;
use alloc::collections::BTreeMap;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use alloc::{format, vec};
use anyhow::{Context, Result};
use edera_sprout_config::RootConfiguration;
use edera_sprout_config::actions::ActionDeclaration;
@@ -7,7 +11,6 @@ use edera_sprout_config::actions::chainload::ChainloadConfiguration;
use edera_sprout_config::entries::EntryDeclaration;
use edera_sprout_config::generators::GeneratorDeclaration;
use edera_sprout_config::generators::list::ListConfiguration;
use std::collections::BTreeMap;
use uefi::CString16;
use uefi::fs::{FileSystem, Path, PathBuf};
use uefi::proto::device_path::DevicePath;

View File

@@ -1,4 +1,6 @@
use crate::utils;
use alloc::string::ToString;
use alloc::{format, vec};
use anyhow::{Context, Result};
use edera_sprout_config::RootConfiguration;
use edera_sprout_config::actions::ActionDeclaration;

View File

@@ -1,10 +1,10 @@
use crate::options::SproutOptions;
use crate::platform::tpm::PlatformTpm;
use crate::utils;
use alloc::vec::Vec;
use anyhow::{Context, Result, bail};
use core::ops::Deref;
use edera_sprout_config::{RootConfiguration, latest_version};
use eficore::platform::tpm::PlatformTpm;
use log::info;
use std::ops::Deref;
use toml::Value;
use uefi::proto::device_path::LoadedImageDevicePath;
@@ -20,7 +20,7 @@ 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(Some(&path), &options.config)
let content = eficore::path::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.

View File

@@ -1,11 +1,15 @@
use crate::options::SproutOptions;
use crate::platform::timer::PlatformTimer;
use alloc::boxed::Box;
use alloc::collections::{BTreeMap, BTreeSet};
use alloc::format;
use alloc::rc::Rc;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use anyhow::anyhow;
use anyhow::{Result, bail};
use core::cmp::Reverse;
use edera_sprout_config::actions::ActionDeclaration;
use std::cmp::Reverse;
use std::collections::{BTreeMap, BTreeSet};
use std::rc::Rc;
use eficore::platform::timer::PlatformTimer;
use uefi::proto::device_path::DevicePath;
/// The maximum number of iterations that can be performed in [SproutContext::finalize].

View File

@@ -1,11 +1,13 @@
use crate::context::SproutContext;
use crate::integrations::shim::{ShimInput, ShimSupport};
use crate::utils;
use alloc::collections::BTreeMap;
use alloc::format;
use alloc::rc::Rc;
use alloc::string::String;
use anyhow::{Context, Result};
pub(crate) use edera_sprout_config::drivers::DriverDeclaration;
use edera_sprout_config::drivers::DriverDeclaration;
use eficore::loader::source::ImageSource;
use eficore::loader::{ImageLoadRequest, ImageLoader};
use log::info;
use std::collections::BTreeMap;
use std::rc::Rc;
use uefi::boot::SearchType;
/// Loads the driver specified by the `driver` declaration.
@@ -14,20 +16,23 @@ fn load_driver(context: Rc<SproutContext>, driver: &DriverDeclaration) -> Result
let sprout_image = uefi::boot::image_handle();
// Resolve the path to the driver image.
let resolved = utils::resolve_path(
let resolved = eficore::path::resolve_path(
Some(context.root().loaded_image_path()?),
&context.stamp(&driver.path),
)
.context("unable to resolve path to driver")?;
// Load the driver image using the shim support integration.
// Create an image load request with the current image and the resolved path.
let request = ImageLoadRequest::new(sprout_image, ImageSource::ResolvedPath(&resolved));
// Load the driver image using the image loader support module.
// 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))?;
let image = ImageLoader::load(request)?;
// 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
// just a standard EFI image.
uefi::boot::start_image(image).context("unable to start driver image")?;
uefi::boot::start_image(*image.handle()).context("unable to start driver image")?;
Ok(())
}

View File

@@ -1,6 +1,7 @@
use crate::context::SproutContext;
use alloc::rc::Rc;
use alloc::string::{String, ToString};
use edera_sprout_config::entries::EntryDeclaration;
use std::rc::Rc;
/// Represents an entry that is stamped and ready to be booted.
#[derive(Clone)]

View File

@@ -1,7 +1,8 @@
use crate::context::SproutContext;
use alloc::rc::Rc;
use alloc::string::String;
use anyhow::{Result, bail};
use edera_sprout_config::extractors::ExtractorDeclaration;
use std::rc::Rc;
/// The filesystem device match extractor.
pub mod filesystem_device_match;

View File

@@ -1,10 +1,11 @@
use crate::context::SproutContext;
use crate::utils;
use alloc::rc::Rc;
use alloc::string::String;
use anyhow::{Context, Result, anyhow, bail};
use core::ops::Deref;
use core::str::FromStr;
use edera_sprout_config::extractors::filesystem_device_match::FilesystemDeviceMatchExtractor;
use std::ops::Deref;
use std::rc::Rc;
use std::str::FromStr;
use eficore::partition::PartitionGuidForm;
use uefi::fs::{FileSystem, Path};
use uefi::proto::device_path::DevicePath;
use uefi::proto::media::file::{File, FileSystemVolumeLabel};
@@ -47,7 +48,8 @@ pub fn extract(
.to_boxed();
// Fetch the partition uuid for this filesystem.
let partition_uuid = utils::partition_guid(&root, utils::PartitionGuidForm::Partition)
let partition_uuid =
eficore::partition::partition_guid(&root, PartitionGuidForm::Partition)
.context("unable to fetch the partition uuid of the filesystem")?;
// Compare the partition uuid to the parsed uuid.
@@ -72,7 +74,7 @@ pub fn extract(
// Fetch the partition type uuid for this filesystem.
let partition_type_uuid =
utils::partition_guid(&root, utils::PartitionGuidForm::PartitionType)
eficore::partition::partition_guid(&root, 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.
@@ -132,7 +134,7 @@ pub fn extract(
.context("unable to open filesystem device path")?;
let path = path.deref();
// Acquire the device path root as a string.
return utils::device_path_root(path).context("unable to get device path root");
return eficore::path::device_path_root(path).context("unable to get device path root");
}
// If there is a fallback value, use it at this point.

View File

@@ -1,9 +1,10 @@
use crate::context::SproutContext;
use crate::entries::BootableEntry;
use alloc::rc::Rc;
use alloc::vec::Vec;
use anyhow::Result;
use anyhow::bail;
use edera_sprout_config::generators::GeneratorDeclaration;
use std::rc::Rc;
/// The BLS generator.
pub mod bls;

View File

@@ -1,13 +1,15 @@
use crate::context::SproutContext;
use crate::entries::BootableEntry;
use crate::generators::bls::entry::BlsEntry;
use crate::utils;
use crate::utils::vercmp;
use alloc::format;
use alloc::rc::Rc;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use anyhow::{Context, Result};
use core::cmp::Ordering;
use core::str::FromStr;
use edera_sprout_config::generators::bls::BlsConfiguration;
use std::cmp::Ordering;
use std::rc::Rc;
use std::str::FromStr;
use uefi::cstr16;
use uefi::fs::{FileSystem, PathBuf};
use uefi::proto::device_path::text::{AllowShortcuts, DisplayOnly};
@@ -86,7 +88,8 @@ 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(Some(context.root().loaded_image_path()?), &path)
let bls_resolved =
eficore::path::resolve_path(Some(context.root().loaded_image_path()?), &path)
.context("unable to resolve bls path")?;
// Construct a filesystem path to the BLS entries directory.

View File

@@ -1,5 +1,6 @@
use alloc::string::{String, ToString};
use anyhow::{Error, Result};
use std::str::FromStr;
use core::str::FromStr;
/// Represents a parsed BLS entry.
/// Fields unrelated to Sprout are not included.

View File

@@ -1,8 +1,10 @@
use crate::context::SproutContext;
use crate::entries::BootableEntry;
use alloc::rc::Rc;
use alloc::string::ToString;
use alloc::vec::Vec;
use anyhow::Result;
use edera_sprout_config::generators::list::ListConfiguration;
use std::rc::Rc;
/// Generates a set of entries using the specified `list` configuration in the `context`.
pub fn generate(

View File

@@ -1,11 +1,14 @@
use crate::context::SproutContext;
use crate::entries::BootableEntry;
use crate::generators::list;
use alloc::collections::BTreeMap;
use alloc::rc::Rc;
use alloc::string::String;
use alloc::vec;
use alloc::vec::Vec;
use anyhow::Result;
use edera_sprout_config::generators::list::ListConfiguration;
use edera_sprout_config::generators::matrix::MatrixConfiguration;
use std::collections::BTreeMap;
use std::rc::Rc;
/// Builds out multiple generations of `input` based on a matrix style.
/// For example, if input is: {"x": ["a", "b"], "y": ["c", "d"]}

View File

@@ -1,26 +1,31 @@
#![doc = include_str!("../README.md")]
#![feature(uefi_std)]
/// The delay to wait for when an error occurs in Sprout.
const DELAY_ON_ERROR: Duration = Duration::from_secs(10);
#![no_std]
#![no_main]
extern crate alloc;
use crate::context::{RootContext, SproutContext};
use crate::entries::BootableEntry;
use crate::integrations::bootloader_interface::{BootloaderInterface, BootloaderInterfaceTimeout};
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 alloc::collections::BTreeMap;
use alloc::format;
use alloc::string::ToString;
use alloc::vec::Vec;
use anyhow::{Context, Result, bail};
use core::ops::Deref;
use core::time::Duration;
use edera_sprout_config::RootConfiguration;
use eficore::bootloader_interface::{BootloaderInterface, BootloaderInterfaceTimeout};
use eficore::partition::PartitionGuidForm;
use eficore::platform::timer::PlatformTimer;
use eficore::platform::tpm::PlatformTpm;
use eficore::secure::SecureBoot;
use eficore::setup;
use log::{error, info, warn};
use std::collections::BTreeMap;
use std::ops::Deref;
use std::time::Duration;
use uefi::entry;
use uefi::proto::device_path::LoadedImageDevicePath;
use uefi_raw::Status;
/// actions: Code that can be configured and executed by Sprout.
pub mod actions;
@@ -46,14 +51,11 @@ 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;
/// options: Parse the options of the Sprout executable.
pub mod options;
/// phases: Hooks into specific parts of the boot process.
pub mod phases;
@@ -61,23 +63,17 @@ 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;
/// options: Parse the options of the Sprout executable.
pub mod options;
/// utils: Utility functions that are used by other parts of Sprout.
pub mod utils;
/// The delay to wait for when an error occurs in Sprout.
const DELAY_ON_ERROR: Duration = Duration::from_secs(10);
/// 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.");
warn!("Sprout Secure Boot is in beta. Some functionality may not work as expected.");
}
// Start the platform timer.
@@ -128,7 +124,7 @@ fn run() -> Result<()> {
// 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)
eficore::partition::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.
@@ -373,23 +369,32 @@ fn run() -> Result<()> {
/// The main entrypoint of sprout.
/// It is possible this function will not return if actions that are executed
/// exit boot services or do not return control to sprout.
fn main() -> Result<()> {
#[entry]
fn efi_main() -> Status {
// Initialize the basic UEFI environment.
setup::init()?;
// If initialization fails, we will return ABORTED.
// NOTE: This function will also initialize the logger.
// The logger will panic if it is unable to initialize.
// It is guaranteed that if this returns, the logger is initialized.
if let Err(error) = setup::init() {
error!("unable to initialize environment: {}", error);
return Status::ABORTED;
}
// Run Sprout, then handle the error.
let result = run();
if let Err(ref error) = result {
// Print an error trace.
error!("sprout encountered an error");
error!("sprout encountered an error: {}", error);
for (index, stack) in error.chain().enumerate() {
error!("[{}]: {}", index, stack);
}
// Sleep to allow the user to read the error.
uefi::boot::stall(DELAY_ON_ERROR);
return Status::ABORTED;
}
// Sprout doesn't necessarily guarantee anything was booted.
// If we reach here, we will exit back to whoever called us.
Ok(())
Status::SUCCESS
}

View File

@@ -1,9 +1,10 @@
use crate::entries::BootableEntry;
use crate::integrations::bootloader_interface::BootloaderInterface;
use crate::platform::timer::PlatformTimer;
use alloc::vec;
use anyhow::{Context, Result, bail};
use core::time::Duration;
use eficore::bootloader_interface::BootloaderInterface;
use eficore::platform::timer::PlatformTimer;
use log::{info, warn};
use std::time::Duration;
use uefi::ResultExt;
use uefi::boot::TimerTrigger;
use uefi::proto::console::text::{Input, Key, ScanCode};
@@ -65,6 +66,7 @@ fn read(input: &mut Input, timeout: &Duration) -> Result<MenuOperation> {
// Close the timer event that we acquired.
// We don't need to close the key event because it is owned globally.
// This should always be called in practice as events are not modified by wait_for_event.
if let Some(timer_event) = events.into_iter().next() {
// Store the result of the close event so we can determine if we can safely assert it.
let close_event_result =

View File

@@ -1,6 +1,7 @@
use crate::options::parser::{OptionDescription, OptionForm, OptionsRepresentable};
use alloc::collections::BTreeMap;
use alloc::string::{String, ToString};
use anyhow::{Context, Result, bail};
use std::collections::BTreeMap;
/// The Sprout options parser.
pub mod parser;

View File

@@ -1,6 +1,10 @@
use alloc::collections::BTreeMap;
use alloc::string::{String, ToString};
use anyhow::{Context, Result, bail};
use core::ptr::null_mut;
use eficore::env;
use log::info;
use std::collections::BTreeMap;
use uefi_raw::Status;
/// The type of option. This disambiguates different behavior
/// of how options are handled.
@@ -14,7 +18,7 @@ pub enum OptionForm {
Help,
}
/// The description of an option, used in the options parser
/// The description of an option, used in the option parser
/// to make decisions about how to progress.
#[derive(Debug, Clone)]
pub struct OptionDescription<'a> {
@@ -31,8 +35,8 @@ pub trait OptionsRepresentable {
type Output;
/// The configured options for this type. This should describe all the options
/// that are valid to produce the type. The left hand side is the name of the option,
/// and the right hand side is the description.
/// that are valid to produce the type. The left-hand side is the name of the option,
/// and the right-hand side is the description.
fn options() -> &'static [(&'static str, OptionDescription<'static>)];
/// Produces the type by taking the `options` and processing it into the output.
@@ -40,30 +44,14 @@ pub trait OptionsRepresentable {
/// For minimalism, we don't want a full argument parser. Instead, we use
/// a simple --xyz = xyz: None and --abc 123 = abc: Some("123") format.
/// We also support --abc=123 = abc: Some("123") format.
/// We also support the format: --abc=123
fn parse_raw() -> Result<BTreeMap<String, Option<String>>> {
// Access the configured options for this type.
let configured: BTreeMap<_, _> = BTreeMap::from_iter(Self::options().to_vec());
// Collect all the arguments to Sprout.
// Skip the first argument, which is the path to our executable.
let mut args = std::env::args().skip(1).collect::<Vec<_>>();
// Correct firmware that may add invalid arguments at the start.
// Witnessed this on a Dell Precision 5690 when direct booting.
loop {
// Grab the first argument or break.
let Some(arg) = args.first() else {
break;
};
// If the argument starts with a tilde, remove it.
if arg.starts_with("`") {
args.remove(0);
continue;
}
break;
}
let args = env::args()?;
// Represent options as key-value pairs.
let mut options = BTreeMap::new();
@@ -77,7 +65,7 @@ pub trait OptionsRepresentable {
break;
};
// If the doesn't start with --, that is invalid.
// If the option doesn't start with --, that is invalid.
if !option.starts_with("--") {
bail!("invalid option: {option}");
}
@@ -144,7 +132,9 @@ pub trait OptionsRepresentable {
);
}
// Exit because the help has been displayed.
std::process::exit(0);
unsafe {
uefi::boot::exit(uefi::boot::image_handle(), Status::SUCCESS, 0, null_mut());
};
}
// Insert the option and the value into the map.

View File

@@ -1,8 +1,9 @@
use crate::actions;
use crate::context::SproutContext;
use alloc::format;
use alloc::rc::Rc;
use anyhow::{Context, Result};
use edera_sprout_config::phases::PhaseConfiguration;
use std::rc::Rc;
/// Executes the specified [phase] of the boot process.
/// The value [phase] should be a reference of a specific phase in the [PhasesConfiguration].

2
crates/boot/src/sbat.rs Normal file
View File

@@ -0,0 +1,2 @@
// Include the generated sbat section in this file.
include!(concat!(env!("OUT_DIR"), "/sbat.generated.rs"));

26
crates/boot/src/utils.rs Normal file
View File

@@ -0,0 +1,26 @@
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use sha2::{Digest, Sha256};
/// Implements a version comparison algorithm according to the BLS specification.
pub mod vercmp;
/// Combine a sequence of strings into a single string, separated by spaces, ignoring empty strings.
pub fn combine_options<T: AsRef<str>>(options: impl Iterator<Item = T>) -> String {
options
.flat_map(|item| empty_is_none(Some(item)))
.map(|item| item.as_ref().to_string())
.collect::<Vec<_>>()
.join(" ")
}
/// Produce a unique hash for the input.
/// This uses SHA-256, which is unique enough but relatively short.
pub fn unique_hash(input: &str) -> String {
hex::encode(Sha256::digest(input.as_bytes()))
}
/// Filter a string-like Option `input` such that an empty string is [None].
pub fn empty_is_none<T: AsRef<str>>(input: Option<T>) -> Option<T> {
input.filter(|input| !input.as_ref().is_empty())
}

View File

@@ -1,5 +1,5 @@
use std::cmp::Ordering;
use std::iter::Peekable;
use core::cmp::Ordering;
use core::iter::Peekable;
/// Handles single character advancement and comparison.
macro_rules! handle_single_char {
@@ -23,9 +23,9 @@ pub fn compare_versions_optional(a: Option<&str>, b: Option<&str>) -> Ordering {
match (a, b) {
// If both have values, compare them.
(Some(a), Some(b)) => compare_versions(a, b),
// If the second value is None, return that it is less than the first.
// If the second value is None, then `a` is less than `b`.
(Some(_a), None) => Ordering::Less,
// If the first value is None, return that it is greater than the second.
// If the first value is None, the `a` is greater than `b`.
(None, Some(_b)) => Ordering::Greater,
// If both values are None, return that they are equal.
(None, None) => Ordering::Equal,

12
crates/build/Cargo.toml Normal file
View File

@@ -0,0 +1,12 @@
[package]
name = "edera-sprout-build"
description = "Sprout Build Tools"
license.workspace = true
version.workspace = true
homepage.workspace = true
repository.workspace = true
edition.workspace = true
[lib]
name = "edera_sprout_build"
path = "src/lib.rs"

76
crates/build/src/lib.rs Normal file
View File

@@ -0,0 +1,76 @@
use std::path::PathBuf;
use std::{env, fs};
/// Block size of the sbat section.
const SBAT_BLOCK_SIZE: usize = 512;
/// Template contents for the sbat.generated.rs file.
const SBAT_RS_TEMPLATE: &str = include_str!("sbat.template.rs");
/// Pad with zeros the given `data` to a multiple of `block_size`.
fn block_pad(data: &mut Vec<u8>, block_size: usize) {
let needed = data.len().div_ceil(block_size).max(1) * block_size;
if needed != data.len() {
data.resize(needed, 0);
}
}
/// Generate an .sbat link section module. This should be coupled with including the sbat module in
/// the crate that intends to embed the sbat section.
/// We intake a sbat.template.csv file in the calling crate and output a sbat.dat
/// which is included by a generated sbat.generated.rs file.
pub fn generate_sbat_module() {
// Notify Cargo that if the version changes, we need to regenerate the sbat.out file.
println!("cargo:rerun-if-env-changed=CARGO_PKG_VERSION");
// The version of the package.
let 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.out file.
let out_file = output_dir.join("sbat.out");
// The output path to the sbat.generated.rs file.
let rs_file = output_dir.join("sbat.generated.rs");
// The path to the root of the crate.
let crate_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 crate.
let sbat_template_file = crate_root.join("src/sbat.csv");
// Notify Cargo that if sbat.csv changes, we need to regenerate the sbat.out file.
println!(
"cargo:rerun-if-changed={}",
sbat_template_file
.to_str()
.expect("unable to convert sbat template path file to a string")
);
// Read the sbat.csv template file.
let sbat_template =
fs::read_to_string(&sbat_template_file).expect("unable to read sbat.csv file");
// Replace the version placeholder in the template with the actual version.
let sbat = sbat_template.replace("{version}", &version);
// Encode the sbat.csv as bytes.
let mut encoded = sbat.as_bytes().to_vec();
// Pad the sbat.csv to the required block size.
block_pad(&mut encoded, SBAT_BLOCK_SIZE);
// Write the sbat.out file to the output directory.
fs::write(&out_file, &encoded).expect("unable to write sbat.out");
// Generate the contents of the sbat.generated.rs file.
// The size must tbe size of the encoded sbat.out file.
let sbat_rs = SBAT_RS_TEMPLATE.replace("{size}", &encoded.len().to_string());
// Write the sbat.generated.rs file to the output directory.
fs::write(&rs_file, sbat_rs).expect("unable to write sbat.generated.rs");
}

View File

@@ -0,0 +1,6 @@
/// 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: This data must be aligned by 512 bytes.
#[used]
#[unsafe(link_section = ".sbat")]
static SBAT: [u8; {size}] = *include_bytes!(concat!(env!("OUT_DIR"), "/sbat.out"));

View File

@@ -9,7 +9,8 @@ edition.workspace = true
[dependencies.serde]
workspace = true
features = ["derive"]
default-features = false
[lib]
name = "edera_sprout_config"
path = "src/lib.rs"

View File

@@ -1,3 +1,5 @@
use alloc::string::String;
use alloc::vec::Vec;
use serde::{Deserialize, Serialize};
/// The configuration of the chainload action.

View File

@@ -1,3 +1,5 @@
use alloc::string::String;
use alloc::vec::Vec;
use serde::{Deserialize, Serialize};
/// The configuration of the edera action which boots the Edera hypervisor.

View File

@@ -1,3 +1,4 @@
use alloc::string::String;
use serde::{Deserialize, Serialize};
/// The configuration of the print action.

View File

@@ -1,3 +1,4 @@
use alloc::string::String;
use serde::{Deserialize, Serialize};
/// Declares a driver configuration.

View File

@@ -1,5 +1,7 @@
use alloc::collections::BTreeMap;
use alloc::string::String;
use alloc::vec::Vec;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
/// Declares a boot entry to display in the boot menu.
///

View File

@@ -1,3 +1,4 @@
use alloc::string::String;
use serde::{Deserialize, Serialize};
/// The filesystem device match extractor.

View File

@@ -1,4 +1,5 @@
use crate::entries::EntryDeclaration;
use alloc::string::{String, ToString};
use serde::{Deserialize, Serialize};
/// The default path to the BLS directory.

View File

@@ -1,6 +1,8 @@
use crate::entries::EntryDeclaration;
use alloc::collections::BTreeMap;
use alloc::string::String;
use alloc::vec::Vec;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
/// List generator configuration.
/// The list generator produces multiple entries based

View File

@@ -1,6 +1,8 @@
use crate::entries::EntryDeclaration;
use alloc::collections::BTreeMap;
use alloc::string::String;
use alloc::vec::Vec;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
/// Matrix generator configuration.
/// The matrix generator produces multiple entries based

View File

@@ -1,5 +1,7 @@
//! Sprout configuration descriptions.
//! This crate provides all the configuration structures for Sprout.
#![no_std]
extern crate alloc;
use crate::actions::ActionDeclaration;
use crate::drivers::DriverDeclaration;
@@ -7,8 +9,9 @@ use crate::entries::EntryDeclaration;
use crate::extractors::ExtractorDeclaration;
use crate::generators::GeneratorDeclaration;
use crate::phases::PhasesConfiguration;
use alloc::collections::BTreeMap;
use alloc::string::String;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
pub mod actions;
pub mod drivers;

View File

@@ -1,5 +1,7 @@
use alloc::collections::BTreeMap;
use alloc::string::String;
use alloc::vec::Vec;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
/// Configures the various phases of the boot process.
/// This allows hooking various phases to run actions.

21
crates/eficore/Cargo.toml Normal file
View File

@@ -0,0 +1,21 @@
[package]
name = "edera-sprout-eficore"
description = "Sprout EFI Core"
license.workspace = true
version.workspace = true
homepage.workspace = true
repository.workspace = true
edition.workspace = true
[dependencies]
anyhow.workspace = true
bitflags.workspace = true
log.workspace = true
shlex.workspace = true
spin.workspace = true
uefi.workspace = true
uefi-raw.workspace = true
[lib]
name = "eficore"
path = "src/lib.rs"

View File

@@ -1,7 +1,9 @@
use crate::integrations::bootloader_interface::bitflags::LoaderFeatures;
use crate::bootloader_interface::bitflags::LoaderFeatures;
use crate::platform::timer::PlatformTimer;
use crate::utils::device_path_subpath;
use crate::utils::variables::{VariableClass, VariableController};
use crate::variables::{VariableClass, VariableController};
use alloc::format;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use anyhow::{Context, Result};
use uefi::proto::device_path::DevicePath;
use uefi::{Guid, guid};
@@ -100,7 +102,8 @@ impl BootloaderInterface {
/// 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")?;
let subpath =
crate::path::device_path_subpath(path).context("unable to get loader path subpath")?;
Self::VENDOR.set_cstr16(
"LoaderImageIdentifier",
&subpath,

69
crates/eficore/src/env.rs Normal file
View File

@@ -0,0 +1,69 @@
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use anyhow::{Context, Result, bail};
use uefi::proto::loaded_image::{LoadOptionsError, LoadedImage};
/// Loads the command-line arguments passed to the current image.
pub fn args() -> Result<Vec<String>> {
// Acquire the current image handle.
let handle = uefi::boot::image_handle();
// Open the LoadedImage protocol for the current image.
let loaded_image = uefi::boot::open_protocol_exclusive::<LoadedImage>(handle)
.context("unable to open loaded image protocol for current image")?;
// Load the command-line argument string.
let options = match loaded_image.load_options_as_cstr16() {
// Load options were passed. We will return them for processing.
Ok(options) => options,
// No load options were passed. We will return an empty vector.
Err(LoadOptionsError::NotSet) => {
return Ok(Vec::new());
}
Err(LoadOptionsError::NotAligned) => {
bail!("load options are not properly aligned");
}
Err(LoadOptionsError::InvalidString(error)) => {
bail!("load options are not a valid string: {}", error);
}
};
// Convert the options to a string.
let options = options.to_string();
// Use shlex to parse the options.
// If shlex fails, we will perform a simple whitespace split.
let mut args = shlex::split(&options).unwrap_or_else(|| {
options
.split_ascii_whitespace()
.map(|string| string.to_string())
.collect::<Vec<_>>()
});
// Correct firmware that may add invalid arguments at the start.
// Witnessed this on a Dell Precision 5690 when direct booting.
args = args
.into_iter()
.skip_while(|arg| {
arg.chars()
.next()
// Filter out unprintable characters and backticks.
// Both of which have been observed in the wild.
.map(|c| c < 0x1f as char || c == '`')
.unwrap_or(false)
})
.collect();
// If there is a first argument, check if it is not an option.
// If it is not, we will assume it is the path to the executable and remove it.
if let Some(arg) = args.first()
&& !arg.starts_with('-')
{
args.remove(0);
}
Ok(args)
}

View File

@@ -1,3 +1,5 @@
use alloc::vec;
use alloc::vec::Vec;
use anyhow::{Context, Result};
use uefi::proto::console::gop::{BltOp, BltPixel, BltRegion, GraphicsOutput};

View File

@@ -0,0 +1,26 @@
use anyhow::{Context, Result};
use uefi::boot::SearchType;
use uefi::{Guid, Handle};
use uefi_raw::Status;
/// 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")
}
}
}
}

44
crates/eficore/src/lib.rs Normal file
View File

@@ -0,0 +1,44 @@
//! Sprout EFI Core.
//! This crate provides tools for working with the EFI environment.
#![no_std]
extern crate alloc;
/// EFI handle helpers.
pub mod handle;
/// Load and start EFI images.
pub mod loader;
/// Logging support for EFI applications.
pub mod logger;
/// Disk partitioning support infrastructure.
pub mod partition;
/// Path handling for UEFI.
pub mod path;
/// platform: Integration or support code for specific hardware platforms.
pub mod platform;
/// Secure Boot support.
pub mod secure;
/// Support for the shim loader application that enables Secure Boot.
pub mod shim;
/// String utilities.
pub mod strings;
/// Implements support for the bootloader interface specification.
pub mod bootloader_interface;
/// Acquire arguments from UEFI environment.
pub mod env;
/// Support code for the EFI framebuffer.
pub mod framebuffer;
/// Support code for the media loader protocol.
pub mod media_loader;
/// setup: Code that initializes the UEFI environment for Sprout.
pub mod setup;
/// Support code for EFI variables.
pub mod variables;

View File

@@ -0,0 +1,141 @@
use crate::loader::source::ImageSource;
use crate::secure::SecureBoot;
use crate::shim::hook::SecurityHook;
use crate::shim::{ShimInput, ShimSupport};
use anyhow::{Context, Result, bail};
use log::warn;
use uefi::Handle;
use uefi::boot::LoadImageSource;
/// Represents EFI image sources generically.
pub mod source;
/// Handle to a loaded EFI image.
pub struct ImageHandle {
/// Handle to the loaded image.
handle: Handle,
}
impl ImageHandle {
/// Create a new image handle based on a handle from the UEFI stack.
pub fn new(handle: Handle) -> Self {
Self { handle }
}
/// Retrieve the underlying handle.
pub fn handle(&self) -> &Handle {
&self.handle
}
}
/// Request to load an image from a source, with support for additional validation features.
pub struct ImageLoadRequest<'source> {
/// Handle to the current image.
current_image: Handle,
/// Source of the image to load.
source: ImageSource<'source>,
}
impl<'source> ImageLoadRequest<'source> {
/// Create a new image load request with a current image and a source.
pub fn new(current_image: Handle, source: ImageSource<'source>) -> Self {
Self {
current_image,
source,
}
}
/// Retrieve the current image.
pub fn current_image(&self) -> &Handle {
&self.current_image
}
/// Retrieve the source of the image to load.
pub fn source(&'source self) -> &'source ImageSource<'source> {
&self.source
}
/// Convert the request into a source.
pub fn into_source(self) -> ImageSource<'source> {
self.source
}
}
/// EFI image loader.
pub struct ImageLoader;
impl ImageLoader {
/// Load an image using the image `request` which allows
pub fn load(request: ImageLoadRequest) -> Result<ImageHandle> {
// Determine whether Secure Boot is enabled.
let secure_boot =
SecureBoot::enabled().context("unable to determine if secure boot is enabled")?;
// Determine whether the shim is loaded.
let shim_loaded = ShimSupport::loaded().context("unable to determine if shim is loaded")?;
// Determine whether the shim loader is available.
let shim_loader_available = ShimSupport::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 = secure_boot && 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.
ShimSupport::retain()?;
}
// Clone the current image handle to use for loading the image.
let current_image = *request.current_image();
// Converts the source to a shim input with an owned data buffer.
let input = ShimInput::from(request.into_source())
.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 = crate::shim::hook::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?;
}
}
// Assert the result and grab the handle.
let handle = result?;
// Retrieve the handle from the result and make a new image handle.
Ok(ImageHandle::new(handle))
}
}

View File

@@ -0,0 +1,25 @@
use crate::path::ResolvedPath;
use crate::shim::ShimInput;
/// Represents a source of an EFI image.
pub enum ImageSource<'source> {
/// The image is located at the specified path that has been resolved.
ResolvedPath(&'source ResolvedPath),
/// The image is located in a buffer.
DataBuffer {
/// Optional path to the image.
path: Option<&'source ResolvedPath>,
/// Buffer containing the image.
buffer: &'source [u8],
},
}
/// Implement conversion from `ImageSource` to `ShimInput`, which is used by the shim support code.
impl<'source> From<ImageSource<'source>> for ShimInput<'source> {
fn from(value: ImageSource<'source>) -> Self {
match value {
ImageSource::ResolvedPath(path) => ShimInput::ResolvedPath(path),
ImageSource::DataBuffer { path, buffer } => ShimInput::DataBuffer(path, buffer),
}
}
}

View File

@@ -0,0 +1,94 @@
//! Based on: https://github.com/rust-osdev/uefi-rs/blob/main/uefi/src/helpers/logger.rs
use alloc::format;
use core::fmt::Write;
use core::ptr;
use core::sync::atomic::{AtomicPtr, Ordering};
use log::{Log, Record};
use uefi::proto::console::text::Output;
/// The global logger object.
static LOGGER: Logger = Logger::new();
/// Logging mechanism for Sprout.
/// Must be initialized to be used, as we use atomic pointers to store the output to write to.
pub struct Logger {
writer: AtomicPtr<Output>,
}
impl Default for Logger {
/// Creates a default logger, which is uninitialized with an output.
fn default() -> Self {
Self::new()
}
}
impl Logger {
/// Create a new logger with an output not specified.
/// This will cause the logger to not print anything until it is configured.
pub const fn new() -> Self {
Self {
writer: AtomicPtr::new(ptr::null_mut()),
}
}
/// Retrieves the pointer to the output.
/// SAFETY: This pointer might be null, it should be checked before use.
#[must_use]
fn output(&self) -> *mut Output {
self.writer.load(Ordering::Acquire)
}
/// Sets the output to write to.
///
/// # Safety
/// This function is unsafe because the output is technically leaked and unmanaged.
pub unsafe fn set_output(&self, output: *mut Output) {
self.writer.store(output, Ordering::Release);
}
}
impl Log for Logger {
/// Enable the logger always.
fn enabled(&self, _metadata: &log::Metadata<'_>) -> bool {
true
}
/// Log the specified `record` to the output if one is set.
fn log(&self, record: &Record) {
// Acquire the output. If one is not set, we do nothing.
let Some(output) = (unsafe { self.output().as_mut() }) else {
return;
};
// Format the log message.
let message = format!("{}", record.args());
// Iterate over every line, formatting the message and writing it to the output.
for line in message.lines() {
// The format writes the log level in front of every line of text.
let _ = writeln!(output, "[{:>5}] {}", record.level(), line);
}
}
/// This log is not buffered, so flushing isn't required.
fn flush(&self) {}
}
/// Initialize the logging environment, calling panic if something goes wrong.
pub fn init() {
// Retrieve the stdout handle and set it as the output for the global logger.
uefi::system::with_stdout(|stdout| unsafe {
// SAFETY: We are using the stdout handle to create a pointer to the output.
// The handle is global and is guaranteed to be valid for the lifetime of the program.
LOGGER.set_output(stdout);
});
// Set the logger to the global logger.
if let Err(error) = log::set_logger(&LOGGER) {
panic!("unable to set logger: {}", error);
}
// Set the max level to the level specified by the log features.
log::set_max_level(log::STATIC_MAX_LEVEL);
}

View File

@@ -1,5 +1,8 @@
use alloc::boxed::Box;
use alloc::vec::Vec;
use anyhow::{Context, Result, bail};
use std::ffi::c_void;
use core::ffi::c_void;
use core::ptr;
use uefi::proto::device_path::DevicePath;
use uefi::proto::device_path::build::DevicePathBuilder;
use uefi::proto::device_path::build::media::Vendor;
@@ -158,14 +161,31 @@ impl MediaLoaderHandle {
// Install a protocol interface for the device path.
// This ensures it can be located by other EFI programs.
let primary_handle = unsafe {
let primary_handle = match unsafe {
uefi::boot::install_protocol_interface(
None,
&DevicePathProtocol::GUID,
path.as_ffi_ptr() as *mut c_void,
)
}
.context("unable to install media loader device path handle")?;
.context("unable to install media loader device path handle")
{
// Acquiring the primary handle succeeded, so we can return the handle.
Ok(handle) => handle,
// If acquiring the primary handle failed, we free the device path and return the error.
Err(error) => {
// SAFETY: We know that the device path is leaked,
// so we can safely take a reference to it again.
// The UEFI stack failed to install the protocol interface
// if we reach here, so the path is no longer in use.
let path = unsafe { Box::from_raw(path) };
// Explicitly drop the path to clarify the lifetime.
drop(path);
// Return the original error.
return Err(error);
}
};
// Leak the data we need to pass to the UEFI stack.
let data = Box::leak(data);
@@ -261,8 +281,7 @@ impl MediaLoaderHandle {
let protocol = Box::from_raw(self.protocol);
// Retrieve a box for the data we passed in.
let slice =
std::ptr::slice_from_raw_parts_mut(protocol.address as *mut u8, protocol.length);
let slice = ptr::slice_from_raw_parts_mut(protocol.address as *mut u8, protocol.length);
let data = Box::from_raw(slice);
// Drop all the allocations explicitly, as we don't want to leak them.

View File

@@ -0,0 +1,55 @@
use anyhow::{Context, Result};
use uefi::Guid;
use uefi::proto::device_path::DevicePath;
use uefi::proto::media::partition::PartitionInfo;
use uefi_raw::Status;
/// 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)
}
}

View File

@@ -1,26 +1,50 @@
use anyhow::{Context, Result, bail};
use sha2::{Digest, Sha256};
use std::ops::Deref;
use uefi::boot::SearchType;
use alloc::borrow::ToOwned;
use alloc::boxed::Box;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use anyhow::{Context, Result};
use core::ops::Deref;
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::proto::media::partition::PartitionInfo;
use uefi::{CString16, Guid, Handle};
use uefi_raw::Status;
use uefi::{CString16, Handle};
/// Support code for the EFI framebuffer.
pub mod framebuffer;
/// Represents the components of a resolved path.
pub struct ResolvedPath {
/// The root path of the resolved path. This is the device itself.
/// For example, "PciRoot(0x0)/Pci(0x4,0x0)/NVMe(0x1,00-00-00-00-00-00-00-00)/HD(1,MBR,0xBE1AFDFA,0x3F,0xFBFC1)/"
pub root_path: Box<DevicePath>,
/// The subpath of the resolved path. This is the path to the file.
/// For example, "\EFI\BOOT\BOOTX64.efi"
pub sub_path: Box<DevicePath>,
/// The full path of the resolved path. This is the safest path to use.
/// For example, "PciRoot(0x0)/Pci(0x4,0x0)/NVMe(0x1,00-00-00-00-00-00-00-00)/HD(1,MBR,0xBE1AFDFA,0x3F,0xFBFC1)/\EFI\BOOT\BOOTX64.efi"
pub full_path: Box<DevicePath>,
/// The handle of the filesystem containing the path.
/// This can be used to acquire a [SimpleFileSystem] protocol to read the file.
pub filesystem_handle: Handle,
}
/// Support code for the media loader protocol.
pub mod media_loader;
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")
}
}
/// Support code for EFI variables.
pub mod variables;
/// Implements a version comparison algorithm according to the BLS specification.
pub mod vercmp;
/// Checks if a [CString16] contains a char `c`.
/// We need to call to_string() because CString16 doesn't support `contains` with a char.
fn cstring16_contains_char(string: &CString16, c: char) -> bool {
string.to_string().contains(c)
}
/// Parses the input `path` as a [DevicePath].
/// Uses the [DevicePathFromText] protocol exclusively, and will fail if it cannot acquire the protocol.
@@ -37,12 +61,6 @@ pub fn text_to_device_path(path: &str) -> Result<PoolDevicePath> {
.context("unable to convert text to device path")
}
/// Checks if a [CString16] contains a char `c`.
/// We need to call to_string() because CString16 doesn't support `contains` with a char.
fn cstring16_contains_char(string: &CString16, c: char) -> bool {
string.to_string().contains(c)
}
/// Grabs the root part of the `path`.
/// For example, given "PciRoot(0x0)/Pci(0x4,0x0)/NVMe(0x1,00-00-00-00-00-00-00-00)/HD(1,MBR,0xBE1AFDFA,0x3F,0xFBFC1)/\EFI\BOOT\BOOTX64.efi"
/// it will give "PciRoot(0x0)/Pci(0x4,0x0)/NVMe(0x1,00-00-00-00-00-00-00-00)/HD(1,MBR,0xBE1AFDFA,0x3F,0xFBFC1)"
@@ -92,36 +110,6 @@ pub fn device_path_subpath(path: &DevicePath) -> Result<String> {
Ok(path)
}
/// Represents the components of a resolved path.
pub struct ResolvedPath {
/// The root path of the resolved path. This is the device itself.
/// For example, "PciRoot(0x0)/Pci(0x4,0x0)/NVMe(0x1,00-00-00-00-00-00-00-00)/HD(1,MBR,0xBE1AFDFA,0x3F,0xFBFC1)/"
pub root_path: Box<DevicePath>,
/// The subpath of the resolved path. This is the path to the file.
/// For example, "\EFI\BOOT\BOOTX64.efi"
pub sub_path: Box<DevicePath>,
/// The full path of the resolved path. This is the safest path to use.
/// For example, "PciRoot(0x0)/Pci(0x4,0x0)/NVMe(0x1,00-00-00-00-00-00-00-00)/HD(1,MBR,0xBE1AFDFA,0x3F,0xFBFC1)/\EFI\BOOT\BOOTX64.efi"
pub full_path: Box<DevicePath>,
/// The handle of the filesystem containing the path.
/// This can be used to acquire a [SimpleFileSystem] protocol to read the file.
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.
@@ -184,114 +172,3 @@ pub fn read_file_contents(default_root_path: Option<&DevicePath>, input: &str) -
let resolved = resolve_path(default_root_path, input)?;
resolved.read_file()
}
/// Filter a string-like Option `input` such that an empty string is [None].
pub fn empty_is_none<T: AsRef<str>>(input: Option<T>) -> Option<T> {
input.filter(|input| !input.as_ref().is_empty())
}
/// Combine a sequence of strings into a single string, separated by spaces, ignoring empty strings.
pub fn combine_options<T: AsRef<str>>(options: impl Iterator<Item = T>) -> String {
options
.flat_map(|item| empty_is_none(Some(item)))
.map(|item| item.as_ref().to_string())
.collect::<Vec<_>>()
.join(" ")
}
/// Produce a unique hash for the input.
/// This uses SHA-256, which is unique enough but relatively short.
pub fn unique_hash(input: &str) -> String {
hex::encode(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")
}
}
}
}
/// Convert a byte slice into a CString16.
pub fn utf16_bytes_to_cstring16(bytes: &[u8]) -> Result<CString16> {
// Validate the input bytes are the right length.
if !bytes.len().is_multiple_of(2) {
bail!("utf16 bytes must be a multiple of 2");
}
// Convert the bytes to UTF-16 data.
let data = bytes
// Chunk everything into two bytes.
.chunks_exact(2)
// Reinterpret the bytes as u16 little-endian.
.map(|chunk| u16::from_le_bytes([chunk[0], chunk[1]]))
// Collect the result into a vector.
.collect::<Vec<_>>();
CString16::try_from(data).context("unable to convert utf16 bytes to CString16")
}

View File

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

View File

@@ -1,7 +1,7 @@
// 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;
use core::time::Duration;
/// Support for aarch64 timers.
#[cfg(target_arch = "aarch64")]
@@ -17,7 +17,7 @@ pub enum TickFrequency {
/// The platform provides the tick frequency.
Hardware(u64),
/// The tick frequency is measured internally.
Measured(u64, Duration),
Measured(u64),
}
impl TickFrequency {
@@ -25,7 +25,7 @@ impl TickFrequency {
fn ticks(&self) -> u64 {
match self {
TickFrequency::Hardware(frequency) => *frequency,
TickFrequency::Measured(frequency, _) => *frequency,
TickFrequency::Measured(frequency) => *frequency,
}
}

View File

@@ -1,5 +1,5 @@
use crate::platform::timer::TickFrequency;
use std::arch::asm;
use core::arch::asm;
/// Reads the cntvct_el0 counter and returns the value.
pub fn ticks() -> u64 {
@@ -10,16 +10,6 @@ pub fn ticks() -> u64 {
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;

View File

@@ -0,0 +1,29 @@
use crate::platform::timer::TickFrequency;
use core::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 {
// SAFETY: Reads the platform timer, which is safe in any context.
unsafe { core::arch::x86_64::_rdtsc() }
}
/// Measure the frequency of the platform timer.
/// NOTE: Intentionally, we do not synchronize rdtsc during measurement to match systemd behavior.
fn measure_frequency() -> u64 {
let start = ticks();
uefi::boot::stall(MEASURE_FREQUENCY_DURATION);
let stop = ticks();
let elapsed = stop.wrapping_sub(start) as f64;
(elapsed / MEASURE_FREQUENCY_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();
TickFrequency::Measured(frequency)
}

View File

@@ -1,4 +1,3 @@
use crate::utils;
use anyhow::{Context, Result};
use uefi::ResultExt;
use uefi::boot::ScopedProtocol;
@@ -43,8 +42,8 @@ impl PlatformTpm {
/// 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")?
let Some(handle) = crate::handle::find_handle(&Tcg2Protocol::GUID)
.context("unable to determine tpm presence")?
else {
return Ok(None);
};
@@ -85,7 +84,9 @@ impl PlatformTpm {
};
// 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 {
if (handle.version().major < 1)
|| (handle.version().major == 1 && (handle.version().minor < 1))
{
return Ok(0);
}

View File

@@ -1,4 +1,4 @@
use crate::utils::variables::VariableController;
use crate::variables::VariableController;
use anyhow::Result;
/// Secure boot services.

View File

@@ -0,0 +1,14 @@
use crate::logger;
use anyhow::{Context, Result};
/// Initializes the UEFI environment.
pub fn init() -> Result<()> {
// Initialize the logger for Sprout.
// NOTE: This cannot use a result type as errors need to be printed
// using the logger, which is not initialized until this returns.
logger::init();
// Initialize further UEFI internals.
uefi::helpers::init().context("unable to initialize uefi environment")?;
Ok(())
}

View File

@@ -1,14 +1,11 @@
use crate::integrations::shim::hook::SecurityHook;
use crate::secure::SecureBoot;
use crate::utils;
use crate::utils::ResolvedPath;
use crate::utils::variables::{VariableClass, VariableController};
use crate::path::ResolvedPath;
use crate::variables::{VariableClass, VariableController};
use alloc::boxed::Box;
use alloc::string::ToString;
use alloc::vec::Vec;
use anyhow::{Context, Result, anyhow, bail};
use log::warn;
use std::ffi::c_void;
use std::pin::Pin;
use uefi::Handle;
use uefi::boot::LoadImageSource;
use core::ffi::c_void;
use core::pin::Pin;
use uefi::proto::device_path::text::{AllowShortcuts, DisplayOnly};
use uefi::proto::device_path::{DevicePath, FfiDevicePath};
use uefi::proto::unsafe_protocol;
@@ -16,7 +13,7 @@ use uefi_raw::table::runtime::VariableVendor;
use uefi_raw::{Guid, Status, guid};
/// Security hook support.
mod hook;
pub mod hook;
/// Support for the shim loader application for Secure Boot.
pub struct ShimSupport;
@@ -87,7 +84,7 @@ impl<'a> ShimInput<'a> {
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())
let path = crate::path::resolve_path(None, &path.to_string())
.context("unable to resolve path")?;
// Read the file path.
let data = path.read_file()?;
@@ -160,14 +157,14 @@ impl ShimSupport {
/// Determines whether the shim is loaded.
pub fn loaded() -> Result<bool> {
Ok(utils::find_handle(&Self::SHIM_LOCK_GUID)
Ok(crate::handle::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)
Ok(crate::handle::find_handle(&Self::SHIM_IMAGE_LOADER_GUID)
.context("unable to find shim image loader protocol")?
.is_some())
}
@@ -175,7 +172,7 @@ impl ShimSupport {
/// 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)
let handle = crate::handle::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.
@@ -235,72 +232,6 @@ impl ShimSupport {
.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 Secure Boot is enabled.
let secure_boot =
SecureBoot::enabled().context("unable to determine if secure boot is enabled")?;
// 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 = secure_boot && 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<()> {

View File

@@ -1,8 +1,8 @@
use crate::integrations::shim::{ShimInput, ShimSupport, ShimVerificationOutput};
use crate::utils;
use anyhow::{Context, Result, bail};
use crate::shim::{ShimInput, ShimSupport, ShimVerificationOutput};
use anyhow::{Context, Result};
use core::slice;
use log::warn;
use std::sync::{LazyLock, Mutex};
use spin::{Lazy, Mutex};
use uefi::proto::device_path::FfiDevicePath;
use uefi::proto::unsafe_protocol;
use uefi::{Guid, guid};
@@ -45,8 +45,7 @@ struct SecurityHookState {
/// 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));
static GLOBAL_HOOK_STATE: Lazy<Mutex<Option<SecurityHookState>>> = Lazy::new(|| Mutex::new(None));
/// Security hook helper.
pub struct SecurityHook;
@@ -110,9 +109,7 @@ impl SecurityHook {
// Verify the input, if it fails, call the original hook.
if !Self::verify(input) {
// Acquire the global hook state to grab the original hook.
let function = match GLOBAL_HOOK_STATE.lock() {
// We have acquired the lock, so we can find the original hook.
Ok(state) => match state.as_ref() {
let function = match GLOBAL_HOOK_STATE.lock().as_ref() {
// The hook state is available, so we can acquire the original hook.
Some(state) => state.original_hook.file_authentication_state,
@@ -121,15 +118,6 @@ impl SecurityHook {
warn!("global hook state is not available, unable to call original hook");
return Status::LOAD_ERROR;
}
},
Err(error) => {
warn!(
"unable to acquire global hook state lock to call original hook: {}",
error,
);
return Status::LOAD_ERROR;
}
};
// Call the original hook function to see what it reports.
@@ -161,7 +149,7 @@ impl SecurityHook {
}
// Construct a slice out of the file buffer and size.
let buffer = unsafe { std::slice::from_raw_parts(file_buffer, file_size) };
let buffer = unsafe { slice::from_raw_parts(file_buffer, file_size) };
// Construct a shim input from the path.
let input = ShimInput::SecurityHookBuffer(Some(path), buffer);
@@ -169,9 +157,7 @@ impl SecurityHook {
// Verify the input, if it fails, call the original hook.
if !Self::verify(input) {
// Acquire the global hook state to grab the original hook.
let function = match GLOBAL_HOOK_STATE.lock() {
// We have acquired the lock, so we can find the original hook.
Ok(state) => match state.as_ref() {
let function = match GLOBAL_HOOK_STATE.lock().as_ref() {
// The hook state is available, so we can acquire the original hook.
Some(state) => state.original_hook2.file_authentication,
@@ -180,15 +166,6 @@ impl SecurityHook {
warn!("global hook state is not available, unable to call original hook");
return Status::LOAD_ERROR;
}
},
Err(error) => {
warn!(
"unable to acquire global hook state lock to call original hook: {}",
error
);
return Status::LOAD_ERROR;
}
};
// Call the original hook function to see what it reports.
@@ -203,14 +180,14 @@ impl SecurityHook {
/// 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)
let Some(hook_arch) = crate::handle::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)
let Some(hook_arch2) = crate::handle::find_handle(&SECURITY_ARCH2_GUID)
.context("unable to check security arch2 existence")?
else {
return Ok(false);
@@ -237,9 +214,7 @@ impl SecurityHook {
};
// 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");
};
let mut global_state = GLOBAL_HOOK_STATE.lock();
global_state.replace(state);
// Install the hooks into the UEFI stack.
@@ -252,14 +227,14 @@ impl SecurityHook {
/// 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)
let Some(hook_arch) = crate::handle::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)
let Some(hook_arch2) = crate::handle::find_handle(&SECURITY_ARCH2_GUID)
.context("unable to check security arch2 existence")?
else {
return Ok(());
@@ -276,9 +251,7 @@ impl SecurityHook {
.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");
};
let mut global_state = GLOBAL_HOOK_STATE.lock();
// Take the state and replace the original functions.
let Some(state) = global_state.take() else {

View File

@@ -0,0 +1,22 @@
use alloc::vec::Vec;
use anyhow::{Context, Result, bail};
use uefi::CString16;
/// Convert a byte slice into a CString16.
pub fn utf16_bytes_to_cstring16(bytes: &[u8]) -> Result<CString16> {
// Validate the input bytes are the right length.
if !bytes.len().is_multiple_of(2) {
bail!("utf16 bytes must be a multiple of 2");
}
// Convert the bytes to UTF-16 data.
let data = bytes
// Chunk everything into two bytes.
.chunks_exact(2)
// Reinterpret the bytes as u16 little-endian.
.map(|chunk| u16::from_le_bytes([chunk[0], chunk[1]]))
// Collect the result into a vector.
.collect::<Vec<_>>();
CString16::try_from(data).context("unable to convert utf16 bytes to CString16")
}

View File

@@ -1,4 +1,7 @@
use crate::utils;
use crate::strings;
use alloc::format;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use anyhow::{Context, Result};
use log::warn;
use uefi::{CString16, guid};
@@ -56,7 +59,7 @@ impl VariableController {
match uefi::runtime::get_variable_boxed(&name, &self.vendor) {
Ok((data, _)) => {
// Try to decode UTF-16 bytes to a CString16.
match utils::utf16_bytes_to_cstring16(&data) {
match strings::utf16_bytes_to_cstring16(&data) {
Ok(value) => {
// We have a value, so return the UTF-8 value.
Ok(Some(value.to_string()))
@@ -142,6 +145,8 @@ impl VariableController {
self.set(key, &value.to_le_bytes(), class)
}
/// Remove the variable specified by `key`.
/// This can fail if the variable is not set.
pub fn remove(&self, key: &str) -> Result<()> {
let name = Self::name(key)?;

View File

@@ -1,57 +0,0 @@
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

@@ -1,4 +0,0 @@
/// 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

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

View File

@@ -1,66 +0,0 @@
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() -> u64 {
let start = start();
uefi::boot::stall(MEASURE_FREQUENCY_DURATION);
let stop = stop();
let elapsed = stop.wrapping_sub(start) as f64;
(elapsed / MEASURE_FREQUENCY_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();
TickFrequency::Measured(frequency, MEASURE_FREQUENCY_DURATION)
}

View File

@@ -1,11 +0,0 @@
/// 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"));

View File

@@ -1,27 +0,0 @@
use anyhow::{Context, Result};
use std::os::uefi as uefi_std;
/// Initializes the UEFI environment.
///
/// This fetches the system table and current image handle from uefi_std and injects
/// them into the uefi crate.
pub fn init() -> Result<()> {
// Acquire the system table and image handle from the uefi_std environment.
let system_table = uefi_std::env::system_table();
let image_handle = uefi_std::env::image_handle();
// SAFETY: The UEFI variables above come from the Rust std.
// These variables are not-null and calling the uefi crates with these values is validated
// to be corrected by hand.
unsafe {
// Set the system table and image handle.
uefi::table::set_system_table(system_table.as_ptr().cast());
let handle = uefi::Handle::from_ptr(image_handle.as_ptr().cast())
.context("unable to resolve image handle")?;
uefi::boot::set_image_handle(handle);
}
// Initialize the uefi logger mechanism and other helpers.
uefi::helpers::init().context("unable to initialize uefi")?;
Ok(())
}

View File

@@ -4,7 +4,6 @@
- Modern Debian release: tested on Debian 13 ARM64
- EFI System Partition mounted on `/boot/efi` (the default)
- ext4 or FAT32/exFAT formatted `/boot` partition
- You will need the following packages installed: `openssl`, `shim-signed`, `mokutil`, `sbsigntool`
## Step 1: Generate and Install Secure Boot Key
@@ -61,7 +60,7 @@ Copy the downloaded `sprout.efi` file to `/boot/efi/EFI/sprout/sprout.unsigned.e
## Step 4: Sign Sprout for Secure Boot
```bash
# For x86_64, sign the unsigned Sprout artifact and name it grubaa64.efi which is what the shim will call.
# For x86_64, sign the unsigned Sprout artifact and name it grubx64.efi which is what the shim will call.
$ sbsign \
--key /etc/sprout/secure-boot/mok.key \
--cert /etc/sprout/secure-boot/mok.crt \
@@ -121,8 +120,8 @@ path = "\\EFI\\sprout\\ext4.efi"
# global options.
[options]
# enable autoconfiguration by detecting bls enabled
# filesystems and generating boot entries for them.
# enable autoconfiguration by detecting installed kernels
# generating boot entries for them.
autoconfigure = true
```
@@ -131,14 +130,16 @@ If you do not have any drivers, exclude the drivers section entirely.
## Step 7: Configure Sprout Boot Entry
In the following commands, replace /dev/ESP_PARTITION with the actual path to the ESP partition block device.
In the following commands, replace /dev/BLOCK_DEVICE with the device that houses your GPT partition table,
and PARTITION_NUMBER with the partition number of the EFI System Partition. For example, if your EFI System Partition is
`/dev/sda1`, the BLOCK_DEVICE would be `/dev/sda` and the PARTITION_NUMBER would be `1`
```bash
# For x86_64, run this command to add Sprout as the default boot entry.
$ efibootmgr -d /dev/ESP_PARTITION -c -L 'Sprout' -l '\EFI\sprout\shimx64.efi'
$ efibootmgr -d /dev/BLOCK_DEVICE -p PARTITION_NUMBER -c -L 'Sprout' -l '\EFI\sprout\shimx64.efi'
# For aarch64, run this command to add Sprout as the default boot entry.
$ efibootmgr -d /dev/ESP_PARTITION -c -L 'Sprout' -l '\EFI\sprout\shimaa64.efi'
$ efibootmgr -d /dev/BLOCK_DEVICE -p PARTITION_NUMBER -c -L 'Sprout' -l '\EFI\sprout\shimaa64.efi'
```
Reboot your machine and it should boot into Sprout.

172
docs/setup/signed/fedora.md Normal file
View File

@@ -0,0 +1,172 @@
# Setup Sprout for Fedora with Secure Boot
## Prerequisites
- Modern Fedora release: tested on Fedora 43 x86_64.
- EFI System Partition mounted on `/boot/efi` (the default)
- You will need the following packages installed: `openssl`, `mokutil`, `sbsigntools`, `efibootmgr`
**NOTE**: Fedora on ARM64 itself does not support Secure Boot consistently.
## Step 1: Generate and Install Secure Boot Key
```bash
# Create a directory to store the Secure Boot MOK key and certificates.
$ mkdir -p /etc/sprout/secure-boot
# Change to the created directory.
$ cd /etc/sprout/secure-boot
# Generate a MOK key and certificate.
$ openssl req \
-newkey rsa:4096 -nodes -keyout mok.key \
-new -x509 -sha256 -days 3650 -subj '/CN=Sprout Secure Boot/' \
-out mok.crt
# Generate a DER encoded certificate for enrollment.
$ openssl x509 -outform DER -in mok.crt -out mok.cer
# Import the certificate into the Secure Boot environment.
# This will ask you to make a password that will be used during enrollment.
$ mokutil --import mok.cer
# Reboot your machine.
# During boot, MOK enrollment should appear. If it doesn't, ensure you are booting into the shim.
# Press any key to begin MOK management. Select "Enroll MOK".
# Select "View key 0", and ensure the subject says "CN=Sprout Secure Boot".
# If the subject does not match, something has gone wrong with MOK enrollment.
# Press Enter to continue, then select the "Continue" option.
# When it asks to enroll the key, select the "Yes" option.
# Enter the password that you created during the mokutil --import step.
# Select "Reboot" to boot back into your Operating System.
```
## Step 2: Prepare the Secure Boot Environment
```bash
# Create a directory for Sprout EFI artifacts.
$ mkdir -p /boot/efi/EFI/sprout
# For x86_64, copy the following artifacts to the Sprout EFI directory.
$ cp /boot/efi/EFI/fedora/shimx64.efi /boot/efi/EFI/sprout/shimx64.efi
$ cp /boot/efi/EFI/fedora/mmx64.efi /boot/efi/EFI/sprout/mmx64.efi
# For aarch64, copy the following artifacts to the Sprout EFI directory.
$ cp /boot/efi/EFI/fedora/shimaa64.efi /boot/efi/EFI/sprout/shimaa64.efi
$ cp /boot/efi/EFI/fedora/mmaa64.efi /boot/efi/EFI/sprout/mmaa64.efi
```
## Step 3: Install Unsigned Sprout
Download the latest sprout.efi release from the [GitHub releases page](https://github.com/edera-dev/sprout/releases).
For x86_64 systems, download the `sprout-x86_64.efi` file, and for ARM64 systems, download the `sprout-aarch64.efi` file.
Copy the downloaded `sprout.efi` file to `/boot/efi/EFI/sprout/sprout.unsigned.efi` on your EFI System Partition.
## Step 4: Sign Sprout for Secure Boot
```bash
# For x86_64, sign the unsigned Sprout artifact and name it grubx64.efi which is what the shim will call.
$ sbsign \
--key /etc/sprout/secure-boot/mok.key \
--cert /etc/sprout/secure-boot/mok.crt \
--output /boot/efi/EFI/sprout/grubx64.efi \
/boot/efi/EFI/sprout/sprout.unsigned.efi
# For aarch64, sign the unsigned Sprout artifact and name it grubaa64.efi which is what the shim will call.
$ sbsign \
--key /etc/sprout/secure-boot/mok.key \
--cert /etc/sprout/secure-boot/mok.crt \
--output /boot/efi/EFI/sprout/grubaa64.efi \
/boot/efi/EFI/sprout/sprout.unsigned.efi
```
## Step 5: Install and Sign EFI Drivers
You will need a filesystem EFI driver if `/boot` is not FAT32 or ExFAT.
### ext4
Most Fedora systems use an ext4 filesystem for `/boot`, if that is the case, use the ext4 instructions here:
Install the necessary `edk2-ext4` package:
```bash
# Install the ext4 driver from the package manager.
$ dnf install edk2-ext4
```
Copy the ext4 driver to `/boot/efi/EFI/sprout/ext4.unsigned.efi`:
```bash
# For x86_64, copy the ext4x64.efi driver to the Sprout EFI directory.
$ cp /usr/share/edk2/drivers/ext4x64.efi /boot/efi/EFI/sprout/ext4.unsigned.efi
# For aarch64, copy the ext4aa64.efi driver to the Sprout EFI directory.
$ cp /usr/share/edk2/drivers/ext4aa64.efi /boot/efi/EFI/sprout/ext4.unsigned.efi
```
```bash
# Sign the ext4 driver at ext4.unsigned.efi, placing it at ext4.efi, which will be used in the configuration.
$ sbsign \
--key /etc/sprout/secure-boot/mok.key \
--cert /etc/sprout/secure-boot/mok.crt \
--output /boot/efi/EFI/sprout/ext4.efi \
/boot/efi/EFI/sprout/ext4.unsigned.efi
```
### Other Filesystems
If you need another driver, you can download EFI filesystem drivers from [EfiFs releases](https://github.com/pbatard/EfiFs/releases).
Copy the driver to `/boot/efi/EFI/sprout/DRIVER_NAME.unsigned.efi` for signing, then sign it like this:
```bash
# Sign your driver, placing it at DRIVER_NAME.efi, which will be used in the configuration.
$ sbsign \
--key /etc/sprout/secure-boot/mok.key \
--cert /etc/sprout/secure-boot/mok.crt \
--output /boot/efi/EFI/sprout/DRIVER_NAME.efi \
/boot/efi/EFI/sprout/DRIVER_NAME.unsigned.efi
```
You will add the driver in your Sprout configuration below, like this:
```toml
[drivers.DRIVER_NAME]
path = "\\EFI\\sprout\\DRIVER_NAME.efi"
```
## Step 6: Create Sprout Configuration
Write the following to the file `/boot/efi/sprout.toml`:
```toml
# sprout configuration: version 1
version = 1
# load an ext4 EFI driver.
# skip this if you do not have a filesystem driver.
# if your filesystem driver is not named ext4, change accordingly.
[drivers.ext4]
path = "\\EFI\\sprout\\ext4.efi"
# global options.
[options]
# enable autoconfiguration by detecting bls enabled
# filesystems and generating boot entries for them.
autoconfigure = true
```
Ensure you add the signed driver paths to the configuration, not the unsigned ones.
If you do not have any drivers, exclude the drivers section entirely.
## Step 7: Configure Sprout Boot Entry
In the following commands, replace /dev/BLOCK_DEVICE with the device that houses your GPT partition table,
and PARTITION_NUMBER with the partition number of the EFI System Partition. For example, if your EFI System Partition is
`/dev/sda1`, the BLOCK_DEVICE would be `/dev/sda` and the PARTITION_NUMBER would be `1`
```bash
# For x86_64, run this command to add Sprout as the default boot entry.
$ efibootmgr -d /dev/BLOCK_DEVICE -p PARTITION_NUMBER -c -L 'Sprout' -l '\EFI\sprout\shimx64.efi'
# For aarch64, run this command to add Sprout as the default boot entry.
$ efibootmgr -d /dev/BLOCK_DEVICE -p PARTITION_NUMBER -c -L 'Sprout' -l '\EFI\sprout\shimaa64.efi'
```
Reboot your machine and it should boot into Sprout.
If Sprout fails to boot, it should boot into the original bootloader.

View File

@@ -0,0 +1,141 @@
# Setup Sprout for openSUSE with Secure Boot
**NOTE:** This guide may not function as written if the system validates hashes.
If your system validates hashes in the shim, you will need to use MokManager to enroll the hashes
of every EFI file involved, such as Sprout and any EFI drivers.
## Prerequisites
- Modern openSUSE release: tested on openSUSE Tumbleweed ARM64
- EFI System Partition mounted on `/boot/efi` (the default)
- You will need the following packages installed: `openssl`, `shim`, `mokutil`, `sbsigntools`
## Step 1: Generate and Install Secure Boot Key
```bash
# Create a directory to store the Secure Boot MOK key and certificates.
$ mkdir -p /etc/sprout/secure-boot
# Change to the created directory.
$ cd /etc/sprout/secure-boot
# Generate a MOK key and certificate.
$ openssl req \
-newkey rsa:4096 -nodes -keyout mok.key \
-new -x509 -sha256 -days 3650 -subj '/CN=Sprout Secure Boot/' \
-out mok.crt
# Generate a DER encoded certificate for enrollment.
$ openssl x509 -outform DER -in mok.crt -out mok.cer
# Import the certificate into the Secure Boot environment.
# This will ask you to make a password that will be used during enrollment.
$ mokutil --import mok.cer
# Reboot your machine.
# During boot, MOK enrollment should appear. If it doesn't, ensure you are booting into the shim.
# Press any key to begin MOK management. Select "Enroll MOK".
# Select "View key 0", and ensure the subject says "CN=Sprout Secure Boot".
# If the subject does not match, something has gone wrong with MOK enrollment.
# Press Enter to continue, then select the "Continue" option.
# When it asks to enroll the key, select the "Yes" option.
# Enter the password that you created during the mokutil --import step.
# Select "Reboot" to boot back into your Operating System.
```
## Step 2: Prepare the Secure Boot Environment
```bash
# Create a directory for Sprout EFI artifacts.
$ mkdir -p /boot/efi/EFI/sprout
# For x86_64, copy the following artifacts to the Sprout EFI directory.
$ cp /usr/share/efi/x86_64/shim.efi /boot/efi/EFI/sprout/shim.efi
$ cp /usr/share/efi/x86_64/MokManager.efi /boot/efi/EFI/sprout/MokManager.efi
$ cp /usr/share/efi/x86_64/fallback.efi /boot/efi/EFI/sprout/fallback.efi
# For aarch64, copy the following artifacts to the Sprout EFI directory.
$ cp /usr/share/efi/aarch64/shim.efi /boot/efi/EFI/sprout/shim.efi
$ cp /usr/share/efi/aarch64/MokManager.efi /boot/efi/EFI/sprout/MokManager.efi
$ cp /usr/share/efi/aarch64/fallback.efi /boot/efi/EFI/sprout/fallback.efi
```
## Step 3: Install Unsigned Sprout
Download the latest sprout.efi release from the [GitHub releases page](https://github.com/edera-dev/sprout/releases).
For x86_64 systems, download the `sprout-x86_64.efi` file, and for ARM64 systems, download the `sprout-aarch64.efi`
file.
Copy the downloaded `sprout.efi` file to `/boot/efi/EFI/sprout/sprout.unsigned.efi` on your EFI System Partition.
## Step 4: Sign Sprout for Secure Boot
```bash
# Sign the unsigned Sprout artifact and name it grub.efi which is what the shim will call.
$ sbsign \
--key /etc/sprout/secure-boot/mok.key \
--cert /etc/sprout/secure-boot/mok.crt \
--output /boot/efi/EFI/sprout/grub.efi \
/boot/efi/EFI/sprout/sprout.unsigned.efi
```
## Step 5: Install and Sign EFI Drivers
You will need a filesystem EFI driver if `/boot` is not FAT32 or ExFAT.
If `/boot` is FAT32 or ExFAT, you can skip this step.
Most Debian systems use an ext4 filesystem for `/boot`.
You can download an EFI filesystem driver from [EfiFs releases](https://github.com/pbatard/EfiFs/releases).
For ext4, download the `ext2` file for your platform. It should work for ext4 filesystems too.
If you have an EFI driver, copy the driver to `/boot/efi/EFI/sprout/DRIVER_NAME.unsigned.efi` for signing.
For example, the `ext4` driver, copy the `ext4.efi` file to `/boot/efi/EFI/sprout/ext4.unsigned.efi`.
Then sign the driver with the Sprout Secure Boot key:
```bash
# Sign the ext4 driver at ext4.unsigned.efi, placing it at ext4.efi, which will be used in the configuration.
$ sbsign \
--key /etc/sprout/secure-boot/mok.key \
--cert /etc/sprout/secure-boot/mok.crt \
--output /boot/efi/EFI/sprout/ext4.efi \
/boot/efi/EFI/sprout/ext4.unsigned.efi
```
## Step 6: Create Sprout Configuration
Write the following to the file `/boot/efi/sprout.toml`:
```toml
# sprout configuration: version 1
version = 1
# global values.
[values]
# your linux kernel command line.
linux-options = "root=UUID=MY_ROOT_UUID"
# load an ext4 EFI driver.
# skip this if you do not have a filesystem driver.
# if your filesystem driver is not named ext4, change accordingly.
[drivers.ext4]
path = "\\EFI\\sprout\\ext4.efi"
# global options.
[options]
# enable autoconfiguration by detecting bls enabled filesystems
# or linux kernels and generating boot entries for them.
autoconfigure = true
```
Ensure you add the signed driver paths to the configuration, not the unsigned ones.
If you do not have any drivers, exclude the drivers section entirely.
## Step 7: Configure Sprout Boot Entry
In the following commands, replace /dev/BLOCK_DEVICE with the device that houses your GPT partition table,
and PARTITION_NUMBER with the partition number of the EFI System Partition. For example, if your EFI System Partition is
`/dev/sda1`, the BLOCK_DEVICE would be `/dev/sda` and the PARTITION_NUMBER would be `1`
```bash
# Run this command to add Sprout as the default boot entry.
$ efibootmgr -d /dev/BLOCK_DEVICE -p PARTITION_NUMBER -c -L 'Sprout' -l '\EFI\sprout\shim.efi'
```
Reboot your machine and it should boot into Sprout.
If Sprout fails to boot, it should boot into the original bootloader.

View File

@@ -4,7 +4,6 @@
- Modern Ubuntu release: tested on Ubuntu 25.10 ARM64
- EFI System Partition mounted on `/boot/efi` (the default)
- ext4 or FAT32/exFAT formatted `/boot` partition
## Step 1: Generate and Install Secure Boot Key
@@ -60,7 +59,7 @@ Copy the downloaded `sprout.efi` file to `/boot/efi/EFI/sprout/sprout.unsigned.e
## Step 4: Sign Sprout for Secure Boot
```bash
# For x86_64, sign the unsigned Sprout artifact and name it grubaa64.efi which is what the shim will call.
# For x86_64, sign the unsigned Sprout artifact and name it grubx64.efi which is what the shim will call.
$ sbsign \
--key /etc/sprout/secure-boot/mok.key \
--cert /etc/sprout/secure-boot/mok.crt \
@@ -120,8 +119,8 @@ path = "\\EFI\\sprout\\ext4.efi"
# global options.
[options]
# enable autoconfiguration by detecting bls enabled
# filesystems and generating boot entries for them.
# enable autoconfiguration by detecting installed kernels
# generating boot entries for them.
autoconfigure = true
```
@@ -130,14 +129,18 @@ If you do not have any drivers, exclude the drivers section entirely.
## Step 7: Configure Sprout Boot Entry
In the following commands, replace /dev/ESP_PARTITION with the actual path to the ESP partition block device.
## Step 7: Configure Sprout Boot Entry
In the following commands, replace /dev/BLOCK_DEVICE with the device that houses your GPT partition table,
and PARTITION_NUMBER with the partition number of the EFI System Partition. For example, if your EFI System Partition is
`/dev/sda1`, the BLOCK_DEVICE would be `/dev/sda` and the PARTITION_NUMBER would be `1`
```bash
# For x86_64, run this command to add Sprout as the default boot entry.
$ efibootmgr -d /dev/ESP_PARTITION -c -L 'Sprout' -l '\EFI\sprout\shimx64.efi'
$ efibootmgr -d /dev/BLOCK_DEVICE -p PARTITION_NUMBER -c -L 'Sprout' -l '\EFI\sprout\shimx64.efi'
# For aarch64, run this command to add Sprout as the default boot entry.
$ efibootmgr -d /dev/ESP_PARTITION -c -L 'Sprout' -l '\EFI\sprout\shimaa64.efi'
$ efibootmgr -d /dev/BLOCK_DEVICE -p PARTITION_NUMBER -c -L 'Sprout' -l '\EFI\sprout\shimaa64.efi'
```
Reboot your machine and it should boot into Sprout.

View File

@@ -27,4 +27,6 @@ if command -v docker >/dev/null 2>&1; then
delete_image sprout-kernel-build-aarch64 || true
delete_image sprout-boot-x86_64 || true
delete_image sprout-boot-aarch64 || true
delete_image sprout-xen-x86_64 || true
delete_image sprout-xen-aarch64 || true
fi

View File

@@ -55,6 +55,13 @@ else
fi
fi
if [ "${NO_INPUT}" != "1" ]; then
set -- "${@}" \
-device qemu-xhci \
-device usb-kbd \
-device usb-mouse
fi
rm -f "${FINAL_DIR}/ovmf-boot.fd"
cp "${FINAL_DIR}/ovmf.fd" "${FINAL_DIR}/ovmf-boot.fd"
if [ "${TARGET_ARCH}" = "aarch64" ]; then

View File

@@ -1,4 +1,4 @@
FROM --platform=$BUILDPLATFORM debian:trixie@sha256:fd8f5a1df07b5195613e4b9a0b6a947d3772a151b81975db27d47f093f60c6e6 AS build
FROM --platform=$BUILDPLATFORM debian:trixie@sha256:01a723bf5bfb21b9dda0c9a33e0538106e4d02cce8f557e118dd61259553d598 AS build
ARG BUILDPLATFORM
ARG EFI_NAME
RUN export DEBIAN_FRONTEND=noninteractive && apt-get update && apt-get install -y \

View File

@@ -1,7 +1,7 @@
ARG KERNEL_SOURCE_URL=https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.17.2.tar.xz
ARG KERNEL_CHECKSUM=sha256:fdebcb065065f5c1b8dc68a6fb59cda50cdddbf9103d207c2196d55ea764f57f
ARG KERNEL_SOURCE_URL=https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.17.8.tar.xz
ARG KERNEL_CHECKSUM=sha256:5a8de64a75fca706c01c6c0a77cf75a74618439db195e25f1f0268af6b2fb1da
FROM --platform=$BUILDPLATFORM debian:trixie@sha256:fd8f5a1df07b5195613e4b9a0b6a947d3772a151b81975db27d47f093f60c6e6 AS buildenv
FROM --platform=$BUILDPLATFORM debian:trixie@sha256:01a723bf5bfb21b9dda0c9a33e0538106e4d02cce8f557e118dd61259553d598 AS buildenv
RUN export DEBIAN_FRONTEND=noninteractive && apt-get update && apt-get install -y \
build-essential squashfs-tools python3-yaml \
patch diffutils sed mawk findutils zstd \

View File

@@ -1 +1 @@
FROM --platform=$BUILDPLATFORM debian:trixie@sha256:fd8f5a1df07b5195613e4b9a0b6a947d3772a151b81975db27d47f093f60c6e6
FROM --platform=$BUILDPLATFORM debian:trixie@sha256:01a723bf5bfb21b9dda0c9a33e0538106e4d02cce8f557e118dd61259553d598

View File

@@ -1,4 +1,4 @@
ARG TARGET_IMAGE=scratch
FROM ${TARGET_IMAGE} AS image
FROM --platform=$BUILDPLATFORM debian:trixie@sha256:fd8f5a1df07b5195613e4b9a0b6a947d3772a151b81975db27d47f093f60c6e6 AS final
FROM --platform=$BUILDPLATFORM debian:trixie@sha256:01a723bf5bfb21b9dda0c9a33e0538106e4d02cce8f557e118dd61259553d598 AS final
COPY --from=image / /image

View File

@@ -1,4 +1,4 @@
FROM alpine:3.22@sha256:4bcff63911fcb4448bd4fdacec207030997caf25e9bea4045fa6c8c44de311d1 AS rootfs
FROM alpine:3.22@sha256:4b7ce07002c69e8f3d704a9c5d6fd3053be500b7f1c69fc0d80990c2ad8dd412 AS rootfs
RUN apk --no-cache add alpine-base tzdata
RUN rc-update add devfs sysinit && \
rc-update add dmesg sysinit && \
@@ -18,7 +18,7 @@ RUN rc-update add devfs sysinit && \
ln -s /usr/share/zoneinfo/UTC /etc/localtime && \
echo 'hvc0::respawn:/sbin/getty -L hvc0 115200 vt100' >> /etc/inittab
FROM alpine:3.22@sha256:4bcff63911fcb4448bd4fdacec207030997caf25e9bea4045fa6c8c44de311d1 AS build
FROM alpine:3.22@sha256:4b7ce07002c69e8f3d704a9c5d6fd3053be500b7f1c69fc0d80990c2ad8dd412 AS build
COPY --from=rootfs / /rootfs
WORKDIR /rootfs
RUN find . | cpio -R 0:0 --ignore-devno --renumber-inodes -o -H newc --quiet > /initramfs

View File

@@ -1,4 +1,4 @@
FROM alpine:3.22@sha256:4bcff63911fcb4448bd4fdacec207030997caf25e9bea4045fa6c8c44de311d1 AS build
FROM alpine:3.22@sha256:4b7ce07002c69e8f3d704a9c5d6fd3053be500b7f1c69fc0d80990c2ad8dd412 AS build
ARG TARGETPLATFORM
RUN if [ "${TARGETPLATFORM}" = "linux/amd64" ] || [ "${TARGETPLATFORM}" = "linux/x86_64" ]; then \
apk --no-cache add ovmf edk2-shell; cp /usr/share/ovmf/bios.bin /ovmf.fd; fi

View File

@@ -1,4 +1,4 @@
FROM alpine:3.22@sha256:4bcff63911fcb4448bd4fdacec207030997caf25e9bea4045fa6c8c44de311d1 AS build
FROM alpine:3.22@sha256:4b7ce07002c69e8f3d704a9c5d6fd3053be500b7f1c69fc0d80990c2ad8dd412 AS build
ARG TARGETPLATFORM
RUN apk add --no-cache xen-hypervisor && cp /usr/lib/efi/xen.efi /xen.efi

Some files were not shown because too many files have changed in this diff Show More