46 Commits

Author SHA1 Message Date
3e5d54913c sprout: version 0.0.19 2025-11-02 23:59:36 -05:00
b616e75e96 chore(workflows): release workflow should attest the efi artifacts 2025-11-02 23:57:58 -05:00
069f858e95 chore(workflows): publish workload should provide build provenance 2025-11-02 23:52:15 -05:00
ada13b7dd5 sprout: version 0.0.18 2025-11-02 23:40:57 -05:00
8179fdb565 fix(hack): remove splash copy options 2025-11-02 23:37:46 -05:00
ed3bfb77c4 chore(crates): introduce new config crate for sprout configuration 2025-11-02 23:28:31 -05:00
ccc75a2e14 chore(workspace): move most things into the workspace 2025-11-02 22:35:07 -05:00
9c12e5f12f chore(code): move sprout code to crates/sprout and remove splash support for minimalism 2025-11-02 22:23:00 -05:00
b103fdacf2 chore(docs): add debian setup guide 2025-11-02 19:52:25 -05:00
7be42ba074 chore(docs): reorganize setup guides 2025-11-02 19:26:02 -05:00
8a6f4dc19d chore(docs): add ubuntu secure boot setup guide 2025-11-02 18:08:57 -05:00
830eaca19a fix(autoconfigure/linux): workaround canonical stubble bug relating to empty load options 2025-11-02 17:58:06 -05:00
3febca5797 fix(chainload): ensure that load options are always set, even if it is to an empty string 2025-11-02 17:47:36 -05:00
524d0871f3 sprout: version 0.0.17 2025-11-02 16:28:07 -05:00
f0628f77e2 fix(shim): repair x86_64 shim verification by using the SYSV calling convention 2025-11-02 05:57:24 -05:00
cc37c2b26a fix(shim): use pinned boxed slices to represent data that needs to be passed to uefi stack 2025-11-02 05:08:03 -05:00
8d403d74c9 fix(shim/hook): clarify const-ness of some parameters 2025-11-02 04:16:05 -05:00
cc4bc6efcc fix(shim/hook): when using older hook protocol, read the data into an owned buffer 2025-11-02 02:48:41 -05:00
d4bcfcd9b1 fix(shim): reflect the const pointer-ness of the verifiable data 2025-11-02 02:45:25 -05:00
c34462b812 fix(options): accidental infinite loop due when not running on Dell firmware 2025-11-02 02:21:29 -05:00
79471f6862 fix(quirk): skip initial options that start with a tilde to work around dell firmware 2025-11-02 02:13:39 -05:00
9c31dba6fa fix(shim): only call into shim if it is available AND secure boot is enabled 2025-11-02 01:52:21 -05:00
84d60e09be fix(bootloader-interface): when there are no entries, don't attempt to set LoaderEntries 2025-11-02 01:38:07 -05:00
eabb612330 fix(shim/hook): call original hook function if the shim verify fails 2025-11-02 01:07:16 -05:00
1a6ed0af99 fix(shim): avoid masking the underlying error when shim verify fails 2025-11-02 00:27:45 -04:00
3b4a66879f fix(autoconfigure/linux): sort kernels by version, newer kernels first 2025-11-01 22:06:48 -04:00
01b3706914 fix(bls): quirk for when the version field is present already in the title 2025-11-01 21:40:39 -04:00
5033bc7bf4 fix(menu): only show the title, not the name of the entry 2025-11-01 21:36:17 -04:00
f6441b5694 feat(bls): add version comparison to ensure entries are always sorted properly 2025-11-01 21:34:55 -04:00
03d0e40141 fix(bls): add support for appending version to the title 2025-11-01 21:03:02 -04:00
3a54970386 feat(bls): initial support for sorting of entries, not using version comparison 2025-11-01 21:00:54 -04:00
8a5dc33b5a fix(options): add missing --autoconfigure flag 2025-11-01 20:07:32 -04:00
757d10ec65 fix(timer): add check for zero frequency 2025-11-01 19:18:04 -04:00
3b32f6c3ce fix(platform/timer/x86_64): if frequency is zero, panic 2025-11-01 19:08:47 -04:00
6b1d220490 fix(bls): skip over any files named ".conf" to avoid empty names 2025-11-01 18:59:41 -04:00
f7558fd024 fix(menu): prevent masking of wait for event errors if closing the timer event fails 2025-11-01 18:58:24 -04:00
d1fd13163f fix(filesystem-device-match): make behavior of ignoring filesystem errors more explicit 2025-11-01 18:51:07 -04:00
a998832f6b fix(utils): improve safety of media loader and utf-16 handling 2025-11-01 18:49:10 -04:00
992520c201 chore(main): collapse duplicate code for menu hidden or disabled 2025-11-01 18:38:08 -04:00
e9cba9da33 fix(menu): handle event freeing if wait for event fails 2025-11-01 18:37:07 -04:00
0f8f12c70f fix(bootloader-interface): fix menu time marking 2025-11-01 18:35:07 -04:00
1c732a1c43 feat(bootloader-interface): add support for LoaderEntryDefault and LoaderEntryOneShot 2025-11-01 18:04:06 -04:00
08b9e2570e fix(bootloader-interface): disable setting of LoaderEntryDefault since this is intended to be user set 2025-11-01 17:50:54 -04:00
f361570b0e feat(bootloader-interface): add support for LoaderConfigTimeout and LoaderConfigTimeoutOneShot 2025-11-01 17:47:41 -04:00
679b0c0290 feat(bootloader-interface): signal support for XBOOTLDR 2025-11-01 17:09:49 -04:00
f9dd56c8e7 feat(bootloader-interface): add support for LoaderFeatures 2025-11-01 03:24:14 -04:00
79 changed files with 1807 additions and 955 deletions

View File

@@ -27,6 +27,8 @@ jobs:
name: artifacts name: artifacts
permissions: permissions:
contents: write # Needed to upload artifacts. contents: write # Needed to upload artifacts.
id-token: write # Needed for attestation.
attestations: write # Needed for attestations.
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: harden runner - name: harden runner
@@ -46,14 +48,28 @@ jobs:
- name: 'assemble artifacts' - name: 'assemble artifacts'
run: ./hack/assemble.sh run: ./hack/assemble.sh
- name: 'upload sprout-x86_64.efi artifact' - name: 'upload sprout-x86_64.efi.zip artifact'
id: upload-sprout-x86_64-efi
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with: with:
name: sprout-x86_64.efi name: sprout-x86_64.efi.zip
path: target/assemble/sprout-x86_64.efi path: target/assemble/sprout-x86_64.efi
- name: 'upload sprout-aarch64.efi artifact' - name: 'upload sprout-aarch64.efi.zip artifact'
id: upload-sprout-aarch64-efi
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with: with:
name: sprout-aarch64.efi name: sprout-aarch64.efi.zip
path: target/assemble/sprout-aarch64.efi path: target/assemble/sprout-aarch64.efi
- name: 'attest sprout-x86_64.efi.zip artifact'
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 }}"

View File

@@ -20,6 +20,8 @@ jobs:
name: release name: release
permissions: permissions:
contents: write # Needed to upload release assets. contents: write # Needed to upload release assets.
id-token: write # Needed for attestation.
attestations: write # Needed for attestations.
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: harden runner - name: harden runner
@@ -39,6 +41,16 @@ jobs:
- name: 'assemble artifacts' - name: 'assemble artifacts'
run: ./hack/assemble.sh run: ./hack/assemble.sh
- name: 'attest sprout-x86_64.efi artifact'
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
- name: 'generate cultivator token' - name: 'generate cultivator token'
uses: actions/create-github-app-token@bf559f85448f9380bcfa2899dbdc01eb5b37be3a # v3.0.0-beta.2 uses: actions/create-github-app-token@bf559f85448f9380bcfa2899dbdc01eb5b37be3a # v3.0.0-beta.2
id: generate-token id: generate-token

169
Cargo.lock generated
View File

@@ -2,35 +2,12 @@
# It is not intended for manual editing. # It is not intended for manual editing.
version = 4 version = 4
[[package]]
name = "adler2"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
[[package]] [[package]]
name = "anyhow" name = "anyhow"
version = "1.0.100" version = "1.0.100"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
[[package]]
name = "async-trait"
version = "0.1.89"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "autocfg"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]] [[package]]
name = "bit_field" name = "bit_field"
version = "0.10.3" version = "0.10.3"
@@ -52,24 +29,6 @@ dependencies = [
"generic-array", "generic-array",
] ]
[[package]]
name = "bytemuck"
version = "1.24.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4"
[[package]]
name = "byteorder-lite"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
[[package]]
name = "bytes"
version = "1.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
[[package]] [[package]]
name = "cfg-if" name = "cfg-if"
version = "1.0.4" version = "1.0.4"
@@ -85,15 +44,6 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "crc32fast"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
dependencies = [
"cfg-if",
]
[[package]] [[package]]
name = "crypto-common" name = "crypto-common"
version = "0.1.6" version = "0.1.6"
@@ -116,43 +66,32 @@ dependencies = [
[[package]] [[package]]
name = "edera-sprout" name = "edera-sprout"
version = "0.0.16" version = "0.0.19"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"image", "bitflags",
"edera-sprout-config",
"hex",
"log", "log",
"serde", "sha2",
"sha256",
"toml", "toml",
"uefi", "uefi",
"uefi-raw", "uefi-raw",
] ]
[[package]]
name = "edera-sprout-config"
version = "0.0.19"
dependencies = [
"serde",
]
[[package]] [[package]]
name = "equivalent" name = "equivalent"
version = "1.0.2" version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "fdeflate"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c"
dependencies = [
"simd-adler32",
]
[[package]]
name = "flate2"
version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb"
dependencies = [
"crc32fast",
"miniz_oxide",
]
[[package]] [[package]]
name = "generic-array" name = "generic-array"
version = "0.14.9" version = "0.14.9"
@@ -175,19 +114,6 @@ version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "image"
version = "0.25.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "529feb3e6769d234375c4cf1ee2ce713682b8e76538cb13f9fc23e1400a591e7"
dependencies = [
"bytemuck",
"byteorder-lite",
"moxcms",
"num-traits",
"png",
]
[[package]] [[package]]
name = "indexmap" name = "indexmap"
version = "2.12.0" version = "2.12.0"
@@ -210,47 +136,6 @@ version = "0.4.28"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432"
[[package]]
name = "miniz_oxide"
version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
dependencies = [
"adler2",
"simd-adler32",
]
[[package]]
name = "moxcms"
version = "0.7.6"
source = "git+https://github.com/edera-dev/sprout-patched-deps.git?rev=2c4fcc84b50d40c28f540d4271109ea0ca7e1268#2c4fcc84b50d40c28f540d4271109ea0ca7e1268"
dependencies = [
"num-traits",
"pxfm",
]
[[package]]
name = "num-traits"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
]
[[package]]
name = "png"
version = "0.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97baced388464909d42d89643fe4361939af9b7ce7a31ee32a168f832a70f2a0"
dependencies = [
"bitflags",
"crc32fast",
"fdeflate",
"flate2",
"miniz_oxide",
]
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.103" version = "1.0.103"
@@ -280,15 +165,6 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "pxfm"
version = "0.1.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3cbdf373972bf78df4d3b518d07003938e2c7d1fb5891e55f9cb6df57009d84"
dependencies = [
"num-traits",
]
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.41" version = "1.0.41"
@@ -348,23 +224,6 @@ dependencies = [
"digest", "digest",
] ]
[[package]]
name = "sha256"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f880fc8562bdeb709793f00eb42a2ad0e672c4f883bbe59122b926eca935c8f6"
dependencies = [
"async-trait",
"bytes",
"hex",
"sha2",
]
[[package]]
name = "simd-adler32"
version = "0.3.7"
source = "git+https://github.com/edera-dev/sprout-patched-deps.git?rev=2c4fcc84b50d40c28f540d4271109ea0ca7e1268#2c4fcc84b50d40c28f540d4271109ea0ca7e1268"
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.108" version = "2.0.108"
@@ -475,9 +334,9 @@ checksum = "0c8352f8c05e47892e7eaf13b34abd76a7f4aeaf817b716e88789381927f199c"
[[package]] [[package]]
name = "unicode-ident" name = "unicode-ident"
version = "1.0.20" version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "462eeb75aeb73aea900253ce739c8e18a67423fadf006037cd3ff27e82748a06" checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
[[package]] [[package]]
name = "version_check" name = "version_check"

View File

@@ -1,45 +1,32 @@
[package] [workspace]
name = "edera-sprout" members = [
description = "Modern UEFI bootloader" "crates/config",
"crates/sprout",
]
resolver = "3"
[workspace.package]
license = "Apache-2.0" license = "Apache-2.0"
version = "0.0.16" version = "0.0.19"
homepage = "https://sprout.edera.dev" homepage = "https://sprout.edera.dev"
repository = "https://github.com/edera-dev/sprout" repository = "https://github.com/edera-dev/sprout"
edition = "2024" edition = "2024"
[dependencies] [workspace.dependencies]
anyhow = "1.0.100" anyhow = "1.0.100"
toml = "0.9.8" bitflags = "2.10.0"
hex = "0.4.3"
log = "0.4.28" 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"
[dependencies.image] # Common build profiles
version = "0.25.8" # NOTE: We have to compile everything for opt-level = 2 due to optimization passes
default-features = false
features = ["png"]
optional = true
[dependencies.serde]
version = "1.0.228"
features = ["derive"]
[dependencies.sha256]
version = "1.6.0"
default-features = false
[dependencies.uefi]
version = "0.36.0"
features = ["alloc", "logger"]
[dependencies.uefi-raw]
version = "0.12.0"
[features]
default = ["splash"]
splash = ["dep:image"]
[profile.dev]
# We have to compile for opt-level = 2 due to optimization passes
# which don't handle the UEFI target properly. # which don't handle the UEFI target properly.
[profile.dev]
opt-level = 2 opt-level = 2
[profile.release] [profile.release]
@@ -56,15 +43,3 @@ inherits = "dev"
strip = "debuginfo" strip = "debuginfo"
debug = 0 debug = 0
opt-level = 2 opt-level = 2
[patch.crates-io.simd-adler32]
git = "https://github.com/edera-dev/sprout-patched-deps.git"
rev = "2c4fcc84b50d40c28f540d4271109ea0ca7e1268"
[patch.crates-io.moxcms]
git = "https://github.com/edera-dev/sprout-patched-deps.git"
rev = "2c4fcc84b50d40c28f540d4271109ea0ca7e1268"
[[bin]]
name = "sprout"
path = "src/main.rs"

View File

@@ -18,6 +18,9 @@ existing UEFI bootloader or booted by the hardware directly.
Sprout is licensed under Apache 2.0 and is open to modifications and contributions. Sprout is licensed under Apache 2.0 and is open to modifications and contributions.
**NOTE**: Sprout is still in beta. Some features may not work as expected.
Please [report any bugs you find](https://github.com/edera-dev/sprout/issues/new/choose).
## Background ## Background
At [Edera] we make compute isolation technology for a wide variety of environments, often ones we do not fully control. At [Edera] we make compute isolation technology for a wide variety of environments, often ones we do not fully control.
@@ -37,10 +40,22 @@ simplify installation and usage.
## Documentation ## Documentation
- [Fedora Setup Guide] ### Setup Guides
- [Generic Linux Setup Guide]
- [Alpine Edge Setup Guide] Some guides support Secure Boot and some do not.
- [Windows Setup Guide] We recommend running Sprout without Secure Boot for development, and with Secure Boot for production.
| Operating System | Secure Boot Enabled | Link |
|------------------|---------------------|-------------------------------------------------------|
| Ubuntu | ✅ | [Setup Guide](./docs/setup/signed/ubuntu.md) |
| Debian | ✅ | [Setup Guide](./docs/setup/signed/debian.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) |
| Windows | ❌ | [Setup Guide](./docs/setup/unsigned/windows.md) |
### Project Documentation
- [Development Guide] - [Development Guide]
- [Contributing Guide] - [Contributing Guide]
- [Sprout License] - [Sprout License]
@@ -49,8 +64,6 @@ simplify installation and usage.
## Features ## Features
**NOTE**: Sprout is still in beta.
### Current ### Current
- [x] Loadable driver support - [x] Loadable driver support
@@ -61,12 +74,12 @@ simplify installation and usage.
- [x] Load Linux initrd from disk - [x] Load Linux initrd from disk
- [x] Basic boot menu - [x] Basic boot menu
- [x] BLS autoconfiguration support - [x] BLS autoconfiguration support
- [x] [Secure Boot support](https://github.com/edera-dev/sprout/issues/20): partial - [x] [Secure Boot support](https://github.com/edera-dev/sprout/issues/20): beta
- [x] [Bootloader interface support](https://github.com/edera-dev/sprout/issues/21): beta
- [x] [BLS specification conformance](https://github.com/edera-dev/sprout/issues/2): beta
### Roadmap ### Roadmap
- [ ] [Bootloader interface support](https://github.com/edera-dev/sprout/issues/21)
- [ ] [BLS specification conformance](https://github.com/edera-dev/sprout/issues/2)
- [ ] [Full-featured boot menu](https://github.com/edera-dev/sprout/issues/1) - [ ] [Full-featured boot menu](https://github.com/edera-dev/sprout/issues/1)
- [ ] [UKI support](https://github.com/edera-dev/sprout/issues/6): partial - [ ] [UKI support](https://github.com/edera-dev/sprout/issues/6): partial
- [ ] [multiboot2 support](https://github.com/edera-dev/sprout/issues/7) - [ ] [multiboot2 support](https://github.com/edera-dev/sprout/issues/7)
@@ -147,10 +160,6 @@ autoconfigure = true
``` ```
[Edera]: https://edera.dev [Edera]: https://edera.dev
[Fedora Setup Guide]: ./docs/fedora-setup.md
[Generic Linux Setup Guide]: ./docs/generic-linux-setup.md
[Alpine Edge Setup Guide]: ./docs/alpine-edge-setup.md
[Windows Setup Guide]: ./docs/windows-setup.md
[Development Guide]: ./DEVELOPMENT.md [Development Guide]: ./DEVELOPMENT.md
[Contributing Guide]: ./CONTRIBUTING.md [Contributing Guide]: ./CONTRIBUTING.md
[Sprout License]: ./LICENSE [Sprout License]: ./LICENSE

15
crates/config/Cargo.toml Normal file
View File

@@ -0,0 +1,15 @@
[package]
name = "edera-sprout-config"
description = "Sprout Configuration"
license.workspace = true
version.workspace = true
homepage.workspace = true
repository.workspace = true
edition.workspace = true
[dependencies.serde]
workspace = true
features = ["derive"]
[lib]
name = "edera_sprout_config"

View File

@@ -0,0 +1,32 @@
use serde::{Deserialize, Serialize};
/// Configuration for the chainload action.
pub mod chainload;
/// Configuration for the edera action.
pub mod edera;
/// Configuration for the print action.
pub mod print;
/// Declares an action that sprout can execute.
/// Actions allow configuring sprout's internal runtime mechanisms with values
/// that you can specify via other concepts.
///
/// Actions are the main work that Sprout gets done, like booting Linux.
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
pub struct ActionDeclaration {
/// Chainload to another EFI application.
/// This allows you to load any EFI application, either to boot an operating system
/// or to perform more EFI actions and return to sprout.
#[serde(default)]
pub chainload: Option<chainload::ChainloadConfiguration>,
/// Print a string to the EFI console.
#[serde(default)]
pub print: Option<print::PrintConfiguration>,
/// Boot the Edera hypervisor and the root operating system.
/// This action is an extension on top of the Xen EFI stub that
/// is specific to Edera.
#[serde(default, rename = "edera")]
pub edera: Option<edera::EderaConfiguration>,
}

View File

@@ -0,0 +1,19 @@
use serde::{Deserialize, Serialize};
/// The configuration of the chainload action.
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
pub struct ChainloadConfiguration {
/// The path to the image to chainload.
/// This can be a Linux EFI stub (vmlinuz usually) or a standard EFI executable.
pub path: String,
/// The options to pass to the image.
/// The options are concatenated by a space and then passed to the EFI application.
#[serde(default)]
pub options: Vec<String>,
/// An optional path to a Linux initrd.
/// This uses the [LINUX_EFI_INITRD_MEDIA_GUID] mechanism to load the initrd into the EFI stack.
/// For Linux, you can also use initrd=\path\to\initrd as an option, but this option is
/// generally better and safer as it can support additional load options in the future.
#[serde(default, rename = "linux-initrd")]
pub linux_initrd: Option<String>,
}

View File

@@ -0,0 +1,21 @@
use serde::{Deserialize, Serialize};
/// The configuration of the edera action which boots the Edera hypervisor.
/// Edera is based on Xen but modified significantly with a Rust stack.
/// Sprout is a component of the Edera stack and provides the boot functionality of Xen.
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
pub struct EderaConfiguration {
/// The path to the Xen hypervisor EFI image.
pub xen: String,
/// The path to the kernel to boot for dom0.
pub kernel: String,
/// The path to the initrd to load for dom0.
#[serde(default)]
pub initrd: Option<String>,
/// The options to pass to the kernel.
#[serde(default, rename = "kernel-options")]
pub kernel_options: Vec<String>,
/// The options to pass to the Xen hypervisor.
#[serde(default, rename = "xen-options")]
pub xen_options: Vec<String>,
}

View File

@@ -0,0 +1,9 @@
use serde::{Deserialize, Serialize};
/// The configuration of the print action.
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
pub struct PrintConfiguration {
/// The text to print to the console.
#[serde(default)]
pub text: String,
}

View File

@@ -0,0 +1,12 @@
use serde::{Deserialize, Serialize};
/// Declares a driver configuration.
/// Drivers allow extending the functionality of Sprout.
/// Drivers are loaded at runtime and can provide extra functionality like filesystem support.
/// Drivers are loaded by their name, which is used to reference them in other concepts.
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
pub struct DriverDeclaration {
/// The filesystem path to the driver.
/// This file should be an EFI executable that can be located and executed.
pub path: String,
}

View File

@@ -0,0 +1,19 @@
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
/// Declares a boot entry to display in the boot menu.
///
/// Entries are the user-facing concept of Sprout, making it possible
/// to run a set of actions with a specific context.
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
pub struct EntryDeclaration {
/// The title of the entry which will be display in the boot menu.
/// This is the pre-stamped value.
pub title: String,
/// The actions to run when the entry is selected.
#[serde(default)]
pub actions: Vec<String>,
/// The values to insert into the context when the entry is selected.
#[serde(default)]
pub values: BTreeMap<String, String>,
}

View File

@@ -1,10 +1,7 @@
use crate::context::SproutContext;
use crate::extractors::filesystem_device_match::FilesystemDeviceMatchExtractor; use crate::extractors::filesystem_device_match::FilesystemDeviceMatchExtractor;
use anyhow::{Result, bail};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::rc::Rc;
/// The filesystem device match extractor. /// Configuration for the filesystem-device-match extractor.
pub mod filesystem_device_match; pub mod filesystem_device_match;
/// Declares an extractor configuration. /// Declares an extractor configuration.
@@ -19,14 +16,3 @@ pub struct ExtractorDeclaration {
#[serde(default, rename = "filesystem-device-match")] #[serde(default, rename = "filesystem-device-match")]
pub filesystem_device_match: Option<FilesystemDeviceMatchExtractor>, pub filesystem_device_match: Option<FilesystemDeviceMatchExtractor>,
} }
/// Extracts the value using the specified `extractor` under the provided `context`.
/// The extractor must return a value, and if a value cannot be determined, an error
/// should be returned.
pub fn extract(context: Rc<SproutContext>, extractor: &ExtractorDeclaration) -> Result<String> {
if let Some(filesystem) = &extractor.filesystem_device_match {
filesystem_device_match::extract(context, filesystem)
} else {
bail!("unknown extractor configuration");
}
}

View File

@@ -0,0 +1,29 @@
use serde::{Deserialize, Serialize};
/// The filesystem device match extractor.
/// This extractor finds a filesystem using some search criteria and returns
/// the device root path that can concatenated with subpaths to access files
/// on a particular filesystem.
/// The fallback value can be used to provide a value if no match is found.
///
/// This extractor requires all the criteria to match. If no criteria is provided,
/// an error is returned.
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
pub struct FilesystemDeviceMatchExtractor {
/// Matches a filesystem that has the specified label.
#[serde(default, rename = "has-label")]
pub has_label: Option<String>,
/// Matches a filesystem that has the specified item.
/// An item is either a directory or file.
#[serde(default, rename = "has-item")]
pub has_item: Option<String>,
/// Matches a filesystem that has the specified partition UUID.
#[serde(default, rename = "has-partition-uuid")]
pub has_partition_uuid: Option<String>,
/// Matches a filesystem that has the specified partition type UUID.
#[serde(default, rename = "has-partition-type-uuid")]
pub has_partition_type_uuid: Option<String>,
/// The fallback value to use if no filesystem matches the criteria.
#[serde(default)]
pub fallback: Option<String>,
}

View File

@@ -1,15 +1,15 @@
use crate::context::SproutContext;
use crate::entries::BootableEntry;
use crate::generators::bls::BlsConfiguration; use crate::generators::bls::BlsConfiguration;
use crate::generators::list::ListConfiguration; use crate::generators::list::ListConfiguration;
use crate::generators::matrix::MatrixConfiguration; use crate::generators::matrix::MatrixConfiguration;
use anyhow::Result;
use anyhow::bail;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::rc::Rc;
/// Configuration for the BLS generator.
pub mod bls; pub mod bls;
/// Configuration for the list generator.
pub mod list; pub mod list;
/// Configuration for the matrix generator.
pub mod matrix; pub mod matrix;
/// Declares a generator configuration. /// Declares a generator configuration.
@@ -38,21 +38,3 @@ pub struct GeneratorDeclaration {
/// Allows you to specify a list of values to generate an entry from. /// Allows you to specify a list of values to generate an entry from.
pub list: Option<ListConfiguration>, pub list: Option<ListConfiguration>,
} }
/// Runs the generator specified by the `generator` option.
/// It uses the specified `context` as the parent context for
/// the generated entries, injecting more values if needed.
pub fn generate(
context: Rc<SproutContext>,
generator: &GeneratorDeclaration,
) -> Result<Vec<BootableEntry>> {
if let Some(matrix) = &generator.matrix {
matrix::generate(context, matrix)
} else if let Some(bls) = &generator.bls {
bls::generate(context, bls)
} else if let Some(list) = &generator.list {
list::generate(context, list)
} else {
bail!("unknown generator configuration");
}
}

View File

@@ -0,0 +1,21 @@
use crate::entries::EntryDeclaration;
use serde::{Deserialize, Serialize};
/// The default path to the BLS directory.
const BLS_TEMPLATE_PATH: &str = "\\loader";
/// The configuration of the BLS generator.
/// The BLS uses the Bootloader Specification to produce
/// entries from an input template.
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
pub struct BlsConfiguration {
/// The entry to use for as a template.
pub entry: EntryDeclaration,
/// The path to the BLS directory.
#[serde(default = "default_bls_path")]
pub path: String,
}
fn default_bls_path() -> String {
BLS_TEMPLATE_PATH.to_string()
}

View File

@@ -0,0 +1,16 @@
use crate::entries::EntryDeclaration;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
/// List generator configuration.
/// The list generator produces multiple entries based
/// on a set of input maps.
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
pub struct ListConfiguration {
/// The template entry to use for each generated entry.
#[serde(default)]
pub entry: EntryDeclaration,
/// The values to use as the input for the matrix.
#[serde(default)]
pub values: Vec<BTreeMap<String, String>>,
}

View File

@@ -0,0 +1,16 @@
use crate::entries::EntryDeclaration;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
/// Matrix generator configuration.
/// The matrix generator produces multiple entries based
/// on input values multiplicatively.
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
pub struct MatrixConfiguration {
/// The template entry to use for each generated entry.
#[serde(default)]
pub entry: EntryDeclaration,
/// The values to use as the input for the matrix.
#[serde(default)]
pub values: BTreeMap<String, Vec<String>>,
}

View File

@@ -1,3 +1,6 @@
//! Sprout configuration descriptions.
//! This crate provides all the configuration structures for Sprout.
use crate::actions::ActionDeclaration; use crate::actions::ActionDeclaration;
use crate::drivers::DriverDeclaration; use crate::drivers::DriverDeclaration;
use crate::entries::EntryDeclaration; use crate::entries::EntryDeclaration;
@@ -7,8 +10,12 @@ use crate::phases::PhasesConfiguration;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::BTreeMap; use std::collections::BTreeMap;
/// The configuration loader mechanisms. pub mod actions;
pub mod loader; pub mod drivers;
pub mod entries;
pub mod extractors;
pub mod generators;
pub mod phases;
/// This is the latest version of the sprout configuration format. /// This is the latest version of the sprout configuration format.
/// This must be incremented when the configuration breaks compatibility. /// This must be incremented when the configuration breaks compatibility.
@@ -80,7 +87,8 @@ pub struct OptionsConfiguration {
pub autoconfigure: bool, pub autoconfigure: bool,
} }
fn latest_version() -> u32 { /// Get the latest version of the Sprout configuration format.
pub fn latest_version() -> u32 {
LATEST_VERSION LATEST_VERSION
} }

View File

@@ -1,9 +1,5 @@
use crate::actions;
use crate::context::SproutContext;
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::BTreeMap; use std::collections::BTreeMap;
use std::rc::Rc;
/// Configures the various phases of the boot process. /// Configures the various phases of the boot process.
/// This allows hooking various phases to run actions. /// This allows hooking various phases to run actions.
@@ -32,23 +28,3 @@ pub struct PhaseConfiguration {
#[serde(default)] #[serde(default)]
pub values: BTreeMap<String, String>, pub values: BTreeMap<String, String>,
} }
/// Executes the specified [phase] of the boot process.
/// The value [phase] should be a reference of a specific phase in the [PhasesConfiguration].
/// Any error from the actions is propagated into the [Result] and will interrupt further
/// execution of phase actions.
pub fn phase(context: Rc<SproutContext>, phase: &[PhaseConfiguration]) -> Result<()> {
for item in phase {
let mut context = context.fork();
// Insert the values into the context.
context.insert(&item.values);
let context = context.freeze();
// Execute all the actions in this phase configuration.
for action in item.actions.iter() {
actions::execute(context.clone(), action)
.context(format!("unable to execute action '{}'", action))?;
}
}
Ok(())
}

28
crates/sprout/Cargo.toml Normal file
View File

@@ -0,0 +1,28 @@
[package]
name = "edera-sprout"
description = "Modern UEFI bootloader"
license.workspace = true
version.workspace = true
homepage.workspace = true
repository.workspace = true
edition.workspace = true
[dependencies]
anyhow.workspace = true
bitflags.workspace = true
edera-sprout-config.path = "../config"
hex.workspace = true
sha2.workspace = true
toml.workspace = true
log.workspace = true
[dependencies.uefi]
workspace = true
features = ["alloc", "logger"]
[dependencies.uefi-raw]
workspace = true
[[bin]]
name = "sprout"
path = "src/main.rs"

3
crates/sprout/README.md Normal file
View File

@@ -0,0 +1,3 @@
# Sprout Bootloader
The main bootable crate of the Sprout bootloader.

View File

@@ -1,6 +1,5 @@
use crate::context::SproutContext; use crate::context::SproutContext;
use anyhow::{Context, Result, bail}; use anyhow::{Context, Result, bail};
use serde::{Deserialize, Serialize};
use std::rc::Rc; use std::rc::Rc;
/// EFI chainloader action. /// EFI chainloader action.
@@ -10,36 +9,6 @@ pub mod edera;
/// EFI console print action. /// EFI console print action.
pub mod print; pub mod print;
/// Splash screen action.
#[cfg(feature = "splash")]
pub mod splash;
/// Declares an action that sprout can execute.
/// Actions allow configuring sprout's internal runtime mechanisms with values
/// that you can specify via other concepts.
///
/// Actions are the main work that Sprout gets done, like booting Linux.
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
pub struct ActionDeclaration {
/// Chainload to another EFI application.
/// This allows you to load any EFI application, either to boot an operating system
/// or to perform more EFI actions and return to sprout.
#[serde(default)]
pub chainload: Option<chainload::ChainloadConfiguration>,
/// Print a string to the EFI console.
#[serde(default)]
pub print: Option<print::PrintConfiguration>,
/// Show an image as a fullscreen splash screen.
#[serde(default)]
#[cfg(feature = "splash")]
pub splash: Option<splash::SplashConfiguration>,
/// Boot the Edera hypervisor and the root operating system.
/// This action is an extension on top of the Xen EFI stub that
/// is specific to Edera.
#[serde(default, rename = "edera")]
pub edera: Option<edera::EderaConfiguration>,
}
/// Execute the action specified by `name` which should be stored in the /// Execute the action specified by `name` which should be stored in the
/// root context of the provided `context`. This function may not return /// root context of the provided `context`. This function may not return
/// if the provided action executes an operating system or an EFI application /// if the provided action executes an operating system or an EFI application
@@ -67,12 +36,6 @@ pub fn execute(context: Rc<SproutContext>, name: impl AsRef<str>) -> Result<()>
return Ok(()); return Ok(());
} }
#[cfg(feature = "splash")]
if let Some(splash) = &action.splash {
splash::splash(context.clone(), splash)?;
return Ok(());
}
// If we reach here, we don't know how to execute the action that was configured. // If we reach here, we don't know how to execute the action that was configured.
// This is likely unreachable, but we should still return an error just in case. // This is likely unreachable, but we should still return an error just in case.
bail!("unknown action configuration"); bail!("unknown action configuration");

View File

@@ -5,30 +5,12 @@ use crate::utils;
use crate::utils::media_loader::MediaLoaderHandle; use crate::utils::media_loader::MediaLoaderHandle;
use crate::utils::media_loader::constants::linux::LINUX_EFI_INITRD_MEDIA_GUID; use crate::utils::media_loader::constants::linux::LINUX_EFI_INITRD_MEDIA_GUID;
use anyhow::{Context, Result, bail}; use anyhow::{Context, Result, bail};
use edera_sprout_config::actions::chainload::ChainloadConfiguration;
use log::error; use log::error;
use serde::{Deserialize, Serialize};
use std::rc::Rc; use std::rc::Rc;
use uefi::CString16; use uefi::CString16;
use uefi::proto::loaded_image::LoadedImage; use uefi::proto::loaded_image::LoadedImage;
/// The configuration of the chainload action.
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
pub struct ChainloadConfiguration {
/// The path to the image to chainload.
/// This can be a Linux EFI stub (vmlinuz usually) or a standard EFI executable.
pub path: String,
/// The options to pass to the image.
/// The options are concatenated by a space and then passed to the EFI application.
#[serde(default)]
pub options: Vec<String>,
/// An optional path to a Linux initrd.
/// This uses the [LINUX_EFI_INITRD_MEDIA_GUID] mechanism to load the initrd into the EFI stack.
/// For Linux, you can also use initrd=\path\to\initrd as an option, but this option is
/// generally better and safer as it can support additional load options in the future.
#[serde(default, rename = "linux-initrd")]
pub linux_initrd: Option<String>,
}
/// Executes the chainload action using the specified `configuration` inside the provided `context`. /// Executes the chainload action using the specified `configuration` inside the provided `context`.
pub fn chainload(context: Rc<SproutContext>, configuration: &ChainloadConfiguration) -> Result<()> { pub fn chainload(context: Rc<SproutContext>, configuration: &ChainloadConfiguration) -> Result<()> {
// Retrieve the current image handle of sprout. // Retrieve the current image handle of sprout.
@@ -53,30 +35,25 @@ pub fn chainload(context: Rc<SproutContext>, configuration: &ChainloadConfigurat
let options = let options =
utils::combine_options(configuration.options.iter().map(|item| context.stamp(item))); utils::combine_options(configuration.options.iter().map(|item| context.stamp(item)));
// Pass the options to the image, if any are provided. // Pass the load options to the image.
// The holder must drop at the end of this function to ensure the options are not leaked, // If no options are provided, the resulting string will be empty.
// and the holder here ensures it outlives the if block here, as a pointer has to be // The options are pinned and boxed to ensure that they are valid for the lifetime of this
// passed to the image. // function, which ensures the lifetime of the options for the image runtime.
// SAFETY: The options outlive the usage of the image, and the image is not used after this. let options = Box::pin(
let mut options_holder: Option<Box<CString16>> = None; CString16::try_from(&options[..])
if !options.is_empty() { .context("unable to convert chainloader options to CString16")?,
let options = Box::new( );
CString16::try_from(&options[..])
.context("unable to convert chainloader options to CString16")?,
);
if options.num_bytes() > u32::MAX as usize { if options.num_bytes() > u32::MAX as usize {
bail!("chainloader options too large"); bail!("chainloader options too large");
} }
// SAFETY: option size is checked to validate it is safe to pass. // SAFETY: option size is checked to validate it is safe to pass.
// Additionally, the pointer is allocated and retained on heap, which makes // Additionally, the pointer is allocated and retained on heap, which makes
// passing the `options` pointer safe to the next image. // passing the `options` pointer safe to the next image.
unsafe { unsafe {
loaded_image_protocol loaded_image_protocol
.set_load_options(options.as_ptr() as *const u8, options.num_bytes() as u32); .set_load_options(options.as_ptr() as *const u8, options.num_bytes() as u32);
}
options_holder = Some(options);
} }
// Stamp the initrd path, if provided. // Stamp the initrd path, if provided.
@@ -118,8 +95,9 @@ pub fn chainload(context: Rc<SproutContext>, configuration: &ChainloadConfigurat
// Assert there was no error starting the image. // Assert there was no error starting the image.
result.context("unable to start image")?; result.context("unable to start image")?;
// Explicitly drop the option holder to clarify the lifetime.
drop(options_holder); // Explicitly drop the options to clarify the lifetime.
drop(options);
// Return control to sprout. // Return control to sprout.
Ok(()) Ok(())

View File

@@ -1,12 +1,7 @@
use std::rc::Rc; use std::rc::Rc;
use anyhow::{Context, Result};
use log::error;
use serde::{Deserialize, Serialize};
use uefi::Guid;
use crate::{ use crate::{
actions::{self, chainload::ChainloadConfiguration}, actions,
context::SproutContext, context::SproutContext,
utils::{ utils::{
self, self,
@@ -18,26 +13,11 @@ use crate::{
}, },
}, },
}; };
use anyhow::{Context, Result};
/// The configuration of the edera action which boots the Edera hypervisor. use edera_sprout_config::actions::chainload::ChainloadConfiguration;
/// Edera is based on Xen but modified significantly with a Rust stack. use edera_sprout_config::actions::edera::EderaConfiguration;
/// Sprout is a component of the Edera stack and provides the boot functionality of Xen. use log::error;
#[derive(Serialize, Deserialize, Debug, Default, Clone)] use uefi::Guid;
pub struct EderaConfiguration {
/// The path to the Xen hypervisor EFI image.
pub xen: String,
/// The path to the kernel to boot for dom0.
pub kernel: String,
/// The path to the initrd to load for dom0.
#[serde(default)]
pub initrd: Option<String>,
/// The options to pass to the kernel.
#[serde(default, rename = "kernel-options")]
pub kernel_options: Vec<String>,
/// The options to pass to the Xen hypervisor.
#[serde(default, rename = "xen-options")]
pub xen_options: Vec<String>,
}
/// Builds a configuration string for the Xen EFI stub using the specified `configuration`. /// Builds a configuration string for the Xen EFI stub using the specified `configuration`.
fn build_xen_config(context: Rc<SproutContext>, configuration: &EderaConfiguration) -> String { fn build_xen_config(context: Rc<SproutContext>, configuration: &EderaConfiguration) -> String {

View File

@@ -1,17 +1,9 @@
use crate::context::SproutContext; use crate::context::SproutContext;
use anyhow::Result; use anyhow::Result;
use edera_sprout_config::actions::print::PrintConfiguration;
use log::info; use log::info;
use serde::{Deserialize, Serialize};
use std::rc::Rc; use std::rc::Rc;
/// The configuration of the print action.
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
pub struct PrintConfiguration {
/// The text to print to the console.
#[serde(default)]
pub text: String,
}
/// Executes the print action with the specified `configuration` inside the provided `context`. /// Executes the print action with the specified `configuration` inside the provided `context`.
pub fn print(context: Rc<SproutContext>, configuration: &PrintConfiguration) -> Result<()> { pub fn print(context: Rc<SproutContext>, configuration: &PrintConfiguration) -> Result<()> {
info!("{}", context.stamp(&configuration.text)); info!("{}", context.stamp(&configuration.text));

View File

@@ -1,5 +1,5 @@
use crate::config::RootConfiguration;
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use edera_sprout_config::RootConfiguration;
use uefi::fs::FileSystem; use uefi::fs::FileSystem;
use uefi::proto::device_path::DevicePath; use uefi::proto::device_path::DevicePath;
use uefi::proto::media::fs::SimpleFileSystem; use uefi::proto::media::fs::SimpleFileSystem;

View File

@@ -1,11 +1,11 @@
use crate::actions::ActionDeclaration;
use crate::actions::chainload::ChainloadConfiguration;
use crate::config::RootConfiguration;
use crate::entries::EntryDeclaration;
use crate::generators::GeneratorDeclaration;
use crate::generators::bls::BlsConfiguration;
use crate::utils; use crate::utils;
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use edera_sprout_config::RootConfiguration;
use edera_sprout_config::actions::ActionDeclaration;
use edera_sprout_config::actions::chainload::ChainloadConfiguration;
use edera_sprout_config::entries::EntryDeclaration;
use edera_sprout_config::generators::GeneratorDeclaration;
use edera_sprout_config::generators::bls::BlsConfiguration;
use uefi::cstr16; use uefi::cstr16;
use uefi::fs::{FileSystem, Path}; use uefi::fs::{FileSystem, Path};
use uefi::proto::device_path::DevicePath; use uefi::proto::device_path::DevicePath;

View File

@@ -1,11 +1,12 @@
use crate::actions::ActionDeclaration;
use crate::actions::chainload::ChainloadConfiguration;
use crate::config::RootConfiguration;
use crate::entries::EntryDeclaration;
use crate::generators::GeneratorDeclaration;
use crate::generators::list::ListConfiguration;
use crate::utils; use crate::utils;
use crate::utils::vercmp;
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use edera_sprout_config::RootConfiguration;
use edera_sprout_config::actions::ActionDeclaration;
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 std::collections::BTreeMap;
use uefi::CString16; use uefi::CString16;
use uefi::fs::{FileSystem, Path, PathBuf}; use uefi::fs::{FileSystem, Path, PathBuf};
@@ -26,6 +27,18 @@ const KERNEL_PREFIXES: &[&str] = &["vmlinuz"];
/// Prefixes of initramfs files to match to. /// Prefixes of initramfs files to match to.
const INITRAMFS_PREFIXES: &[&str] = &["initramfs", "initrd", "initrd.img"]; const INITRAMFS_PREFIXES: &[&str] = &["initramfs", "initrd", "initrd.img"];
/// This is really silly, but if what we are booting is the Canonical stubble stub,
/// there is a chance it will assert that the load options are non-empty.
/// Technically speaking, load options can be empty. However, it assumes load options
/// have something in it. Canonical's stubble copied code from systemd that does this
/// and then uses that code improperly by asserting that the pointer is non-null.
/// To give a good user experience, we place a placeholder value here to ensure it's non-empty.
/// For stubble, this code ensures the command line pointer becomes null:
/// https://github.com/ubuntu/stubble/blob/e56643979addfb98982266018e08921c07424a0c/stub.c#L61-L64
/// Then this code asserts on it, stopping the boot process:
/// https://github.com/ubuntu/stubble/blob/e56643979addfb98982266018e08921c07424a0c/stub.c#L27
const DEFAULT_LINUX_OPTIONS: &str = "placeholder";
/// Pair of kernel and initramfs. /// Pair of kernel and initramfs.
/// This is what scanning a directory is meant to find. /// This is what scanning a directory is meant to find.
struct KernelPair { struct KernelPair {
@@ -170,6 +183,9 @@ pub fn scan(
return Ok(false); return Ok(false);
} }
// Sort the kernel pairs by kernel version, if it has one, newer kernels first.
pairs.sort_by(|a, b| vercmp::compare_versions(&a.kernel, &b.kernel).reverse());
// Generate a unique name for the linux chainload action. // Generate a unique name for the linux chainload action.
let chainload_action_name = format!("{}{}", LINUX_CHAINLOAD_ACTION_PREFIX, root_unique_hash,); let chainload_action_name = format!("{}{}", LINUX_CHAINLOAD_ACTION_PREFIX, root_unique_hash,);
@@ -208,9 +224,10 @@ pub fn scan(
// Insert a default value for the linux-options if it doesn't exist. // Insert a default value for the linux-options if it doesn't exist.
if !config.values.contains_key("linux-options") { if !config.values.contains_key("linux-options") {
config config.values.insert(
.values "linux-options".to_string(),
.insert("linux-options".to_string(), "".to_string()); DEFAULT_LINUX_OPTIONS.to_string(),
);
} }
// Generate a chainload configuration for the list generator. // Generate a chainload configuration for the list generator.

View File

@@ -1,9 +1,9 @@
use crate::actions::ActionDeclaration;
use crate::actions::chainload::ChainloadConfiguration;
use crate::config::RootConfiguration;
use crate::entries::EntryDeclaration;
use crate::utils; use crate::utils;
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use edera_sprout_config::RootConfiguration;
use edera_sprout_config::actions::ActionDeclaration;
use edera_sprout_config::actions::chainload::ChainloadConfiguration;
use edera_sprout_config::entries::EntryDeclaration;
use uefi::CString16; use uefi::CString16;
use uefi::fs::{FileSystem, Path}; use uefi::fs::{FileSystem, Path};
use uefi::proto::device_path::DevicePath; use uefi::proto::device_path::DevicePath;

View File

@@ -0,0 +1,2 @@
/// The configuration loader mechanisms.
pub mod loader;

View File

@@ -1,8 +1,8 @@
use crate::config::{RootConfiguration, latest_version};
use crate::options::SproutOptions; use crate::options::SproutOptions;
use crate::platform::tpm::PlatformTpm; use crate::platform::tpm::PlatformTpm;
use crate::utils; use crate::utils;
use anyhow::{Context, Result, bail}; use anyhow::{Context, Result, bail};
use edera_sprout_config::{RootConfiguration, latest_version};
use log::info; use log::info;
use std::ops::Deref; use std::ops::Deref;
use toml::Value; use toml::Value;

View File

@@ -1,8 +1,8 @@
use crate::actions::ActionDeclaration;
use crate::options::SproutOptions; use crate::options::SproutOptions;
use crate::platform::timer::PlatformTimer; use crate::platform::timer::PlatformTimer;
use anyhow::anyhow; use anyhow::anyhow;
use anyhow::{Result, bail}; use anyhow::{Result, bail};
use edera_sprout_config::actions::ActionDeclaration;
use std::cmp::Reverse; use std::cmp::Reverse;
use std::collections::{BTreeMap, BTreeSet}; use std::collections::{BTreeMap, BTreeSet};
use std::rc::Rc; use std::rc::Rc;

View File

@@ -2,23 +2,12 @@ use crate::context::SproutContext;
use crate::integrations::shim::{ShimInput, ShimSupport}; use crate::integrations::shim::{ShimInput, ShimSupport};
use crate::utils; use crate::utils;
use anyhow::{Context, Result}; use anyhow::{Context, Result};
pub(crate) use edera_sprout_config::drivers::DriverDeclaration;
use log::info; use log::info;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap; use std::collections::BTreeMap;
use std::rc::Rc; use std::rc::Rc;
use uefi::boot::SearchType; use uefi::boot::SearchType;
/// Declares a driver configuration.
/// Drivers allow extending the functionality of Sprout.
/// Drivers are loaded at runtime and can provide extra functionality like filesystem support.
/// Drivers are loaded by their name, which is used to reference them in other concepts.
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
pub struct DriverDeclaration {
/// The filesystem path to the driver.
/// This file should be an EFI executable that can be located and executed.
pub path: String,
}
/// Loads the driver specified by the `driver` declaration. /// Loads the driver specified by the `driver` declaration.
fn load_driver(context: Rc<SproutContext>, driver: &DriverDeclaration) -> Result<()> { fn load_driver(context: Rc<SproutContext>, driver: &DriverDeclaration) -> Result<()> {
// Acquire the handle and device path of the loaded image. // Acquire the handle and device path of the loaded image.

View File

@@ -1,25 +1,7 @@
use crate::context::SproutContext; use crate::context::SproutContext;
use serde::{Deserialize, Serialize}; use edera_sprout_config::entries::EntryDeclaration;
use std::collections::BTreeMap;
use std::rc::Rc; use std::rc::Rc;
/// Declares a boot entry to display in the boot menu.
///
/// Entries are the user-facing concept of Sprout, making it possible
/// to run a set of actions with a specific context.
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
pub struct EntryDeclaration {
/// The title of the entry which will be display in the boot menu.
/// This is the pre-stamped value.
pub title: String,
/// The actions to run when the entry is selected.
#[serde(default)]
pub actions: Vec<String>,
/// The values to insert into the context when the entry is selected.
#[serde(default)]
pub values: BTreeMap<String, String>,
}
/// Represents an entry that is stamped and ready to be booted. /// Represents an entry that is stamped and ready to be booted.
#[derive(Clone)] #[derive(Clone)]
pub struct BootableEntry { pub struct BootableEntry {
@@ -94,6 +76,11 @@ impl BootableEntry {
self.default = true; self.default = true;
} }
// Unmark this entry as the default entry.
pub fn unmark_default(&mut self) {
self.default = false;
}
/// Mark this entry as being pinned, which prevents prefixing. /// Mark this entry as being pinned, which prevents prefixing.
pub fn mark_pin_name(&mut self) { pub fn mark_pin_name(&mut self) {
self.pin_name = true; self.pin_name = true;

View File

@@ -0,0 +1,18 @@
use crate::context::SproutContext;
use anyhow::{Result, bail};
use edera_sprout_config::extractors::ExtractorDeclaration;
use std::rc::Rc;
/// The filesystem device match extractor.
pub mod filesystem_device_match;
/// Extracts the value using the specified `extractor` under the provided `context`.
/// The extractor must return a value, and if a value cannot be determined, an error
/// should be returned.
pub fn extract(context: Rc<SproutContext>, extractor: &ExtractorDeclaration) -> Result<String> {
if let Some(filesystem) = &extractor.filesystem_device_match {
filesystem_device_match::extract(context, filesystem)
} else {
bail!("unknown extractor configuration");
}
}

View File

@@ -1,7 +1,7 @@
use crate::context::SproutContext; use crate::context::SproutContext;
use crate::utils; use crate::utils;
use anyhow::{Context, Result, anyhow, bail}; use anyhow::{Context, Result, anyhow, bail};
use serde::{Deserialize, Serialize}; use edera_sprout_config::extractors::filesystem_device_match::FilesystemDeviceMatchExtractor;
use std::ops::Deref; use std::ops::Deref;
use std::rc::Rc; use std::rc::Rc;
use std::str::FromStr; use std::str::FromStr;
@@ -11,34 +11,6 @@ use uefi::proto::media::file::{File, FileSystemVolumeLabel};
use uefi::proto::media::fs::SimpleFileSystem; use uefi::proto::media::fs::SimpleFileSystem;
use uefi::{CString16, Guid}; use uefi::{CString16, Guid};
/// The filesystem device match extractor.
/// This extractor finds a filesystem using some search criteria and returns
/// the device root path that can concatenated with subpaths to access files
/// on a particular filesystem.
/// The fallback value can be used to provide a value if no match is found.
///
/// This extractor requires all the criteria to match. If no criteria is provided,
/// an error is returned.
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
pub struct FilesystemDeviceMatchExtractor {
/// Matches a filesystem that has the specified label.
#[serde(default, rename = "has-label")]
pub has_label: Option<String>,
/// Matches a filesystem that has the specified item.
/// An item is either a directory or file.
#[serde(default, rename = "has-item")]
pub has_item: Option<String>,
/// Matches a filesystem that has the specified partition UUID.
#[serde(default, rename = "has-partition-uuid")]
pub has_partition_uuid: Option<String>,
/// Matches a filesystem that has the specified partition type UUID.
#[serde(default, rename = "has-partition-type-uuid")]
pub has_partition_type_uuid: Option<String>,
/// The fallback value to use if no filesystem matches the criteria.
#[serde(default)]
pub fallback: Option<String>,
}
/// Extract a filesystem device path using the specified `context` and `extractor` configuration. /// Extract a filesystem device path using the specified `context` and `extractor` configuration.
pub fn extract( pub fn extract(
context: Rc<SproutContext>, context: Rc<SproutContext>,
@@ -138,14 +110,11 @@ pub fn extract(
let mut filesystem = FileSystem::new(filesystem); let mut filesystem = FileSystem::new(filesystem);
// Check the metadata of the item. // Check the metadata of the item.
let metadata = filesystem.metadata(Path::new(&want_item));
// Ignore filesystem errors as we can't do anything useful with the error. // Ignore filesystem errors as we can't do anything useful with the error.
if metadata.is_err() { let Some(metadata) = filesystem.metadata(Path::new(&want_item)).ok() else {
continue; continue;
} };
let metadata = metadata?;
// Only check directories and files. // Only check directories and files.
if !(metadata.is_directory() || metadata.is_regular_file()) { if !(metadata.is_directory() || metadata.is_regular_file()) {
continue; continue;

View File

@@ -0,0 +1,33 @@
use crate::context::SproutContext;
use crate::entries::BootableEntry;
use anyhow::Result;
use anyhow::bail;
use edera_sprout_config::generators::GeneratorDeclaration;
use std::rc::Rc;
/// The BLS generator.
pub mod bls;
/// The list generator.
pub mod list;
/// The matrix generator.
pub mod matrix;
/// Runs the generator specified by the `generator` option.
/// It uses the specified `context` as the parent context for
/// the generated entries, injecting more values if needed.
pub fn generate(
context: Rc<SproutContext>,
generator: &GeneratorDeclaration,
) -> Result<Vec<BootableEntry>> {
if let Some(matrix) = &generator.matrix {
matrix::generate(context, matrix)
} else if let Some(bls) = &generator.bls {
bls::generate(context, bls)
} else if let Some(list) = &generator.list {
list::generate(context, list)
} else {
bail!("unknown generator configuration");
}
}

View File

@@ -1,9 +1,11 @@
use crate::context::SproutContext; use crate::context::SproutContext;
use crate::entries::{BootableEntry, EntryDeclaration}; use crate::entries::BootableEntry;
use crate::generators::bls::entry::BlsEntry; use crate::generators::bls::entry::BlsEntry;
use crate::utils; use crate::utils;
use crate::utils::vercmp;
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use serde::{Deserialize, Serialize}; use edera_sprout_config::generators::bls::BlsConfiguration;
use std::cmp::Ordering;
use std::rc::Rc; use std::rc::Rc;
use std::str::FromStr; use std::str::FromStr;
use uefi::cstr16; use uefi::cstr16;
@@ -14,25 +16,6 @@ use uefi::proto::media::fs::SimpleFileSystem;
/// BLS entry parser. /// BLS entry parser.
mod entry; mod entry;
/// The default path to the BLS directory.
const BLS_TEMPLATE_PATH: &str = "\\loader";
/// The configuration of the BLS generator.
/// The BLS uses the Bootloader Specification to produce
/// entries from an input template.
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
pub struct BlsConfiguration {
/// The entry to use for as a template.
pub entry: EntryDeclaration,
/// The path to the BLS directory.
#[serde(default = "default_bls_path")]
pub path: String,
}
fn default_bls_path() -> String {
BLS_TEMPLATE_PATH.to_string()
}
// TODO(azenla): remove this once variable substitution is implemented. // TODO(azenla): remove this once variable substitution is implemented.
/// This function is used to remove the `tuned_initrd` variable from entry values. /// This function is used to remove the `tuned_initrd` variable from entry values.
/// Fedora uses tuned which adds an initrd that shouldn't be used. /// Fedora uses tuned which adds an initrd that shouldn't be used.
@@ -40,6 +23,60 @@ fn quirk_initrd_remove_tuned(input: String) -> String {
input.replace("$tuned_initrd", "").trim().to_string() input.replace("$tuned_initrd", "").trim().to_string()
} }
/// Sorts two entries according to the BLS sort system.
/// Reference: https://uapi-group.org/specifications/specs/boot_loader_specification/#sorting
fn sort_entries(a: &(BlsEntry, BootableEntry), b: &(BlsEntry, BootableEntry)) -> Ordering {
// Grab the components of both entries.
let (a_bls, a_boot) = a;
let (b_bls, b_boot) = b;
// Grab the sort keys from both entries.
let a_sort_key = a_bls.sort_key();
let b_sort_key = b_bls.sort_key();
// Compare the sort keys of both entries.
match a_sort_key.cmp(&b_sort_key) {
// If A and B sort keys are equal, sort by machine-id.
Ordering::Equal => {
// Grab the machine-id from both entries.
let a_machine_id = a_bls.machine_id();
let b_machine_id = b_bls.machine_id();
// Compare the machine-id of both entries.
match a_machine_id.cmp(&b_machine_id) {
// If both machine-id values are equal, sort by version.
Ordering::Equal => {
// Grab the version from both entries.
let a_version = a_bls.version();
let b_version = b_bls.version();
// Compare the version of both entries, sorting newer versions first.
match vercmp::compare_versions_optional(
a_version.as_deref(),
b_version.as_deref(),
)
.reverse()
{
// If both versions are equal, sort by file name in reverse order.
Ordering::Equal => {
// Grab the file name from both entries.
let a_name = a_boot.name();
let b_name = b_boot.name();
// Compare the file names of both entries, sorting newer entries first.
vercmp::compare_versions(a_name, b_name).reverse()
}
other => other,
}
}
other => other,
}
}
other => other,
}
}
/// Generates entries from the BLS entries directory using the specified `bls` configuration and /// Generates entries from the BLS entries directory using the specified `bls` configuration and
/// `context`. The BLS conversion is best-effort and will ignore any unsupported entries. /// `context`. The BLS conversion is best-effort and will ignore any unsupported entries.
pub fn generate(context: Rc<SproutContext>, bls: &BlsConfiguration) -> Result<Vec<BootableEntry>> { pub fn generate(context: Rc<SproutContext>, bls: &BlsConfiguration) -> Result<Vec<BootableEntry>> {
@@ -93,6 +130,11 @@ pub fn generate(context: Rc<SproutContext>, bls: &BlsConfiguration) -> Result<Ve
// Remove the .conf extension. // Remove the .conf extension.
name.truncate(name.len() - 5); name.truncate(name.len() - 5);
// Skip over files that are named just ".conf" as they are not valid entry files.
if name.is_empty() {
continue;
}
// Create a mutable path so we can append the file name to produce the full path. // Create a mutable path so we can append the file name to produce the full path.
let mut full_entry_path = entries_path.to_path_buf(); let mut full_entry_path = entries_path.to_path_buf();
full_entry_path.push(entry.file_name()); full_entry_path.push(entry.file_name());
@@ -116,20 +158,33 @@ pub fn generate(context: Rc<SproutContext>, bls: &BlsConfiguration) -> Result<Ve
// Produce a new sprout context for the entry with the extracted values. // Produce a new sprout context for the entry with the extracted values.
let mut context = context.fork(); let mut context = context.fork();
let title = entry.title().unwrap_or_else(|| name.clone()); let title_base = entry.title().unwrap_or_else(|| name.clone());
let chainload = entry.chainload_path().unwrap_or_default(); let chainload = entry.chainload_path().unwrap_or_default();
let options = entry.options().unwrap_or_default(); let options = entry.options().unwrap_or_default();
let version = entry.version().unwrap_or_default();
let machine_id = entry.machine_id().unwrap_or_default();
// Put the initrd through a quirk modifier to support Fedora. // Put the initrd through a quirk modifier to support Fedora.
let initrd = quirk_initrd_remove_tuned(entry.initrd_path().unwrap_or_default()); let initrd = quirk_initrd_remove_tuned(entry.initrd_path().unwrap_or_default());
context.set("title", title); // Combine the title with the version if a version is present, except if it already contains it.
// Sometimes BLS will have a version in the title already, and this makes it unique.
let title_full = if !version.is_empty() && !title_base.contains(&version) {
format!("{} {}", title_base, version)
} else {
title_base.clone()
};
context.set("title-base", title_base);
context.set("title", title_full);
context.set("chainload", chainload); context.set("chainload", chainload);
context.set("options", options); context.set("options", options);
context.set("initrd", initrd); context.set("initrd", initrd);
context.set("version", version);
context.set("machine-id", machine_id);
// Produce a new bootable entry. // Produce a new bootable entry.
let mut entry = BootableEntry::new( let mut boot = BootableEntry::new(
name, name,
bls.entry.title.clone(), bls.entry.title.clone(),
context.freeze(), context.freeze(),
@@ -139,11 +194,15 @@ pub fn generate(context: Rc<SproutContext>, bls: &BlsConfiguration) -> Result<Ve
// Pin the entry name to prevent prefixing. // Pin the entry name to prevent prefixing.
// This is needed as the bootloader interface requires the name to be // This is needed as the bootloader interface requires the name to be
// the same as the entry file name, minus the .conf extension. // the same as the entry file name, minus the .conf extension.
entry.mark_pin_name(); boot.mark_pin_name();
// Add the entry to the list with a frozen context. // Add the BLS entry to the list, along with the bootable entry.
entries.push(entry); entries.push((entry, boot));
} }
Ok(entries) // Sort all the entries according to the BLS sort system.
entries.sort_by(sort_entries);
// Collect all the bootable entries and return them.
Ok(entries.into_iter().map(|(_, boot)| boot).collect())
} }

View File

@@ -15,6 +15,12 @@ pub struct BlsEntry {
pub initrd: Option<String>, pub initrd: Option<String>,
/// The path to an EFI image. /// The path to an EFI image.
pub efi: Option<String>, pub efi: Option<String>,
/// The sort key for the entry.
pub sort_key: Option<String>,
/// The version of the entry.
pub version: Option<String>,
/// The machine id of the entry.
pub machine_id: Option<String>,
} }
/// Parser for a BLS entry. /// Parser for a BLS entry.
@@ -30,6 +36,9 @@ impl FromStr for BlsEntry {
let mut linux: Option<String> = None; let mut linux: Option<String> = None;
let mut initrd: Option<String> = None; let mut initrd: Option<String> = None;
let mut efi: Option<String> = None; let mut efi: Option<String> = None;
let mut sort_key: Option<String> = None;
let mut version: Option<String> = None;
let mut machine_id: Option<String> = None;
// Iterate over each line in the input and parse it. // Iterate over each line in the input and parse it.
for line in input.lines() { for line in input.lines() {
@@ -74,6 +83,18 @@ impl FromStr for BlsEntry {
efi = Some(value.trim().to_string()); efi = Some(value.trim().to_string());
} }
"sort-key" => {
sort_key = Some(value.trim().to_string());
}
"version" => {
version = Some(value.trim().to_string());
}
"machine-id" => {
machine_id = Some(value.trim().to_string());
}
// Ignore any other key. // Ignore any other key.
_ => { _ => {
continue; continue;
@@ -88,6 +109,9 @@ impl FromStr for BlsEntry {
linux, linux,
initrd, initrd,
efi, efi,
sort_key,
version,
machine_id,
}) })
} }
} }
@@ -125,4 +149,19 @@ impl BlsEntry {
pub fn title(&self) -> Option<String> { pub fn title(&self) -> Option<String> {
self.title.clone() self.title.clone()
} }
/// Fetches the sort key of the entry, if any.
pub fn sort_key(&self) -> Option<String> {
self.sort_key.clone()
}
/// Fetches the version of the entry, if any.
pub fn version(&self) -> Option<String> {
self.version.clone()
}
/// Fetches the machine id of the entry, if any.
pub fn machine_id(&self) -> Option<String> {
self.machine_id.clone()
}
} }

View File

@@ -1,23 +1,9 @@
use crate::context::SproutContext; use crate::context::SproutContext;
use crate::entries::{BootableEntry, EntryDeclaration}; use crate::entries::BootableEntry;
use anyhow::Result; use anyhow::Result;
use serde::{Deserialize, Serialize}; use edera_sprout_config::generators::list::ListConfiguration;
use std::collections::BTreeMap;
use std::rc::Rc; use std::rc::Rc;
/// List generator configuration.
/// The list generator produces multiple entries based
/// on a set of input maps.
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
pub struct ListConfiguration {
/// The template entry to use for each generated entry.
#[serde(default)]
pub entry: EntryDeclaration,
/// The values to use as the input for the matrix.
#[serde(default)]
pub values: Vec<BTreeMap<String, String>>,
}
/// Generates a set of entries using the specified `list` configuration in the `context`. /// Generates a set of entries using the specified `list` configuration in the `context`.
pub fn generate( pub fn generate(
context: Rc<SproutContext>, context: Rc<SproutContext>,

View File

@@ -1,24 +1,12 @@
use crate::context::SproutContext; use crate::context::SproutContext;
use crate::entries::{BootableEntry, EntryDeclaration}; use crate::entries::BootableEntry;
use crate::generators::list; use crate::generators::list;
use anyhow::Result; use anyhow::Result;
use serde::{Deserialize, Serialize}; use edera_sprout_config::generators::list::ListConfiguration;
use edera_sprout_config::generators::matrix::MatrixConfiguration;
use std::collections::BTreeMap; use std::collections::BTreeMap;
use std::rc::Rc; use std::rc::Rc;
/// Matrix generator configuration.
/// The matrix generator produces multiple entries based
/// on input values multiplicatively.
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
pub struct MatrixConfiguration {
/// The template entry to use for each generated entry.
#[serde(default)]
pub entry: EntryDeclaration,
/// The values to use as the input for the matrix.
#[serde(default)]
pub values: BTreeMap<String, Vec<String>>,
}
/// Builds out multiple generations of `input` based on a matrix style. /// Builds out multiple generations of `input` based on a matrix style.
/// For example, if input is: {"x": ["a", "b"], "y": ["c", "d"]} /// For example, if input is: {"x": ["a", "b"], "y": ["c", "d"]}
/// It will produce: /// It will produce:
@@ -61,7 +49,7 @@ pub fn generate(
// Use the list generator to generate entries for each combination. // Use the list generator to generate entries for each combination.
list::generate( list::generate(
context, context,
&list::ListConfiguration { &ListConfiguration {
entry: matrix.entry.clone(), entry: matrix.entry.clone(),
values: combinations, values: combinations,
}, },

View File

@@ -0,0 +1,314 @@
use crate::integrations::bootloader_interface::bitflags::LoaderFeatures;
use crate::platform::timer::PlatformTimer;
use crate::utils::device_path_subpath;
use crate::utils::variables::{VariableClass, VariableController};
use anyhow::{Context, Result};
use uefi::proto::device_path::DevicePath;
use uefi::{Guid, guid};
use uefi_raw::table::runtime::VariableVendor;
/// bitflags: LoaderFeatures bitflags.
mod bitflags;
/// The name of the bootloader to tell the system.
const LOADER_NAME: &str = "Sprout";
/// Represents the configured timeout for the bootloader interface.
pub enum BootloaderInterfaceTimeout {
/// Force the menu to be shown.
MenuForce,
/// Hide the menu.
MenuHidden,
/// Disable the menu.
MenuDisabled,
/// Set a timeout for the menu.
Timeout(u64),
/// Timeout is unspecified.
Unspecified,
}
/// Bootloader Interface support.
pub struct BootloaderInterface;
impl BootloaderInterface {
/// Bootloader Interface GUID from https://systemd.io/BOOT_LOADER_INTERFACE
const VENDOR: VariableController = VariableController::new(VariableVendor(guid!(
"4a67b082-0a4c-41cf-b6c7-440b29bb8c4f"
)));
/// The feature we support in Sprout.
fn features() -> LoaderFeatures {
LoaderFeatures::Xbootldr
| LoaderFeatures::LoadDriver
| LoaderFeatures::Tpm2ActivePcrBanks
| LoaderFeatures::RetainShim
| LoaderFeatures::ConfigTimeout
| LoaderFeatures::ConfigTimeoutOneShot
| LoaderFeatures::MenuDisable
| LoaderFeatures::EntryDefault
| LoaderFeatures::EntryOneShot
}
/// Tell the system that Sprout was initialized at the current time.
pub fn mark_init(timer: &PlatformTimer) -> Result<()> {
Self::mark_time("LoaderTimeInitUSec", timer)
}
/// Tell the system that Sprout is about to execute the boot entry.
pub fn mark_exec(timer: &PlatformTimer) -> Result<()> {
Self::mark_time("LoaderTimeExecUSec", timer)
}
/// Tell the system that Sprout is about to display the menu.
pub fn mark_menu(timer: &PlatformTimer) -> Result<()> {
Self::mark_time("LoaderTimeMenuUSec", timer)
}
/// Tell the system about the current time as measured by the platform timer.
/// Sets the variable specified by `key` to the number of microseconds.
fn mark_time(key: &str, timer: &PlatformTimer) -> Result<()> {
// Measure the elapsed time since the hardware timer was started.
let elapsed = timer.elapsed_since_lifetime();
Self::VENDOR.set_cstr16(
key,
&elapsed.as_micros().to_string(),
VariableClass::BootAndRuntimeTemporary,
)
}
/// Tell the system what loader is being used and our features.
pub fn set_loader_info() -> Result<()> {
// Set the LoaderInfo variable with the name of the loader.
Self::VENDOR
.set_cstr16(
"LoaderInfo",
LOADER_NAME,
VariableClass::BootAndRuntimeTemporary,
)
.context("unable to set loader info variable")?;
// Set the LoaderFeatures variable with the features we support.
Self::VENDOR
.set_u64le(
"LoaderFeatures",
Self::features().bits(),
VariableClass::BootAndRuntimeTemporary,
)
.context("unable to set loader features variable")?;
Ok(())
}
/// Tell the system the relative path to the partition root of the current bootloader.
pub fn set_loader_path(path: &DevicePath) -> Result<()> {
let subpath = device_path_subpath(path).context("unable to get loader path subpath")?;
Self::VENDOR.set_cstr16(
"LoaderImageIdentifier",
&subpath,
VariableClass::BootAndRuntimeTemporary,
)
}
/// Tell the system what the partition GUID of the ESP Sprout was booted from is.
pub fn set_partition_guid(guid: &Guid) -> Result<()> {
Self::VENDOR.set_cstr16(
"LoaderDevicePartUUID",
&guid.to_string(),
VariableClass::BootAndRuntimeTemporary,
)
}
/// Tell the system what boot entries are available.
pub fn set_entries<N: AsRef<str>>(entries: impl Iterator<Item = N>) -> Result<()> {
// Entries are stored as a null-terminated list of CString16 strings back to back.
// Iterate over the entries and convert them to CString16 placing them into data.
let mut data = Vec::new();
for entry in entries {
// Convert the entry to CString16 little endian.
let encoded = entry
.as_ref()
.encode_utf16()
.flat_map(|c| c.to_le_bytes())
.collect::<Vec<u8>>();
// Write the bytes into the data buffer.
data.extend_from_slice(&encoded);
// Add a null terminator to the end of the entry.
data.extend_from_slice(&[0, 0]);
}
// If no data was generated, we will do nothing.
if data.is_empty() {
return Ok(());
}
Self::VENDOR.set(
"LoaderEntries",
&data,
VariableClass::BootAndRuntimeTemporary,
)
}
/// Tell the system what the selected boot entry is.
pub fn set_selected_entry(entry: String) -> Result<()> {
Self::VENDOR.set_cstr16(
"LoaderEntrySelected",
&entry,
VariableClass::BootAndRuntimeTemporary,
)
}
/// Tell the system about the UEFI firmware we are running on.
pub fn set_firmware_info() -> Result<()> {
// Access the firmware revision.
let firmware_revision = uefi::system::firmware_revision();
// Access the UEFI revision.
let uefi_revision = uefi::system::uefi_revision();
// Format the firmware information string into something human-readable.
let firmware_info = format!(
"{} {}.{:02}",
uefi::system::firmware_vendor(),
firmware_revision >> 16,
firmware_revision & 0xffff,
);
Self::VENDOR.set_cstr16(
"LoaderFirmwareInfo",
&firmware_info,
VariableClass::BootAndRuntimeTemporary,
)?;
// Format the firmware revision into something human-readable.
let firmware_type = format!(
"UEFI {}.{:02}",
uefi_revision.major(),
uefi_revision.minor()
);
Self::VENDOR.set_cstr16(
"LoaderFirmwareType",
&firmware_type,
VariableClass::BootAndRuntimeTemporary,
)
}
/// Tell the system what the number of active PCR banks is.
/// If this is zero, that is okay.
pub fn set_tpm2_active_pcr_banks(value: u32) -> Result<()> {
// Format the value into the specification format.
let value = format!("0x{:08x}", value);
Self::VENDOR.set_cstr16(
"LoaderTpm2ActivePcrBanks",
&value,
VariableClass::BootAndRuntimeTemporary,
)
}
/// Retrieve the timeout value from the bootloader interface, using the specified `key`.
/// `remove` indicates whether, when found, we remove the variable.
fn get_timeout_value(key: &str, remove: bool) -> Result<Option<BootloaderInterfaceTimeout>> {
// Retrieve the timeout value from the bootloader interface.
let Some(value) = Self::VENDOR
.get_cstr16(key)
.context("unable to get timeout value")?
else {
return Ok(None);
};
// If we reach here, we know the value was specified.
// If `remove` is true, remove the variable.
if remove {
Self::VENDOR
.remove(key)
.context("unable to remove timeout variable")?;
}
// If the value is empty, return Unspecified.
if value.is_empty() {
return Ok(Some(BootloaderInterfaceTimeout::Unspecified));
}
// If the value is "menu-force", return MenuForce.
if value == "menu-force" {
return Ok(Some(BootloaderInterfaceTimeout::MenuForce));
}
// If the value is "menu-hidden", return MenuHidden.
if value == "menu-hidden" {
return Ok(Some(BootloaderInterfaceTimeout::MenuHidden));
}
// If the value is "menu-disabled", return MenuDisabled.
if value == "menu-disabled" {
return Ok(Some(BootloaderInterfaceTimeout::MenuDisabled));
}
// Parse the value as a u64 to decode an numeric value.
let value = value
.parse::<u64>()
.context("unable to parse timeout value")?;
// The specification says that a value of 0 means that the menu should be hidden.
if value == 0 {
return Ok(Some(BootloaderInterfaceTimeout::MenuHidden));
}
// If we reach here, we know it must be a real timeout value.
Ok(Some(BootloaderInterfaceTimeout::Timeout(value)))
}
/// Get the timeout from the bootloader interface.
/// This indicates how the menu should behave.
/// If no values are set, Unspecified is returned.
pub fn get_timeout() -> Result<BootloaderInterfaceTimeout> {
// Attempt to acquire the value of the LoaderConfigTimeoutOneShot variable.
// This should take precedence over the LoaderConfigTimeout variable.
let oneshot = Self::get_timeout_value("LoaderConfigTimeoutOneShot", true)
.context("unable to check for LoaderConfigTimeoutOneShot variable")?;
// If oneshot was found, return it.
if let Some(oneshot) = oneshot {
return Ok(oneshot);
}
// Attempt to acquire the value of the LoaderConfigTimeout variable.
// This will be used if the LoaderConfigTimeoutOneShot variable is not set.
let direct = Self::get_timeout_value("LoaderConfigTimeout", false)
.context("unable to check for LoaderConfigTimeout variable")?;
// If direct was found, return it.
if let Some(direct) = direct {
return Ok(direct);
}
// If we reach here, we know that neither variable was set.
// We provide the unspecified value instead.
Ok(BootloaderInterfaceTimeout::Unspecified)
}
/// Get the default entry set by the bootloader interface.
pub fn get_default_entry() -> Result<Option<String>> {
Self::VENDOR
.get_cstr16("LoaderEntryDefault")
.context("unable to get default entry from bootloader interface")
}
/// Get the oneshot entry set by the bootloader interface.
/// This should be the entry we boot.
pub fn get_oneshot_entry() -> Result<Option<String>> {
// Acquire the value of the LoaderEntryOneShot variable.
// If it is not set, return None.
let Some(value) = Self::VENDOR
.get_cstr16("LoaderEntryOneShot")
.context("unable to get oneshot entry from bootloader interface")?
else {
return Ok(None);
};
// Remove the oneshot entry from the bootloader interface.
Self::VENDOR
.remove("LoaderEntryOneShot")
.context("unable to remove oneshot entry")?;
// Return the oneshot value.
Ok(Some(value))
}
}

View File

@@ -0,0 +1,46 @@
use bitflags::bitflags;
bitflags! {
/// Feature bitflags for the bootloader interface.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct LoaderFeatures: u64 {
/// Bootloader supports LoaderConfigTimeout.
const ConfigTimeout = 1 << 0;
/// Bootloader supports LoaderConfigTimeoutOneShot.
const ConfigTimeoutOneShot = 1 << 1;
/// Bootloader supports LoaderEntryDefault.
const EntryDefault = 1 << 2;
/// Bootloader supports LoaderEntryOneShot.
const EntryOneShot = 1 << 3;
/// Bootloader supports boot counting.
const BootCounting = 1 << 4;
/// Bootloader supports detection from XBOOTLDR partitions.
const Xbootldr = 1 << 5;
/// Bootloader supports the handling of random seeds.
const RandomSeed = 1 << 6;
/// Bootloader supports loading drivers.
const LoadDriver = 1 << 7;
/// Bootloader supports sort keys.
const SortKey = 1 << 8;
/// Bootloader supports saved entries.
const SavedEntry = 1 << 9;
/// Bootloader supports device trees.
const DeviceTree = 1 << 10;
/// Bootloader supports secure boot enroll.
const SecureBootEnroll = 1 << 11;
/// Bootloader retains the shim.
const RetainShim = 1 << 12;
/// Bootloader supports disabling the menu via the menu timeout variable.
const MenuDisable = 1 << 13;
/// Bootloader supports multi-profile UKI.
const MultiProfileUki = 1 << 14;
/// Bootloader reports URLs.
const ReportUrl = 1 << 15;
/// Bootloader supports type-1 UKIs.
const Type1Uki = 1 << 16;
/// Bootloader supports type-1 UKI urls.
const Type1UkiUrl = 1 << 17;
/// Bootloader indicates TPM2 active PCR banks.
const Tpm2ActivePcrBanks = 1 << 18;
}
}

View File

@@ -1,10 +1,12 @@
use crate::integrations::shim::hook::SecurityHook; use crate::integrations::shim::hook::SecurityHook;
use crate::secure::SecureBoot;
use crate::utils; use crate::utils;
use crate::utils::ResolvedPath; use crate::utils::ResolvedPath;
use crate::utils::variables::{VariableClass, VariableController}; use crate::utils::variables::{VariableClass, VariableController};
use anyhow::{Context, Result, anyhow, bail}; use anyhow::{Context, Result, anyhow, bail};
use log::warn; use log::warn;
use std::ffi::c_void; use std::ffi::c_void;
use std::pin::Pin;
use uefi::Handle; use uefi::Handle;
use uefi::boot::LoadImageSource; use uefi::boot::LoadImageSource;
use uefi::proto::device_path::text::{AllowShortcuts, DisplayOnly}; use uefi::proto::device_path::text::{AllowShortcuts, DisplayOnly};
@@ -22,13 +24,13 @@ pub struct ShimSupport;
/// Input to the shim mechanisms. /// Input to the shim mechanisms.
pub enum ShimInput<'a> { pub enum ShimInput<'a> {
/// Data loaded into a buffer and ready to be verified, owned. /// Data loaded into a buffer and ready to be verified, owned.
OwnedDataBuffer(Option<&'a ResolvedPath>, Vec<u8>), OwnedDataBuffer(Option<&'a ResolvedPath>, Pin<Box<[u8]>>),
/// Data loaded into a buffer and ready to be verified. /// Data loaded into a buffer and ready to be verified.
DataBuffer(Option<&'a ResolvedPath>, &'a [u8]), DataBuffer(Option<&'a ResolvedPath>, &'a [u8]),
/// Low-level data buffer provided by the security hook. /// Low-level data buffer provided by the security hook.
SecurityHookBuffer(Option<*const FfiDevicePath>, &'a [u8]), SecurityHookBuffer(Option<*const FfiDevicePath>, &'a [u8]),
/// Low-level owned data buffer provided by the security hook. /// Low-level owned data buffer provided by the security hook.
SecurityHookOwnedBuffer(Option<*const FfiDevicePath>, Vec<u8>), SecurityHookOwnedBuffer(Option<*const FfiDevicePath>, Pin<Box<[u8]>>),
/// Low-level path provided by the security hook. /// Low-level path provided by the security hook.
SecurityHookPath(*const FfiDevicePath), SecurityHookPath(*const FfiDevicePath),
/// Data is provided as a resolved path. We will need to load the data to verify it. /// Data is provided as a resolved path. We will need to load the data to verify it.
@@ -71,9 +73,10 @@ impl<'a> ShimInput<'a> {
match self { match self {
ShimInput::OwnedDataBuffer(root, data) => Ok(ShimInput::OwnedDataBuffer(root, data)), ShimInput::OwnedDataBuffer(root, data) => Ok(ShimInput::OwnedDataBuffer(root, data)),
ShimInput::DataBuffer(root, data) => { ShimInput::DataBuffer(root, data) => Ok(ShimInput::OwnedDataBuffer(
Ok(ShimInput::OwnedDataBuffer(root, data.to_vec())) root,
} Box::into_pin(data.to_vec().into_boxed_slice()),
)),
ShimInput::SecurityHookPath(ffi_path) => { ShimInput::SecurityHookPath(ffi_path) => {
// Acquire the file path. // Acquire the file path.
@@ -88,7 +91,10 @@ impl<'a> ShimInput<'a> {
.context("unable to resolve path")?; .context("unable to resolve path")?;
// Read the file path. // Read the file path.
let data = path.read_file()?; let data = path.read_file()?;
Ok(ShimInput::SecurityHookOwnedBuffer(Some(ffi_path), data)) Ok(ShimInput::SecurityHookOwnedBuffer(
Some(ffi_path),
Box::into_pin(data.to_vec().into_boxed_slice()),
))
} }
ShimInput::SecurityHookBuffer(_, _) => { ShimInput::SecurityHookBuffer(_, _) => {
@@ -96,7 +102,12 @@ impl<'a> ShimInput<'a> {
} }
ShimInput::ResolvedPath(path) => { ShimInput::ResolvedPath(path) => {
Ok(ShimInput::OwnedDataBuffer(Some(path), path.read_file()?)) // Read the file path.
let data = path.read_file()?;
Ok(ShimInput::OwnedDataBuffer(
Some(path),
Box::into_pin(data.to_vec().into_boxed_slice()),
))
} }
ShimInput::SecurityHookOwnedBuffer(path, data) => { ShimInput::SecurityHookOwnedBuffer(path, data) => {
@@ -111,7 +122,7 @@ impl<'a> ShimInput<'a> {
/// to actually boot. /// to actually boot.
pub enum ShimVerificationOutput { pub enum ShimVerificationOutput {
/// The verification failed. /// The verification failed.
VerificationFailed, VerificationFailed(Status),
/// The data provided to the verifier was already a buffer. /// The data provided to the verifier was already a buffer.
VerifiedDataNotLoaded, VerifiedDataNotLoaded,
/// Verifying the data resulted in loading the data from the source. /// Verifying the data resulted in loading the data from the source.
@@ -123,7 +134,14 @@ pub enum ShimVerificationOutput {
#[unsafe_protocol(ShimSupport::SHIM_LOCK_GUID)] #[unsafe_protocol(ShimSupport::SHIM_LOCK_GUID)]
struct ShimLockProtocol { struct ShimLockProtocol {
/// Verify the data in `buffer` with the size `buffer_size` to determine if it is valid. /// Verify the data in `buffer` with the size `buffer_size` to determine if it is valid.
pub shim_verify: unsafe extern "efiapi" fn(buffer: *mut c_void, buffer_size: u32) -> Status, /// NOTE: On x86_64, this function uses SYSV calling conventions. On aarch64 it uses the
/// efiapi calling convention. This is truly wild, but you can verify it yourself by
/// looking at: https://github.com/rhboot/shim/blob/15.8/shim.h#L207-L212
/// There is no calling convention declared like there should be.
#[cfg(target_arch = "x86_64")]
pub shim_verify: unsafe extern "sysv64" fn(buffer: *const c_void, buffer_size: u32) -> Status,
#[cfg(target_arch = "aarch64")]
pub shim_verify: unsafe extern "efiapi" fn(buffer: *const c_void, buffer_size: u32) -> Status,
/// Unused function that is defined by the shim. /// Unused function that is defined by the shim.
_generate_header: *mut c_void, _generate_header: *mut c_void,
/// Unused function that is defined by the shim. /// Unused function that is defined by the shim.
@@ -201,12 +219,13 @@ impl ShimSupport {
// SAFETY: The shim verify function is specified by the shim lock protocol. // SAFETY: The shim verify function is specified by the shim lock protocol.
// Calling this function is considered safe because the shim verify function is // Calling this function is considered safe because the shim verify function is
// guaranteed to be defined by the environment if we are able to acquire the protocol. // guaranteed to be defined by the environment if we are able to acquire the protocol.
let status = let status = unsafe {
unsafe { (protocol.shim_verify)(buffer.as_ptr() as *mut c_void, buffer.len() as u32) }; (protocol.shim_verify)(buffer.as_ptr() as *const c_void, buffer.len() as u32)
};
// If the verification failed, return the verification failure output. // If the verification failed, return the verification failure output.
if !status.is_success() { if !status.is_success() {
return Ok(ShimVerificationOutput::VerificationFailed); return Ok(ShimVerificationOutput::VerificationFailed(status));
} }
// If verification succeeded, return the validation output, // If verification succeeded, return the validation output,
@@ -218,6 +237,10 @@ impl ShimSupport {
/// Load the image specified by the `input` and returns an image handle. /// Load the image specified by the `input` and returns an image handle.
pub fn load(current_image: Handle, input: ShimInput) -> Result<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. // Determine whether the shim is loaded.
let shim_loaded = Self::loaded().context("unable to determine if shim is loaded")?; let shim_loaded = Self::loaded().context("unable to determine if shim is loaded")?;
@@ -228,7 +251,7 @@ impl ShimSupport {
// Determines whether LoadImage in Boot Services must be patched. // Determines whether LoadImage in Boot Services must be patched.
// Version 16 of the shim doesn't require extra effort to load Secure Boot binaries. // 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. // If the image loader is installed, we can skip over the security hook.
let requires_security_hook = shim_loaded && !shim_loader_available; let requires_security_hook = secure_boot && shim_loaded && !shim_loader_available;
// If the security hook is required, we will bail for now. // If the security hook is required, we will bail for now.
if requires_security_hook { if requires_security_hook {

View File

@@ -20,7 +20,7 @@ pub struct SecurityArchProtocol {
pub file_authentication_state: unsafe extern "efiapi" fn( pub file_authentication_state: unsafe extern "efiapi" fn(
this: *const SecurityArchProtocol, this: *const SecurityArchProtocol,
status: u32, status: u32,
path: *mut FfiDevicePath, path: *const FfiDevicePath,
) -> Status, ) -> Status,
} }
@@ -30,8 +30,8 @@ pub struct SecurityArch2Protocol {
/// Determines the file authentication. /// Determines the file authentication.
pub file_authentication: unsafe extern "efiapi" fn( pub file_authentication: unsafe extern "efiapi" fn(
this: *const SecurityArch2Protocol, this: *const SecurityArch2Protocol,
path: *mut FfiDevicePath, path: *const FfiDevicePath,
file_buffer: *mut u8, file_buffer: *const u8,
file_size: usize, file_size: usize,
boot_policy: bool, boot_policy: bool,
) -> Status, ) -> Status,
@@ -53,12 +53,13 @@ pub struct SecurityHook;
impl SecurityHook { impl SecurityHook {
/// Shared verifier logic for both hook types. /// Shared verifier logic for both hook types.
fn verify(input: ShimInput) -> Status { #[must_use]
// Verify the input. fn verify(input: ShimInput) -> bool {
match ShimSupport::verify(input) { // Verify the input and convert the result to a status.
let status = match ShimSupport::verify(input) {
Ok(output) => match output { Ok(output) => match output {
// If the verification failed, return the access-denied status. // If the verification failed, return the access-denied status.
ShimVerificationOutput::VerificationFailed => Status::ACCESS_DENIED, ShimVerificationOutput::VerificationFailed(status) => status,
// If the verification succeeded, return the success status. // If the verification succeeded, return the success status.
ShimVerificationOutput::VerifiedDataNotLoaded => Status::SUCCESS, ShimVerificationOutput::VerifiedDataNotLoaded => Status::SUCCESS,
ShimVerificationOutput::VerifiedDataBuffer(_) => Status::SUCCESS, ShimVerificationOutput::VerifiedDataBuffer(_) => Status::SUCCESS,
@@ -70,15 +71,23 @@ impl SecurityHook {
warn!("unable to verify image: {}", error); warn!("unable to verify image: {}", error);
Status::ACCESS_DENIED Status::ACCESS_DENIED
} }
};
// If the status is not a success, log the status.
if !status.is_success() {
warn!("shim verification failed: {}", status);
} }
// Return whether the status is a success.
// If it's not a success, the original hook should be called.
status.is_success()
} }
/// File authentication state verifier for the EFI_SECURITY_ARCH protocol. /// File authentication state verifier for the EFI_SECURITY_ARCH protocol.
/// Takes the `path` and determines the verification. /// Takes the `path` and determines the verification.
unsafe extern "efiapi" fn arch_file_authentication_state( unsafe extern "efiapi" fn arch_file_authentication_state(
_this: *const SecurityArchProtocol, this: *const SecurityArchProtocol,
_status: u32, status: u32,
path: *mut FfiDevicePath, path: *const FfiDevicePath,
) -> Status { ) -> Status {
// Verify the path is not null. // Verify the path is not null.
if path.is_null() { if path.is_null() {
@@ -88,16 +97,56 @@ impl SecurityHook {
// Construct a shim input from the path. // Construct a shim input from the path.
let input = ShimInput::SecurityHookPath(path); let input = ShimInput::SecurityHookPath(path);
// Verify the input. // Convert the input to an owned data buffer.
Self::verify(input) let input = match input.into_owned_data_buffer() {
Ok(input) => input,
// If an error occurs, log the error and return the not found status.
Err(error) => {
warn!("unable to read data to be authenticated: {}", error);
return Status::NOT_FOUND;
}
};
// 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() {
// The hook state is available, so we can acquire the original hook.
Some(state) => state.original_hook.file_authentication_state,
// The hook state is not available, so we can't call the original hook.
None => {
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.
// SAFETY: This function is safe to call as it is stored by us and is required
// in the UEFI specification.
unsafe { function(this, status, path) }
} else {
Status::SUCCESS
}
} }
/// File authentication verifier for the EFI_SECURITY_ARCH2 protocol. /// File authentication verifier for the EFI_SECURITY_ARCH2 protocol.
/// Takes the `path` and a file buffer to determine the verification. /// Takes the `path` and a file buffer to determine the verification.
unsafe extern "efiapi" fn arch2_file_authentication( unsafe extern "efiapi" fn arch2_file_authentication(
_this: *const SecurityArch2Protocol, this: *const SecurityArch2Protocol,
path: *mut FfiDevicePath, path: *const FfiDevicePath,
file_buffer: *mut u8, file_buffer: *const u8,
file_size: usize, file_size: usize,
boot_policy: bool, boot_policy: bool,
) -> Status { ) -> Status {
@@ -117,8 +166,38 @@ impl SecurityHook {
// Construct a shim input from the path. // Construct a shim input from the path.
let input = ShimInput::SecurityHookBuffer(Some(path), buffer); let input = ShimInput::SecurityHookBuffer(Some(path), buffer);
// Verify the input. // Verify the input, if it fails, call the original hook.
Self::verify(input) 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() {
// The hook state is available, so we can acquire the original hook.
Some(state) => state.original_hook2.file_authentication,
// The hook state is not available, so we can't call the original hook.
None => {
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.
// SAFETY: This function is safe to call as it is stored by us and is required
// in the UEFI specification.
unsafe { function(this, path, file_buffer, file_size, boot_policy) }
} else {
Status::SUCCESS
}
} }
/// Install the security hook if needed. /// Install the security hook if needed.

View File

@@ -1,14 +1,12 @@
#![doc = include_str!("../README.md")] #![doc = include_str!("../README.md")]
#![feature(uefi_std)] #![feature(uefi_std)]
extern crate core;
/// The delay to wait for when an error occurs in Sprout. /// The delay to wait for when an error occurs in Sprout.
const DELAY_ON_ERROR: Duration = Duration::from_secs(10); const DELAY_ON_ERROR: Duration = Duration::from_secs(10);
use crate::config::RootConfiguration;
use crate::context::{RootContext, SproutContext}; use crate::context::{RootContext, SproutContext};
use crate::entries::BootableEntry; use crate::entries::BootableEntry;
use crate::integrations::bootloader_interface::BootloaderInterface; use crate::integrations::bootloader_interface::{BootloaderInterface, BootloaderInterfaceTimeout};
use crate::options::SproutOptions; use crate::options::SproutOptions;
use crate::options::parser::OptionsRepresentable; use crate::options::parser::OptionsRepresentable;
use crate::phases::phase; use crate::phases::phase;
@@ -17,6 +15,7 @@ use crate::platform::tpm::PlatformTpm;
use crate::secure::SecureBoot; use crate::secure::SecureBoot;
use crate::utils::PartitionGuidForm; use crate::utils::PartitionGuidForm;
use anyhow::{Context, Result, bail}; use anyhow::{Context, Result, bail};
use edera_sprout_config::RootConfiguration;
use log::{error, info, warn}; use log::{error, info, warn};
use std::collections::BTreeMap; use std::collections::BTreeMap;
use std::ops::Deref; use std::ops::Deref;
@@ -263,24 +262,6 @@ fn run() -> Result<()> {
} }
} }
// If no entries were the default, pick the first entry as the default entry.
if entries.iter().all(|entry| !entry.is_default())
&& let Some(entry) = entries.first_mut()
{
entry.mark_default();
}
// Iterate over all the entries and tell the bootloader interface what the entries are.
for entry in &entries {
// If the entry is the default entry, tell the bootloader interface it is the default.
if entry.is_default() {
// Tell the bootloader interface what the default entry is.
BootloaderInterface::set_default_entry(entry.name().to_string())
.context("unable to set default entry in bootloader interface")?;
break;
}
}
// Tell the bootloader interface what entries are available. // Tell the bootloader interface what entries are available.
BootloaderInterface::set_entries(entries.iter().map(|entry| entry.name())) BootloaderInterface::set_entries(entries.iter().map(|entry| entry.name()))
.context("unable to set entries in bootloader interface")?; .context("unable to set entries in bootloader interface")?;
@@ -288,18 +269,81 @@ fn run() -> Result<()> {
// Execute the late phase. // Execute the late phase.
phase(context.clone(), &config.phases.late).context("unable to execute late phase")?; phase(context.clone(), &config.phases.late).context("unable to execute late phase")?;
// Acquire the timeout setting from the bootloader interface.
let bootloader_interface_timeout =
BootloaderInterface::get_timeout().context("unable to get bootloader interface timeout")?;
// Acquire the default entry from the bootloader interface.
let bootloader_interface_default_entry = BootloaderInterface::get_default_entry()
.context("unable to get bootloader interface default entry")?;
// Acquire the oneshot entry from the bootloader interface.
let bootloader_interface_oneshot_entry = BootloaderInterface::get_oneshot_entry()
.context("unable to get bootloader interface oneshot entry")?;
// If --boot is specified, boot that entry immediately. // If --boot is specified, boot that entry immediately.
let force_boot_entry = context.root().options().boot.as_ref(); let mut force_boot_entry = context.root().options().boot.clone();
// If --force-menu is specified, show the boot menu regardless of the value of --boot. // If --force-menu is specified, show the boot menu regardless of the value of --boot.
let force_boot_menu = context.root().options().force_menu; let mut force_boot_menu = context.root().options().force_menu;
// Determine the menu timeout in seconds based on the options or configuration. // Determine the menu timeout in seconds based on the options or configuration.
// We prefer the options over the configuration to allow for overriding. // We prefer the options over the configuration to allow for overriding.
let menu_timeout = context let mut menu_timeout = context
.root() .root()
.options() .options()
.menu_timeout .menu_timeout
.unwrap_or(config.options.menu_timeout); .unwrap_or(config.options.menu_timeout);
// Apply bootloader interface timeout settings.
match bootloader_interface_timeout {
BootloaderInterfaceTimeout::MenuForce => {
// Force the boot menu.
force_boot_menu = true;
}
BootloaderInterfaceTimeout::MenuHidden | BootloaderInterfaceTimeout::MenuDisabled => {
// Hide the boot menu by setting the timeout to zero.
menu_timeout = 0;
}
BootloaderInterfaceTimeout::Timeout(timeout) => {
// Configure the timeout to the specified value.
menu_timeout = timeout;
}
BootloaderInterfaceTimeout::Unspecified => {
// Do nothing.
}
}
// Apply bootloader interface default entry settings.
if let Some(ref bootloader_interface_default_entry) = bootloader_interface_default_entry {
// Iterate over all the entries and mark the default entry as the one specified.
for entry in &mut entries {
// Mark the entry as the default entry if it matches the specified entry.
// If the entry does not match the specified entry, unmark it as the default entry.
if entry.is_match(bootloader_interface_default_entry) {
entry.mark_default();
} else {
entry.unmark_default();
}
}
}
// Apply bootloader interface oneshot entry settings.
// If set, we will force booting the oneshot entry.
if let Some(ref bootloader_interface_oneshot_entry) = bootloader_interface_oneshot_entry {
force_boot_entry = Some(bootloader_interface_oneshot_entry.clone());
}
// If no entries were the default, pick the first entry as the default entry.
if entries.iter().all(|entry| !entry.is_default())
&& let Some(entry) = entries.first_mut()
{
entry.mark_default();
}
// Convert the menu timeout to a duration.
let menu_timeout = Duration::from_secs(menu_timeout); let menu_timeout = Duration::from_secs(menu_timeout);
// Use the forced boot entry if possible, otherwise pick the first entry using a boot menu. // Use the forced boot entry if possible, otherwise pick the first entry using a boot menu.

View File

@@ -2,7 +2,7 @@ use crate::entries::BootableEntry;
use crate::integrations::bootloader_interface::BootloaderInterface; use crate::integrations::bootloader_interface::BootloaderInterface;
use crate::platform::timer::PlatformTimer; use crate::platform::timer::PlatformTimer;
use anyhow::{Context, Result, bail}; use anyhow::{Context, Result, bail};
use log::info; use log::{info, warn};
use std::time::Duration; use std::time::Duration;
use uefi::ResultExt; use uefi::ResultExt;
use uefi::boot::TimerTrigger; use uefi::boot::TimerTrigger;
@@ -56,16 +56,35 @@ fn read(input: &mut Input, timeout: &Duration) -> Result<MenuOperation> {
uefi::boot::set_timer(&timer_event, trigger).context("unable to set timeout timer")?; uefi::boot::set_timer(&timer_event, trigger).context("unable to set timeout timer")?;
let mut events = vec![timer_event, key_event]; let mut events = vec![timer_event, key_event];
let event = uefi::boot::wait_for_event(&mut events)
// Wait for either the timer event or the key event to trigger.
// Store the result so that we can free the timer event.
let event_result = uefi::boot::wait_for_event(&mut events)
.discard_errdata() .discard_errdata()
.context("unable to wait for event")?; .context("unable to wait for event");
// Close the timer event that we acquired. // Close the timer event that we acquired.
// We don't need to close the key event because it is owned globally. // We don't need to close the key event because it is owned globally.
if let Some(timer_event) = events.into_iter().next() { if let Some(timer_event) = events.into_iter().next() {
uefi::boot::close_event(timer_event).context("unable to close timer event")?; // Store the result of the close event so we can determine if we can safely assert it.
let close_event_result =
uefi::boot::close_event(timer_event).context("unable to close timer event");
if event_result.is_err()
&& let Err(ref close_event_error) = close_event_result
{
// Log a warning if we failed to close the timer event.
// This is done to ensure we don't mask the wait_for_event error.
warn!("unable to close timer event: {}", close_event_error);
} else {
// If we reach here, we can safely assert that the close event succeeded without
// masking the wait_for_event error.
close_event_result?;
}
} }
// Acquire the event that triggered.
let event = event_result?;
// The first event is the timer event. // The first event is the timer event.
// If it has triggered, the user did not select a numbered entry. // If it has triggered, the user did not select a numbered entry.
if event == 0 { if event == 0 {
@@ -114,7 +133,7 @@ fn select_with_input<'a>(
info!("Boot Menu:"); info!("Boot Menu:");
for (index, entry) in entries.iter().enumerate() { for (index, entry) in entries.iter().enumerate() {
let title = entry.context().stamp(&entry.declaration().title); let title = entry.context().stamp(&entry.declaration().title);
info!(" [{}] {} ({})", index, title, entry.name()); info!(" [{}] {}", index, title);
} }
} }

View File

@@ -44,6 +44,13 @@ impl OptionsRepresentable for SproutOptions {
/// All the Sprout options that are defined. /// All the Sprout options that are defined.
fn options() -> &'static [(&'static str, OptionDescription<'static>)] { fn options() -> &'static [(&'static str, OptionDescription<'static>)] {
&[ &[
(
"autoconfigure",
OptionDescription {
description: "Enable Sprout Autoconfiguration",
form: OptionForm::Flag,
},
),
( (
"config", "config",
OptionDescription { OptionDescription {

View File

@@ -46,8 +46,24 @@ pub trait OptionsRepresentable {
let configured: BTreeMap<_, _> = BTreeMap::from_iter(Self::options().to_vec()); let configured: BTreeMap<_, _> = BTreeMap::from_iter(Self::options().to_vec());
// Collect all the arguments to Sprout. // Collect all the arguments to Sprout.
// Skip the first argument which is the path to our executable. // Skip the first argument, which is the path to our executable.
let args = std::env::args().skip(1).collect::<Vec<_>>(); 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;
}
// Represent options as key-value pairs. // Represent options as key-value pairs.
let mut options = BTreeMap::new(); let mut options = BTreeMap::new();

View File

@@ -0,0 +1,25 @@
use crate::actions;
use crate::context::SproutContext;
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].
/// Any error from the actions is propagated into the [Result] and will interrupt further
/// execution of phase actions.
pub fn phase(context: Rc<SproutContext>, phase: &[PhaseConfiguration]) -> Result<()> {
for item in phase {
let mut context = context.fork();
// Insert the values into the context.
context.insert(&item.values);
let context = context.freeze();
// Execute all the actions in this phase configuration.
for action in item.actions.iter() {
actions::execute(context.clone(), action)
.context(format!("unable to execute action '{}'", action))?;
}
}
Ok(())
}

View File

@@ -53,9 +53,14 @@ fn arch_ticks() -> u64 {
/// Acquire the tick frequency reported by the platform. /// Acquire the tick frequency reported by the platform.
fn arch_frequency() -> TickFrequency { fn arch_frequency() -> TickFrequency {
#[cfg(target_arch = "aarch64")] #[cfg(target_arch = "aarch64")]
return aarch64::frequency(); let frequency = aarch64::frequency();
#[cfg(target_arch = "x86_64")] #[cfg(target_arch = "x86_64")]
return x86_64::frequency(); let frequency = x86_64::frequency();
// If the frequency is 0, then something went very wrong and we should panic.
if frequency.ticks() == 0 {
panic!("timer frequency is zero");
}
frequency
} }
/// Platform timer that allows measurement of the elapsed time. /// Platform timer that allows measurement of the elapsed time.

View File

@@ -50,17 +50,17 @@ pub fn stop() -> u64 {
} }
/// Measure the frequency of the platform timer. /// Measure the frequency of the platform timer.
fn measure_frequency(duration: &Duration) -> u64 { fn measure_frequency() -> u64 {
let start = start(); let start = start();
uefi::boot::stall(*duration); uefi::boot::stall(MEASURE_FREQUENCY_DURATION);
let stop = stop(); let stop = stop();
let elapsed = stop.wrapping_sub(start) as f64; let elapsed = stop.wrapping_sub(start) as f64;
(elapsed / duration.as_secs_f64()) as u64 (elapsed / MEASURE_FREQUENCY_DURATION.as_secs_f64()) as u64
} }
/// Acquire the platform timer frequency. /// Acquire the platform timer frequency.
/// On x86_64, this is slightly expensive, so it should be done once. /// On x86_64, this is slightly expensive, so it should be done once.
pub fn frequency() -> TickFrequency { pub fn frequency() -> TickFrequency {
let frequency = measure_frequency(&MEASURE_FREQUENCY_DURATION); let frequency = measure_frequency();
TickFrequency::Measured(frequency, MEASURE_FREQUENCY_DURATION) TickFrequency::Measured(frequency, MEASURE_FREQUENCY_DURATION)
} }

View File

@@ -1,4 +1,5 @@
use anyhow::{Context, Result}; use anyhow::{Context, Result, bail};
use sha2::{Digest, Sha256};
use std::ops::Deref; use std::ops::Deref;
use uefi::boot::SearchType; use uefi::boot::SearchType;
use uefi::fs::{FileSystem, Path}; use uefi::fs::{FileSystem, Path};
@@ -18,6 +19,9 @@ pub mod media_loader;
/// Support code for EFI variables. /// Support code for EFI variables.
pub mod variables; pub mod variables;
/// Implements a version comparison algorithm according to the BLS specification.
pub mod vercmp;
/// Parses the input `path` as a [DevicePath]. /// Parses the input `path` as a [DevicePath].
/// Uses the [DevicePathFromText] protocol exclusively, and will fail if it cannot acquire the protocol. /// Uses the [DevicePathFromText] protocol exclusively, and will fail if it cannot acquire the protocol.
pub fn text_to_device_path(path: &str) -> Result<PoolDevicePath> { pub fn text_to_device_path(path: &str) -> Result<PoolDevicePath> {
@@ -198,7 +202,7 @@ pub fn combine_options<T: AsRef<str>>(options: impl Iterator<Item = T>) -> Strin
/// Produce a unique hash for the input. /// Produce a unique hash for the input.
/// This uses SHA-256, which is unique enough but relatively short. /// This uses SHA-256, which is unique enough but relatively short.
pub fn unique_hash(input: &str) -> String { pub fn unique_hash(input: &str) -> String {
sha256::digest(input.as_bytes()) hex::encode(Sha256::digest(input.as_bytes()))
} }
/// Represents the type of partition GUID that can be retrieved. /// Represents the type of partition GUID that can be retrieved.
@@ -272,3 +276,22 @@ pub fn find_handle(protocol: &Guid) -> Result<Option<Handle>> {
} }
} }
} }
/// 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

@@ -33,8 +33,6 @@ struct MediaLoaderProtocol {
/// You MUST call [MediaLoaderHandle::unregister] when ready to unregister. /// You MUST call [MediaLoaderHandle::unregister] when ready to unregister.
/// [Drop] is not implemented for this type. /// [Drop] is not implemented for this type.
pub struct MediaLoaderHandle { pub struct MediaLoaderHandle {
/// The vendor GUID of the media loader.
guid: Guid,
/// The handle of the media loader in the UEFI stack. /// The handle of the media loader in the UEFI stack.
handle: Handle, handle: Handle,
/// The protocol interface pointer. /// The protocol interface pointer.
@@ -229,7 +227,6 @@ impl MediaLoaderHandle {
// Return a handle to the media loader. // Return a handle to the media loader.
Ok(Self { Ok(Self {
guid,
handle: primary_handle, handle: primary_handle,
protocol, protocol,
path, path,
@@ -239,13 +236,8 @@ impl MediaLoaderHandle {
/// Unregisters a media loader from the UEFI stack. /// Unregisters a media loader from the UEFI stack.
/// This will free the memory allocated by the passed data. /// This will free the memory allocated by the passed data.
pub fn unregister(self) -> Result<()> { pub fn unregister(self) -> Result<()> {
// Check if the media loader is registered. // SAFETY: We know that the media loader is registered if the handle is valid,
// If it is not, we don't need to do anything. // so we can safely uninstall it.
if !Self::already_registered(self.guid)? {
return Ok(());
}
// SAFETY: We know that the media loader is registered, so we can safely uninstall it.
// We should have allocated the pointers involved, so we can safely free them. // We should have allocated the pointers involved, so we can safely free them.
unsafe { unsafe {
// Uninstall the protocol interface for the device path protocol. // Uninstall the protocol interface for the device path protocol.

View File

@@ -1,4 +1,6 @@
use crate::utils;
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use log::warn;
use uefi::{CString16, guid}; use uefi::{CString16, guid};
use uefi_raw::Status; use uefi_raw::Status;
use uefi_raw::table::runtime::{VariableAttributes, VariableVendor}; use uefi_raw::table::runtime::{VariableAttributes, VariableVendor};
@@ -44,6 +46,41 @@ impl VariableController {
CString16::try_from(key).context("unable to convert variable name to CString16") CString16::try_from(key).context("unable to convert variable name to CString16")
} }
/// Retrieve the cstr16 value specified by the `key`.
/// Returns None if the value isn't set.
/// If the value is not decodable, we will return None and log a warning.
pub fn get_cstr16(&self, key: &str) -> Result<Option<String>> {
let name = Self::name(key)?;
// Retrieve the variable data, handling variable not existing as None.
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) {
Ok(value) => {
// We have a value, so return the UTF-8 value.
Ok(Some(value.to_string()))
}
Err(error) => {
// We encountered an error, so warn and return None.
warn!("efi variable '{}' is not valid UTF-16: {}", key, error);
Ok(None)
}
}
}
Err(error) => {
// If the variable does not exist, we will return None.
if error.status() == Status::NOT_FOUND {
Ok(None)
} else {
Err(error).with_context(|| format!("unable to get efi variable {}", key))
}
}
}
}
/// Retrieve a boolean value specified by the `key`. /// Retrieve a boolean value specified by the `key`.
pub fn get_bool(&self, key: &str) -> Result<bool> { pub fn get_bool(&self, key: &str) -> Result<bool> {
let name = Self::name(key)?; let name = Self::name(key)?;
@@ -98,4 +135,18 @@ impl VariableController {
pub fn set_bool(&self, key: &str, value: bool, class: VariableClass) -> Result<()> { pub fn set_bool(&self, key: &str, value: bool, class: VariableClass) -> Result<()> {
self.set(key, &[value as u8], class) self.set(key, &[value as u8], class)
} }
/// Set the u64 little-endian variable specified by `key` to `value`.
/// The variable `class` controls the attributes for the variable.
pub fn set_u64le(&self, key: &str, value: u64, class: VariableClass) -> Result<()> {
self.set(key, &value.to_le_bytes(), class)
}
pub fn remove(&self, key: &str) -> Result<()> {
let name = Self::name(key)?;
// Delete the variable from UEFI.
uefi::runtime::delete_variable(&name, &self.vendor)
.with_context(|| format!("unable to remove efi variable {}", key))
}
} }

View File

@@ -0,0 +1,184 @@
use std::cmp::Ordering;
use std::iter::Peekable;
/// Handles single character advancement and comparison.
macro_rules! handle_single_char {
($ca: expr, $cb:expr, $a_chars:expr, $b_chars:expr, $c:expr) => {
match ($ca == $c, $cb == $c) {
(true, false) => return Ordering::Less,
(false, true) => return Ordering::Greater,
(true, true) => {
$a_chars.next();
$b_chars.next();
continue;
}
_ => {}
}
};
}
/// Compares two strings using the BLS version comparison specification.
/// Handles optional values as well by comparing only if both are specified.
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.
(Some(_a), None) => Ordering::Less,
// If the first value is None, return that it is greater than the second.
(None, Some(_b)) => Ordering::Greater,
// If both values are None, return that they are equal.
(None, None) => Ordering::Equal,
}
}
/// Compares two strings using the BLS version comparison specification.
/// See: https://uapi-group.org/specifications/specs/version_format_specification/
pub fn compare_versions(a: &str, b: &str) -> Ordering {
// Acquire a peekable iterator for each string.
let mut a_chars = a.chars().peekable();
let mut b_chars = b.chars().peekable();
// Loop until we have reached the end of one of the strings.
loop {
// Skip invalid characters in both strings.
skip_invalid(&mut a_chars);
skip_invalid(&mut b_chars);
// Check if either string has ended.
match (a_chars.peek(), b_chars.peek()) {
// No more characters in either string.
(None, None) => return Ordering::Equal,
// One string has ended, the other hasn't.
(None, Some(_)) => return Ordering::Less,
(Some(_), None) => return Ordering::Greater,
// Both strings have characters left.
(Some(&ca), Some(&cb)) => {
// Handle the ~ character.
handle_single_char!(ca, cb, a_chars, b_chars, '~');
// Handle '-' character.
handle_single_char!(ca, cb, a_chars, b_chars, '-');
// Handle the '^' character.
handle_single_char!(ca, cb, a_chars, b_chars, '^');
// Handle the '.' character.
handle_single_char!(ca, cb, a_chars, b_chars, '.');
// Handle digits with numerical comparison.
// We key off of the A character being a digit intentionally as we presume
// this indicates it will be the same at this position.
if ca.is_ascii_digit() || cb.is_ascii_digit() {
let result = compare_numeric(&mut a_chars, &mut b_chars);
if result != Ordering::Equal {
return result;
}
continue;
}
// Handle letters with alphabetical comparison.
// We key off of the A character being alphabetical intentionally as we presume
// this indicates it will be the same at this position.
if ca.is_ascii_alphabetic() || cb.is_ascii_alphabetic() {
let result = compare_alphabetic(&mut a_chars, &mut b_chars);
if result != Ordering::Equal {
return result;
}
continue;
}
}
}
}
}
/// Skips characters that are not in the valid character set.
fn skip_invalid<I: Iterator<Item = char>>(iter: &mut Peekable<I>) {
while let Some(&c) = iter.peek() {
if is_valid_char(c) {
break;
}
iter.next();
}
}
/// Checks if a character is in the valid character set for comparison.
fn is_valid_char(c: char) -> bool {
matches!(c, 'a'..='z' | 'A'..='Z' | '0'..='9' | '-' | '.' | '~' | '^')
}
/// Compares numerical prefixes by extracting numbers.
fn compare_numeric<I: Iterator<Item = char>>(
iter_a: &mut Peekable<I>,
iter_b: &mut Peekable<I>,
) -> Ordering {
let num_a = extract_number(iter_a);
let num_b = extract_number(iter_b);
num_a.cmp(&num_b)
}
/// Extracts a number from the iterator, skipping leading zeros.
fn extract_number<I: Iterator<Item = char>>(iter: &mut Peekable<I>) -> u64 {
// Skip leading zeros
while let Some(&'0') = iter.peek() {
iter.next();
}
let mut num = 0u64;
while let Some(&c) = iter.peek() {
if c.is_ascii_digit() {
iter.next();
num = num.saturating_mul(10).saturating_add(c as u64 - '0' as u64);
} else {
break;
}
}
num
}
/// Compares alphabetical prefixes
/// Capital letters compare lower than lowercase letters (B < a)
fn compare_alphabetic<I: Iterator<Item = char>>(
iter_a: &mut Peekable<I>,
iter_b: &mut Peekable<I>,
) -> Ordering {
loop {
return match (iter_a.peek(), iter_b.peek()) {
(Some(&ca), Some(&cb)) if ca.is_ascii_alphabetic() && cb.is_ascii_alphabetic() => {
if ca == cb {
// Same character, we should continue.
iter_a.next();
iter_b.next();
continue;
}
// Different characters found.
// All capital letters compare lower than lowercase letters.
match (ca.is_ascii_uppercase(), cb.is_ascii_uppercase()) {
(true, false) => Ordering::Less, // uppercase < lowercase
(false, true) => Ordering::Greater, // lowercase > uppercase
(true, true) => ca.cmp(&cb), // both are uppercase
(false, false) => ca.cmp(&cb), // both are lowercase
}
}
(Some(&ca), Some(_)) if ca.is_ascii_alphabetic() => {
// a has letters, b doesn't
Ordering::Greater
}
(Some(_), Some(&cb)) if cb.is_ascii_alphabetic() => {
// b has letters, a doesn't
Ordering::Less
}
(Some(&ca), None) if ca.is_ascii_alphabetic() => Ordering::Greater,
(None, Some(&cb)) if cb.is_ascii_alphabetic() => Ordering::Less,
_ => Ordering::Equal,
};
}
}

145
docs/setup/signed/debian.md Normal file
View File

@@ -0,0 +1,145 @@
# Setup Sprout for Debian with Secure Boot
## Prerequisites
- 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
```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/lib/shim/shimx64.efi.signed /boot/efi/EFI/sprout/shimx64.efi
$ cp /usr/lib/shim/mmx64.efi.signed /boot/efi/EFI/sprout/mmx64.efi
$ cp /usr/lib/shim/fbx64.efi.signed /boot/efi/EFI/sprout/fbx64.efi
# For aarch64, copy the following artifacts to the Sprout EFI directory.
$ cp /usr/lib/shim/shimaa64.efi.signed /boot/efi/EFI/sprout/shimaa64.efi
$ cp /usr/lib/shim/mmaa64.efi.signed /boot/efi/EFI/sprout/mmaa64.efi
$ cp /usr/lib/shim/fbaa64.efi.signed /boot/efi/EFI/sprout/fbaa64.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 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/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.
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 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/ESP_PARTITION with the actual path to the ESP partition block device.
```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'
# 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'
```
Reboot your machine and it should boot into Sprout.
If Sprout fails to boot, it should boot into the original bootloader.

144
docs/setup/signed/ubuntu.md Normal file
View File

@@ -0,0 +1,144 @@
# Setup Sprout for Ubuntu with Secure Boot
## Prerequisites
- 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
```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/lib/shim/shimx64.efi.signed /boot/efi/EFI/sprout/shimx64.efi
$ cp /usr/lib/shim/mmx64.efi /boot/efi/EFI/sprout/mmx64.efi
$ cp /usr/lib/shim/fbx64.efi /boot/efi/EFI/sprout/fbx64.efi
# For aarch64, copy the following artifacts to the Sprout EFI directory.
$ cp /usr/lib/shim/shimaa64.efi.signed /boot/efi/EFI/sprout/shimaa64.efi
$ cp /usr/lib/shim/mmaa64.efi /boot/efi/EFI/sprout/mmaa64.efi
$ cp /usr/lib/shim/fbaa64.efi /boot/efi/EFI/sprout/fbaa64.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 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/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.
If `/boot` is FAT32 or ExFAT, you can skip this step.
Most Ubuntu 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 will 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 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/ESP_PARTITION with the actual path to the ESP partition block device.
```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'
# 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'
```
Reboot your machine and it should boot into Sprout.
If Sprout fails to boot, it should boot into the original bootloader.

View File

@@ -1,4 +1,4 @@
# Setup Sprout on Alpine Edge # Setup Sprout for Alpine Edge without Secure Boot
## Prerequisites ## Prerequisites

View File

@@ -1,4 +1,4 @@
# Setup Sprout on Fedora # Setup Sprout for Fedora without Secure Boot
## Prerequisites ## Prerequisites

View File

@@ -1,4 +1,4 @@
# Setup Sprout to boot Linux # Setup Sprout for Linux without Secure Boot
## Prerequisites ## Prerequisites

View File

@@ -1,4 +1,4 @@
# Setup Sprout to boot Windows # Setup Sprout for Windows without Secure Boot
## Prerequisites ## Prerequisites

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 MiB

View File

@@ -12,7 +12,6 @@ COPY shell.efi /work/SHELL.EFI
COPY xen.efi /work/XEN.EFI COPY xen.efi /work/XEN.EFI
COPY xen.cfg /work/XEN.CFG COPY xen.cfg /work/XEN.CFG
COPY initramfs /work/INITRAMFS COPY initramfs /work/INITRAMFS
COPY edera-splash.png /work/EDERA-SPLASH.PNG
COPY bls.conf /work/BLS.CONF COPY bls.conf /work/BLS.CONF
RUN truncate -s128MiB sprout.img && \ RUN truncate -s128MiB sprout.img && \
parted --script sprout.img mklabel gpt > /dev/null 2>&1 && \ parted --script sprout.img mklabel gpt > /dev/null 2>&1 && \
@@ -29,7 +28,6 @@ RUN truncate -s128MiB sprout.img && \
mcopy -i sprout.img XEN.EFI ::/EFI/BOOT/ && \ mcopy -i sprout.img XEN.EFI ::/EFI/BOOT/ && \
mcopy -i sprout.img XEN.CFG ::/EFI/BOOT/ && \ mcopy -i sprout.img XEN.CFG ::/EFI/BOOT/ && \
mcopy -i sprout.img SPROUT.TOML ::/ && \ mcopy -i sprout.img SPROUT.TOML ::/ && \
mcopy -i sprout.img EDERA-SPLASH.PNG ::/ && \
mcopy -i sprout.img INITRAMFS ::/ && \ mcopy -i sprout.img INITRAMFS ::/ && \
mcopy -i sprout.img BLS.CONF ::/LOADER/ENTRIES/ && \ mcopy -i sprout.img BLS.CONF ::/LOADER/ENTRIES/ && \
mv sprout.img /sprout.img mv sprout.img /sprout.img

View File

@@ -107,7 +107,6 @@ if [ "${SKIP_SPROUT_BUILD}" != "1" ]; then
cp "hack/dev/configs/${SPROUT_CONFIG_NAME}.sprout.toml" "${FINAL_DIR}/sprout.toml" cp "hack/dev/configs/${SPROUT_CONFIG_NAME}.sprout.toml" "${FINAL_DIR}/sprout.toml"
cp "hack/dev/configs/xen.cfg" "${FINAL_DIR}/xen.cfg" cp "hack/dev/configs/xen.cfg" "${FINAL_DIR}/xen.cfg"
cp "hack/dev/assets/edera-splash.png" "${FINAL_DIR}/edera-splash.png"
cp "hack/dev/configs/bls.conf" "${FINAL_DIR}/bls.conf" cp "hack/dev/configs/bls.conf" "${FINAL_DIR}/bls.conf"
mkdir -p "${FINAL_DIR}/efi/EFI/BOOT" mkdir -p "${FINAL_DIR}/efi/EFI/BOOT"
@@ -125,7 +124,6 @@ if [ "${SKIP_SPROUT_BUILD}" != "1" ]; then
cp "${FINAL_DIR}/xen.cfg" "${FINAL_DIR}/efi/EFI/BOOT/XEN.CFG" cp "${FINAL_DIR}/xen.cfg" "${FINAL_DIR}/efi/EFI/BOOT/XEN.CFG"
fi fi
cp "${FINAL_DIR}/sprout.toml" "${FINAL_DIR}/efi/SPROUT.TOML" cp "${FINAL_DIR}/sprout.toml" "${FINAL_DIR}/efi/SPROUT.TOML"
cp "${FINAL_DIR}/edera-splash.png" "${FINAL_DIR}/efi/EDERA-SPLASH.PNG"
cp "${FINAL_DIR}/initramfs" "${FINAL_DIR}/efi/INITRAMFS" cp "${FINAL_DIR}/initramfs" "${FINAL_DIR}/efi/INITRAMFS"
fi fi

View File

@@ -1,161 +0,0 @@
use crate::context::SproutContext;
use crate::utils::framebuffer::Framebuffer;
use crate::utils::read_file_contents;
use anyhow::{Context, Result};
use image::imageops::{FilterType, resize};
use image::math::Rect;
use image::{DynamicImage, ImageBuffer, ImageFormat, ImageReader, Rgba};
use serde::{Deserialize, Serialize};
use std::io::Cursor;
use std::rc::Rc;
use std::time::Duration;
use uefi::boot::ScopedProtocol;
use uefi::proto::console::gop::GraphicsOutput;
/// We set the default splash time to zero, as this makes it so any logging shows up
/// on top of the splash and does not hold up the boot process.
const DEFAULT_SPLASH_TIME: u32 = 0;
/// The configuration of the splash action.
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
pub struct SplashConfiguration {
/// The path to the image to display.
/// Currently, only PNG images are supported.
pub image: String,
/// The time to display the splash image without interruption, in seconds.
/// The default value is `0` which will display the image and let everything
/// continue.
#[serde(default = "default_splash_time")]
pub time: u32,
}
fn default_splash_time() -> u32 {
DEFAULT_SPLASH_TIME
}
/// Acquire the [GraphicsOutput]. We will find the first graphics output only.
fn setup_graphics() -> Result<ScopedProtocol<GraphicsOutput>> {
// Grab the handle for the graphics output protocol.
let gop_handle = uefi::boot::get_handle_for_protocol::<GraphicsOutput>()
.context("unable to get graphics output")?;
// Open the graphics output protocol exclusively.
uefi::boot::open_protocol_exclusive::<GraphicsOutput>(gop_handle)
.context("unable to open graphics output")
}
/// Produces a [Rect] that fits the `image` inside the specified `frame`.
/// The output [Rect] should be used to resize the image.
fn fit_to_frame(image: &DynamicImage, frame: Rect) -> Rect {
// Convert the image dimensions to a [Rect].
let input = Rect {
x: 0,
y: 0,
width: image.width(),
height: image.height(),
};
// Handle the case where the image is zero-sized.
if input.height == 0 || input.width == 0 {
return input;
}
// Calculate the ratio of the image dimensions.
let input_ratio = input.width as f32 / input.height as f32;
// Calculate the ratio of the frame dimensions.
let frame_ratio = frame.width as f32 / frame.height as f32;
// Create [Rect] to store the output dimensions.
let mut output = Rect {
x: 0,
y: 0,
width: frame.width,
height: frame.height,
};
// Handle the case where the output is zero-sized.
if output.height == 0 || output.width == 0 {
return output;
}
if input_ratio < frame_ratio {
output.width = (frame.height as f32 * input_ratio).floor() as u32;
output.height = frame.height;
output.x = frame.x + (frame.width - output.width) / 2;
output.y = frame.y;
} else {
output.width = frame.width;
output.height = (frame.width as f32 / input_ratio).floor() as u32;
output.x = frame.x;
output.y = frame.y + (frame.height - output.height) / 2;
}
output
}
/// Resize the input `image` to fit the `frame`.
fn resize_to_fit(image: &DynamicImage, frame: Rect) -> ImageBuffer<Rgba<u8>, Vec<u8>> {
let image = image.to_rgba8();
resize(&image, frame.width, frame.height, FilterType::Lanczos3)
}
/// Draw the `image` on the screen using [GraphicsOutput].
fn draw(image: DynamicImage) -> Result<()> {
// Acquire the [GraphicsOutput] protocol.
let mut gop = setup_graphics()?;
// Acquire the current screen size.
let (width, height) = gop.current_mode_info().resolution();
// Create a display frame.
let display_frame = Rect {
x: 0,
y: 0,
width: width as _,
height: height as _,
};
// Fit the image to the display frame.
let fit = fit_to_frame(&image, display_frame);
// Resize the image to fit the display frame.
let image = resize_to_fit(&image, fit);
// Create a framebuffer to draw the image on.
let mut framebuffer =
Framebuffer::new(width, height).context("unable to create framebuffer")?;
// Iterate over the pixels in the image and put them on the framebuffer.
for (x, y, pixel) in image.enumerate_pixels() {
let Some(fb) = framebuffer.pixel((x + fit.x) as usize, (fit.y + y) as usize) else {
continue;
};
fb.red = pixel[0];
fb.green = pixel[1];
fb.blue = pixel[2];
}
// Blit the framebuffer to the screen.
framebuffer.blit(&mut gop)?;
Ok(())
}
/// Runs the splash action with the specified `configuration` inside the provided `context`.
pub fn splash(context: Rc<SproutContext>, configuration: &SplashConfiguration) -> Result<()> {
// Stamp the image path value.
let image = context.stamp(&configuration.image);
// Read the image contents.
let image = read_file_contents(Some(context.root().loaded_image_path()?), &image)?;
// Decode the image as a PNG.
let image = ImageReader::with_format(Cursor::new(image), ImageFormat::Png)
.decode()
.context("unable to decode splash image")?;
// Draw the image on the screen.
draw(image)?;
// Sleep for the specified time.
std::thread::sleep(Duration::from_secs(configuration.time as u64));
// Return control to sprout.
Ok(())
}

View File

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