39 Commits

Author SHA1 Message Date
0ce6ffa3da sprout: version 0.0.23 2025-11-04 12:16:53 -05:00
a1028c629d fix(eficore/env): improve quirk handling for dell systems 2025-11-03 23:58:48 -05:00
503a9cba0a chore(code): move load options parsing to crates/eficore 2025-11-03 23:45:35 -05:00
532fb38d5a chore(code): move crates/sprout to crates/boot and name it edera-sprout-boot 2025-11-03 22:52:54 -05:00
9a803ad355 chore(code): sbat section generator build tool 2025-11-03 22:37:06 -05:00
632781abbf chore(code): split much of the efi support code to crates/eficore 2025-11-03 20:47:21 -05:00
48497700d8 fix(sprout): make secure boot warning more specific 2025-11-03 15:31:44 -05:00
34ac57d291 sprout: version 0.0.22 2025-11-03 14:47:59 -05:00
37abe49347 feat(sprout): implement custom logger which shortens output 2025-11-03 14:45:48 -05:00
79615f7436 chore(docs): add openSUSE Secure Boot guide 2025-11-03 04:49:55 -05:00
7a7d92ef70 chore(hack): add keyboard and mouse to dev qemu 2025-11-03 03:41:03 -05:00
b34c171ccb fix(hack): remove xen images during clean 2025-11-03 03:18:23 -05:00
384c1e7eaf chore(docker): swap rust for docker builds to 1.91.0 2025-11-03 03:08:09 -05:00
0b7b5066e4 chore(workflows): align on push/pull_request events across workflows 2025-11-03 03:01:57 -05:00
ba634ed68a fix(platform/timer): on x86_64, elide usage of asm!() and use _rdtsc() intrinsic 2025-11-03 02:57:22 -05:00
be63c5171b chore(doc): add clarifying comments in vercmp 2025-11-03 02:46:41 -05:00
f740c35568 fix(tpm): add clarifying parentheses to version check 2025-11-03 02:37:52 -05:00
8a0b70a99b chore(doc): add documentation to VariableController::remove 2025-11-03 02:35:57 -05:00
223a00563e chore(menu): add note as to why we match on the timer event 2025-11-03 02:35:01 -05:00
029e59b209 sprout: version 0.0.21 2025-11-03 02:15:23 -05:00
bde1cd01c8 Merge pull request #28 from edera-dev/experiment/no-std
feat(sprout): introduce no_std sprout which uses stable rust
2025-11-02 23:11:52 -08:00
0017d7874d feat(sprout): introduce no_std sprout which uses stable rust 2025-11-03 02:04:21 -05:00
1c2acdc568 chore(build): pin rust-toolchain to nightly-2025-11-03 2025-11-03 00:37:04 -05:00
1f322ff4bf chore(workflows): publish should upload and attest all artifacts in a single zip 2025-11-03 00:32:54 -05:00
0bb7d7ccb1 sprout: version 0.0.20 2025-11-03 00:10:20 -05:00
74b6a8deb3 chore(workflows): release workflow should attest all artifacts together 2025-11-03 00:06:53 -05:00
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
102 changed files with 1785 additions and 1286 deletions

View File

@@ -1,10 +1,12 @@
name: zizmor name: zizmor
on: on:
push:
branches: ["main"]
pull_request: pull_request:
branches: ["**"] branches:
- main
push:
branches:
- main
permissions: permissions:
contents: read # Needed to checkout the repository. contents: read # Needed to checkout the repository.

View File

@@ -1,10 +1,12 @@
name: codeql name: codeql
on: on:
push:
branches: [ "main" ]
pull_request: pull_request:
branches: [ "main" ] branches:
- main
push:
branches:
- main
schedule: schedule:
- cron: '33 16 * * 0' - cron: '33 16 * * 0'

View File

@@ -1,19 +1,12 @@
name: publish name: publish
on: on:
push:
branches:
- main
pull_request: pull_request:
branches: branches:
- main - main
paths: push:
- bin/** branches:
- src/** - main
- Cargo.*
- rust-toolchain.toml
- .github/workflows/publish.yaml
permissions: permissions:
contents: read # Needed to checkout the repository. contents: read # Needed to checkout the repository.
@@ -27,6 +20,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 +41,15 @@ jobs:
- name: 'assemble artifacts' - name: 'assemble artifacts'
run: ./hack/assemble.sh run: ./hack/assemble.sh
- name: 'upload sprout-x86_64.efi artifact' - name: 'upload artifacts'
id: upload
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with: with:
name: sprout-x86_64.efi name: artifacts
path: target/assemble/sprout-x86_64.efi path: target/assemble/*
- name: 'upload sprout-aarch64.efi artifact' - name: 'attest artifacts'
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 uses: actions/attest-build-provenance@977bb373ede98d70efdf65b84cb5f73e068dcc2a # v3.0.0
with: with:
name: sprout-aarch64.efi subject-name: artifacts.zip
path: target/assemble/sprout-aarch64.efi subject-digest: "sha256:${{ steps.upload.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
@@ -36,9 +38,14 @@ jobs:
run: | run: |
cargo version cargo version
- name: 'assemble artifacts' - name: 'assemble release artifacts'
run: ./hack/assemble.sh run: ./hack/assemble.sh
- name: 'attest release artifacts'
uses: actions/attest-build-provenance@977bb373ede98d70efdf65b84cb5f73e068dcc2a # v3.0.0
with:
subject-path: target/assemble/*
- 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

220
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"
@@ -115,43 +65,43 @@ dependencies = [
] ]
[[package]] [[package]]
name = "edera-sprout" name = "edera-sprout-boot"
version = "0.0.17" version = "0.0.23"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bitflags", "edera-sprout-build",
"image", "edera-sprout-config",
"edera-sprout-eficore",
"hex",
"log", "log",
"serde", "sha2",
"sha256",
"toml", "toml",
"uefi", "uefi",
"uefi-raw", "uefi-raw",
] ]
[[package]] [[package]]
name = "equivalent" name = "edera-sprout-build"
version = "1.0.2" version = "0.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]] [[package]]
name = "fdeflate" name = "edera-sprout-config"
version = "0.3.7" version = "0.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c"
dependencies = [ dependencies = [
"simd-adler32", "serde",
] ]
[[package]] [[package]]
name = "flate2" name = "edera-sprout-eficore"
version = "1.1.5" version = "0.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb"
dependencies = [ dependencies = [
"crc32fast", "anyhow",
"miniz_oxide", "bitflags",
"log",
"shlex",
"spin",
"uefi",
"uefi-raw",
] ]
[[package]] [[package]]
@@ -164,94 +114,33 @@ dependencies = [
"version_check", "version_check",
] ]
[[package]]
name = "hashbrown"
version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d"
[[package]] [[package]]
name = "hex" name = "hex"
version = "0.4.3" 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]]
name = "indexmap"
version = "2.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f"
dependencies = [
"equivalent",
"hashbrown",
]
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.177" version = "0.2.177"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976"
[[package]]
name = "lock_api"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
dependencies = [
"scopeguard",
]
[[package]] [[package]]
name = "log" name = "log"
version = "0.4.28" 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"
@@ -281,15 +170,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"
@@ -299,6 +179,12 @@ dependencies = [
"proc-macro2", "proc-macro2",
] ]
[[package]]
name = "scopeguard"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.228" version = "1.0.228"
@@ -350,21 +236,19 @@ dependencies = [
] ]
[[package]] [[package]]
name = "sha256" name = "shlex"
version = "1.6.0" version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f880fc8562bdeb709793f00eb42a2ad0e672c4f883bbe59122b926eca935c8f6" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
dependencies = [
"async-trait",
"bytes",
"hex",
"sha2",
]
[[package]] [[package]]
name = "simd-adler32" name = "spin"
version = "0.3.7" version = "0.10.0"
source = "git+https://github.com/edera-dev/sprout-patched-deps.git?rev=2c4fcc84b50d40c28f540d4271109ea0ca7e1268#2c4fcc84b50d40c28f540d4271109ea0ca7e1268" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591"
dependencies = [
"lock_api",
]
[[package]] [[package]]
name = "syn" name = "syn"
@@ -383,12 +267,10 @@ version = "0.9.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0dc8b1fb61449e27716ec0e1bdf0f6b8f3e8f6b05391e8497b8b6d7804ea6d8" checksum = "f0dc8b1fb61449e27716ec0e1bdf0f6b8f3e8f6b05391e8497b8b6d7804ea6d8"
dependencies = [ dependencies = [
"indexmap",
"serde_core", "serde_core",
"serde_spanned", "serde_spanned",
"toml_datetime", "toml_datetime",
"toml_parser", "toml_parser",
"toml_writer",
"winnow", "winnow",
] ]
@@ -410,12 +292,6 @@ dependencies = [
"winnow", "winnow",
] ]
[[package]]
name = "toml_writer"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df8b2b54733674ad286d16267dcfc7a71ed5c776e4ac7aa3c3e2561f7c637bf2"
[[package]] [[package]]
name = "typenum" name = "typenum"
version = "1.19.0" version = "1.19.0"
@@ -476,9 +352,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,46 +1,61 @@
[package] [workspace]
name = "edera-sprout" members = [
description = "Modern UEFI bootloader" "crates/boot",
"crates/build",
"crates/config",
"crates/eficore",
]
resolver = "3"
[workspace.package]
license = "Apache-2.0" license = "Apache-2.0"
version = "0.0.17" version = "0.0.23"
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"
bitflags = "2.10.0" bitflags = "2.10.0"
toml = "0.9.8"
log = "0.4.28" log = "0.4.28"
spin = "0.10.0"
uefi-raw = "0.12.0"
[dependencies.image] [workspace.dependencies.anyhow]
version = "0.25.8" version = "1.0.100"
default-features = false default-features = false
features = ["png"]
optional = true
[dependencies.serde] [workspace.dependencies.hex]
version = "0.4.3"
default-features = false
features = ["alloc"]
[workspace.dependencies.serde]
version = "1.0.228" version = "1.0.228"
features = ["derive"] default-features = false
features = ["alloc", "derive"]
[dependencies.sha256] [workspace.dependencies.sha2]
version = "1.6.0" version = "0.10.9"
default-features = false default-features = false
[dependencies.uefi] [workspace.dependencies.shlex]
version = "1.3.0"
default-features = false
[workspace.dependencies.toml]
version = "0.9.8"
default-features = false
features = ["serde", "parse"]
[workspace.dependencies.uefi]
version = "0.36.0" version = "0.36.0"
features = ["alloc", "logger"] default-features = false
features = ["alloc", "global_allocator", "panic_handler"]
[dependencies.uefi-raw] # Common build profiles
version = "0.12.0" # NOTE: We have to compile everything for opt-level = 2 due to optimization passes
[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]
@@ -57,15 +72,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

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

View File

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

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,23 @@ 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) |
| openSUSE | ✅ | [Setup Guide](./docs/setup/signed/opensuse.md) |
| Fedora | ❌ | [Setup Guide](./docs/setup/unsigned/fedora.md) |
| Alpine Edge | ❌ | [Setup Guide](./docs/setup/unsigned/alpine-edge.md) |
| Generic Linux | ❌ | [Setup Guide](./docs/setup/unsigned/generic-linux.md) |
| Windows | ❌ | [Setup Guide](./docs/setup/unsigned/windows.md) |
### Project Documentation
- [Development Guide] - [Development Guide]
- [Contributing Guide] - [Contributing Guide]
- [Sprout License] - [Sprout License]
@@ -49,8 +65,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 +75,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 +161,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

View File

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

26
crates/boot/Cargo.toml Normal file
View File

@@ -0,0 +1,26 @@
[package]
name = "edera-sprout-boot"
description = "Sprout: Modern UEFI Bootloader"
license.workspace = true
version.workspace = true
homepage.workspace = true
repository.workspace = true
edition.workspace = true
[dependencies]
anyhow.workspace = true
edera-sprout-config.path = "../config"
edera-sprout-eficore.path = "../eficore"
hex.workspace = true
sha2.workspace = true
toml.workspace = true
log.workspace = true
uefi.workspace = true
uefi-raw.workspace = true
[build-dependencies]
edera-sprout-build.path = "../build"
[[bin]]
name = "sprout"
path = "src/main.rs"

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

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

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

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

View File

@@ -1,7 +1,6 @@
use crate::context::SproutContext; use crate::context::SproutContext;
use alloc::rc::Rc;
use anyhow::{Context, Result, bail}; use anyhow::{Context, Result, bail};
use serde::{Deserialize, Serialize};
use std::rc::Rc;
/// EFI chainloader action. /// EFI chainloader action.
pub mod chainload; pub mod chainload;
@@ -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

@@ -1,41 +1,24 @@
use crate::context::SproutContext; use crate::context::SproutContext;
use crate::integrations::bootloader_interface::BootloaderInterface;
use crate::integrations::shim::{ShimInput, ShimSupport};
use crate::utils; use crate::utils;
use crate::utils::media_loader::MediaLoaderHandle; use alloc::boxed::Box;
use crate::utils::media_loader::constants::linux::LINUX_EFI_INITRD_MEDIA_GUID; use alloc::rc::Rc;
use anyhow::{Context, Result, bail}; use anyhow::{Context, Result, bail};
use edera_sprout_config::actions::chainload::ChainloadConfiguration;
use eficore::bootloader_interface::BootloaderInterface;
use eficore::media_loader::MediaLoaderHandle;
use eficore::media_loader::constants::linux::LINUX_EFI_INITRD_MEDIA_GUID;
use eficore::shim::{ShimInput, ShimSupport};
use log::error; use log::error;
use serde::{Deserialize, Serialize};
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.
let sprout_image = uefi::boot::image_handle(); let sprout_image = uefi::boot::image_handle();
// Resolve the path to the image to chainload. // Resolve the path to the image to chainload.
let resolved = utils::resolve_path( let resolved = eficore::path::resolve_path(
Some(context.root().loaded_image_path()?), Some(context.root().loaded_image_path()?),
&context.stamp(&configuration.path), &context.stamp(&configuration.path),
) )
@@ -53,30 +36,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.
@@ -90,9 +68,11 @@ pub fn chainload(context: Rc<SproutContext>, configuration: &ChainloadConfigurat
// If an initrd is provided, register it with the EFI stack. // If an initrd is provided, register it with the EFI stack.
let mut initrd_handle = None; let mut initrd_handle = None;
if let Some(linux_initrd) = initrd { if let Some(linux_initrd) = initrd {
let content = let content = eficore::path::read_file_contents(
utils::read_file_contents(Some(context.root().loaded_image_path()?), &linux_initrd) Some(context.root().loaded_image_path()?),
.context("unable to read linux initrd")?; &linux_initrd,
)
.context("unable to read linux initrd")?;
let handle = let handle =
MediaLoaderHandle::register(LINUX_EFI_INITRD_MEDIA_GUID, content.into_boxed_slice()) MediaLoaderHandle::register(LINUX_EFI_INITRD_MEDIA_GUID, content.into_boxed_slice())
.context("unable to register linux initrd")?; .context("unable to register linux initrd")?;
@@ -118,8 +98,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,43 +1,22 @@
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, };
media_loader::{ use alloc::rc::Rc;
MediaLoaderHandle, use alloc::string::{String, ToString};
constants::xen::{ use alloc::{format, vec};
XEN_EFI_CONFIG_MEDIA_GUID, XEN_EFI_KERNEL_MEDIA_GUID, XEN_EFI_RAMDISK_MEDIA_GUID, use anyhow::{Context, Result};
}, use edera_sprout_config::actions::chainload::ChainloadConfiguration;
}, use edera_sprout_config::actions::edera::EderaConfiguration;
use eficore::media_loader::{
MediaLoaderHandle,
constants::xen::{
XEN_EFI_CONFIG_MEDIA_GUID, XEN_EFI_KERNEL_MEDIA_GUID, XEN_EFI_RAMDISK_MEDIA_GUID,
}, },
}; };
use log::error;
/// The configuration of the edera action which boots the Edera hypervisor. use uefi::Guid;
/// 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>,
}
/// 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 {
@@ -98,8 +77,9 @@ fn register_media_loader_file(
// Stamp the path to the file. // Stamp the path to the file.
let path = context.stamp(path); let path = context.stamp(path);
// Read the file contents. // Read the file contents.
let content = utils::read_file_contents(Some(context.root().loaded_image_path()?), &path) let content =
.context(format!("unable to read {} file", what))?; eficore::path::read_file_contents(Some(context.root().loaded_image_path()?), &path)
.context(format!("unable to read {} file", what))?;
// Register the media loader. // Register the media loader.
let handle = MediaLoaderHandle::register(guid, content.into_boxed_slice()) let handle = MediaLoaderHandle::register(guid, content.into_boxed_slice())
.context(format!("unable to register {} media loader", what))?; .context(format!("unable to register {} media loader", what))?;

View File

@@ -1,16 +1,8 @@
use crate::context::SproutContext; use crate::context::SproutContext;
use alloc::rc::Rc;
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;
/// 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<()> {

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,13 @@
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 alloc::string::ToString;
use alloc::{format, vec};
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,13 +1,16 @@
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 crate::utils::vercmp;
use alloc::collections::BTreeMap;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use alloc::{format, vec};
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use std::collections::BTreeMap; 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 uefi::CString16; use uefi::CString16;
use uefi::fs::{FileSystem, Path, PathBuf}; use uefi::fs::{FileSystem, Path, PathBuf};
use uefi::proto::device_path::DevicePath; use uefi::proto::device_path::DevicePath;
@@ -27,6 +30,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 {
@@ -212,9 +227,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,11 @@
use crate::actions::ActionDeclaration;
use crate::actions::chainload::ChainloadConfiguration;
use crate::config::RootConfiguration;
use crate::entries::EntryDeclaration;
use crate::utils; use crate::utils;
use alloc::string::ToString;
use alloc::{format, vec};
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,10 +1,10 @@
use crate::config::{RootConfiguration, latest_version};
use crate::options::SproutOptions; use crate::options::SproutOptions;
use crate::platform::tpm::PlatformTpm; use alloc::vec::Vec;
use crate::utils;
use anyhow::{Context, Result, bail}; use anyhow::{Context, Result, bail};
use core::ops::Deref;
use edera_sprout_config::{RootConfiguration, latest_version};
use eficore::platform::tpm::PlatformTpm;
use log::info; use log::info;
use std::ops::Deref;
use toml::Value; use toml::Value;
use uefi::proto::device_path::LoadedImageDevicePath; use uefi::proto::device_path::LoadedImageDevicePath;
@@ -20,7 +20,7 @@ fn load_raw_config(options: &SproutOptions) -> Result<Vec<u8>> {
info!("configuration file: {}", options.config); info!("configuration file: {}", options.config);
// Read the contents of the sprout config file. // Read the contents of the sprout config file.
let content = utils::read_file_contents(Some(&path), &options.config) let content = eficore::path::read_file_contents(Some(&path), &options.config)
.context("unable to read sprout config file")?; .context("unable to read sprout config file")?;
// Measure the sprout.toml into the TPM, if needed and possible. // Measure the sprout.toml into the TPM, if needed and possible.

View File

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

View File

@@ -1,31 +1,21 @@
use crate::context::SproutContext; use crate::context::SproutContext;
use crate::integrations::shim::{ShimInput, ShimSupport}; use alloc::collections::BTreeMap;
use crate::utils; use alloc::format;
use alloc::rc::Rc;
use alloc::string::String;
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use edera_sprout_config::drivers::DriverDeclaration;
use eficore::shim::{ShimInput, ShimSupport};
use log::info; use log::info;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
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.
let sprout_image = uefi::boot::image_handle(); let sprout_image = uefi::boot::image_handle();
// Resolve the path to the driver image. // Resolve the path to the driver image.
let resolved = utils::resolve_path( let resolved = eficore::path::resolve_path(
Some(context.root().loaded_image_path()?), Some(context.root().loaded_image_path()?),
&context.stamp(&driver.path), &context.stamp(&driver.path),
) )

View File

@@ -1,24 +1,7 @@
use crate::context::SproutContext; use crate::context::SproutContext;
use serde::{Deserialize, Serialize}; use alloc::rc::Rc;
use std::collections::BTreeMap; use alloc::string::{String, ToString};
use std::rc::Rc; use edera_sprout_config::entries::EntryDeclaration;
/// 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)]

View File

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

View File

@@ -0,0 +1,34 @@
use crate::context::SproutContext;
use crate::entries::BootableEntry;
use alloc::rc::Rc;
use alloc::vec::Vec;
use anyhow::Result;
use anyhow::bail;
use edera_sprout_config::generators::GeneratorDeclaration;
/// 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,13 +1,15 @@
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::vercmp; use crate::utils::vercmp;
use alloc::format;
use alloc::rc::Rc;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use serde::{Deserialize, Serialize}; use core::cmp::Ordering;
use std::cmp::Ordering; use core::str::FromStr;
use std::rc::Rc; use edera_sprout_config::generators::bls::BlsConfiguration;
use std::str::FromStr;
use uefi::cstr16; use uefi::cstr16;
use uefi::fs::{FileSystem, PathBuf}; use uefi::fs::{FileSystem, PathBuf};
use uefi::proto::device_path::text::{AllowShortcuts, DisplayOnly}; use uefi::proto::device_path::text::{AllowShortcuts, DisplayOnly};
@@ -16,25 +18,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.
@@ -105,8 +88,9 @@ pub fn generate(context: Rc<SproutContext>, bls: &BlsConfiguration) -> Result<Ve
let path = context.stamp(&bls.path); let path = context.stamp(&bls.path);
// Resolve the path to the BLS directory. // Resolve the path to the BLS directory.
let bls_resolved = utils::resolve_path(Some(context.root().loaded_image_path()?), &path) let bls_resolved =
.context("unable to resolve bls path")?; eficore::path::resolve_path(Some(context.root().loaded_image_path()?), &path)
.context("unable to resolve bls path")?;
// Construct a filesystem path to the BLS entries directory. // Construct a filesystem path to the BLS entries directory.
let mut entries_path = PathBuf::from( let mut entries_path = PathBuf::from(

View File

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

View File

@@ -1,22 +1,10 @@
use crate::context::SproutContext; use crate::context::SproutContext;
use crate::entries::{BootableEntry, EntryDeclaration}; use crate::entries::BootableEntry;
use alloc::rc::Rc;
use alloc::string::ToString;
use alloc::vec::Vec;
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;
/// 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(

View File

@@ -1,23 +1,14 @@
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 alloc::collections::BTreeMap;
use alloc::rc::Rc;
use alloc::string::String;
use alloc::vec;
use alloc::vec::Vec;
use anyhow::Result; use anyhow::Result;
use serde::{Deserialize, Serialize}; use edera_sprout_config::generators::list::ListConfiguration;
use std::collections::BTreeMap; use edera_sprout_config::generators::matrix::MatrixConfiguration;
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"]}
@@ -61,7 +52,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

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

View File

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

View File

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

View File

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

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

@@ -0,0 +1,26 @@
use crate::actions;
use crate::context::SproutContext;
use alloc::format;
use alloc::rc::Rc;
use anyhow::{Context, Result};
use edera_sprout_config::phases::PhaseConfiguration;
/// 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(())
}

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

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

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

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

View File

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

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

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

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

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

View File

@@ -0,0 +1,6 @@
/// Define the SBAT attestation by including the sbat.csv file.
/// See this document for more details: https://github.com/rhboot/shim/blob/main/SBAT.md
/// NOTE: This data must be aligned by 512 bytes.
#[used]
#[unsafe(link_section = ".sbat")]
static SBAT: [u8; {size}] = *include_bytes!(concat!(env!("OUT_DIR"), "/sbat.out"));

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

@@ -0,0 +1,16 @@
[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
default-features = false
[lib]
name = "edera_sprout_config"
path = "src/lib.rs"

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,21 @@
use alloc::string::String;
use alloc::vec::Vec;
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,23 @@
use alloc::string::String;
use alloc::vec::Vec;
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,10 @@
use alloc::string::String;
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,13 @@
use alloc::string::String;
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,21 @@
use alloc::collections::BTreeMap;
use alloc::string::String;
use alloc::vec::Vec;
use serde::{Deserialize, Serialize};
/// 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,30 @@
use alloc::string::String;
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,22 @@
use crate::entries::EntryDeclaration;
use alloc::string::{String, ToString};
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,18 @@
use crate::entries::EntryDeclaration;
use alloc::collections::BTreeMap;
use alloc::string::String;
use alloc::vec::Vec;
use serde::{Deserialize, Serialize};
/// 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,18 @@
use crate::entries::EntryDeclaration;
use alloc::collections::BTreeMap;
use alloc::string::String;
use alloc::vec::Vec;
use serde::{Deserialize, Serialize};
/// 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,14 +1,24 @@
//! Sprout configuration descriptions.
//! This crate provides all the configuration structures for Sprout.
#![no_std]
extern crate alloc;
use crate::actions::ActionDeclaration; use crate::actions::ActionDeclaration;
use crate::drivers::DriverDeclaration; use crate::drivers::DriverDeclaration;
use crate::entries::EntryDeclaration; use crate::entries::EntryDeclaration;
use crate::extractors::ExtractorDeclaration; use crate::extractors::ExtractorDeclaration;
use crate::generators::GeneratorDeclaration; use crate::generators::GeneratorDeclaration;
use crate::phases::PhasesConfiguration; use crate::phases::PhasesConfiguration;
use alloc::collections::BTreeMap;
use alloc::string::String;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
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 +90,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,7 @@
use crate::actions; use alloc::collections::BTreeMap;
use crate::context::SproutContext; use alloc::string::String;
use anyhow::{Context, Result}; use alloc::vec::Vec;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
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 +30,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(())
}

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

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

View File

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

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

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

View File

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

View File

@@ -0,0 +1,26 @@
use anyhow::{Context, Result};
use uefi::boot::SearchType;
use uefi::{Guid, Handle};
use uefi_raw::Status;
/// Find a handle that provides the specified `protocol`.
pub fn find_handle(protocol: &Guid) -> Result<Option<Handle>> {
// Locate the requested protocol handle.
match uefi::boot::locate_handle_buffer(SearchType::ByProtocol(protocol)) {
// If a handle is found, the protocol is available.
Ok(handles) => Ok(if handles.is_empty() {
None
} else {
Some(handles[0])
}),
// If an error occurs, check if it is because the protocol is not available.
// If so, return false. Otherwise, return the error.
Err(error) => {
if error.status() == Status::NOT_FOUND {
Ok(None)
} else {
Err(error).context("unable to determine if the protocol is available")
}
}
}
}

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

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

View File

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

View File

@@ -1,5 +1,8 @@
use alloc::boxed::Box;
use alloc::vec::Vec;
use anyhow::{Context, Result, bail}; use anyhow::{Context, Result, bail};
use std::ffi::c_void; use core::ffi::c_void;
use core::ptr;
use uefi::proto::device_path::DevicePath; use uefi::proto::device_path::DevicePath;
use uefi::proto::device_path::build::DevicePathBuilder; use uefi::proto::device_path::build::DevicePathBuilder;
use uefi::proto::device_path::build::media::Vendor; use uefi::proto::device_path::build::media::Vendor;
@@ -261,8 +264,7 @@ impl MediaLoaderHandle {
let protocol = Box::from_raw(self.protocol); let protocol = Box::from_raw(self.protocol);
// Retrieve a box for the data we passed in. // Retrieve a box for the data we passed in.
let slice = let slice = ptr::slice_from_raw_parts_mut(protocol.address as *mut u8, protocol.length);
std::ptr::slice_from_raw_parts_mut(protocol.address as *mut u8, protocol.length);
let data = Box::from_raw(slice); let data = Box::from_raw(slice);
// Drop all the allocations explicitly, as we don't want to leak them. // Drop all the allocations explicitly, as we don't want to leak them.

View File

@@ -0,0 +1,55 @@
use anyhow::{Context, Result};
use uefi::Guid;
use uefi::proto::device_path::DevicePath;
use uefi::proto::media::partition::PartitionInfo;
use uefi_raw::Status;
/// Represents the type of partition GUID that can be retrieved.
#[derive(PartialEq, Eq)]
pub enum PartitionGuidForm {
/// The partition GUID is the unique partition GUID.
Partition,
/// The partition GUID is the partition type GUID.
PartitionType,
}
/// Retrieve the partition / partition type GUID of the device root `path`.
/// This only works on GPT partitions. If the root is not a GPT partition, None is returned.
/// If the GUID is all zeros, this will return None.
pub fn partition_guid(path: &DevicePath, form: PartitionGuidForm) -> Result<Option<Guid>> {
// Clone the path so we can pass it to the UEFI stack.
let path = path.to_boxed();
let result = uefi::boot::locate_device_path::<PartitionInfo>(&mut &*path);
let handle = match result {
Ok(handle) => Ok(Some(handle)),
Err(error) => {
// If the error is NOT_FOUND or UNSUPPORTED, we can return None.
// These are non-fatal errors.
if error.status() == Status::NOT_FOUND || error.status() == Status::UNSUPPORTED {
Ok(None)
} else {
Err(error)
}
}
}
.context("unable to locate device path")?;
// If we have the handle, we can try to open the partition info protocol.
if let Some(handle) = handle {
// Open the partition info protocol.
let partition_info = uefi::boot::open_protocol_exclusive::<PartitionInfo>(handle)
.context("unable to open partition info protocol")?;
// Find the unique partition GUID.
// If this is not a GPT partition, this will produce None.
Ok(partition_info
.gpt_partition_entry()
.map(|entry| match form {
// Match the form of the partition GUID.
PartitionGuidForm::Partition => entry.unique_partition_guid,
PartitionGuidForm::PartitionType => entry.partition_type_guid.0,
})
.filter(|guid| !guid.is_zero()))
} else {
Ok(None)
}
}

View File

@@ -1,25 +1,50 @@
use anyhow::{Context, Result, bail}; use alloc::borrow::ToOwned;
use std::ops::Deref; use alloc::boxed::Box;
use uefi::boot::SearchType; use alloc::string::{String, ToString};
use alloc::vec::Vec;
use anyhow::{Context, Result};
use core::ops::Deref;
use uefi::fs::{FileSystem, Path}; use uefi::fs::{FileSystem, Path};
use uefi::proto::device_path::text::{AllowShortcuts, DevicePathFromText, DisplayOnly}; use uefi::proto::device_path::text::{AllowShortcuts, DevicePathFromText, DisplayOnly};
use uefi::proto::device_path::{DevicePath, PoolDevicePath}; use uefi::proto::device_path::{DevicePath, PoolDevicePath};
use uefi::proto::media::fs::SimpleFileSystem; use uefi::proto::media::fs::SimpleFileSystem;
use uefi::proto::media::partition::PartitionInfo; use uefi::{CString16, Handle};
use uefi::{CString16, Guid, Handle};
use uefi_raw::Status;
/// Support code for the EFI framebuffer. /// Represents the components of a resolved path.
pub mod framebuffer; pub struct ResolvedPath {
/// The root path of the resolved path. This is the device itself.
/// For example, "PciRoot(0x0)/Pci(0x4,0x0)/NVMe(0x1,00-00-00-00-00-00-00-00)/HD(1,MBR,0xBE1AFDFA,0x3F,0xFBFC1)/"
pub root_path: Box<DevicePath>,
/// The subpath of the resolved path. This is the path to the file.
/// For example, "\EFI\BOOT\BOOTX64.efi"
pub sub_path: Box<DevicePath>,
/// The full path of the resolved path. This is the safest path to use.
/// For example, "PciRoot(0x0)/Pci(0x4,0x0)/NVMe(0x1,00-00-00-00-00-00-00-00)/HD(1,MBR,0xBE1AFDFA,0x3F,0xFBFC1)/\EFI\BOOT\BOOTX64.efi"
pub full_path: Box<DevicePath>,
/// The handle of the filesystem containing the path.
/// This can be used to acquire a [SimpleFileSystem] protocol to read the file.
pub filesystem_handle: Handle,
}
/// Support code for the media loader protocol. impl ResolvedPath {
pub mod media_loader; /// Read the file specified by this path into a buffer and return it.
pub fn read_file(&self) -> Result<Vec<u8>> {
let fs = uefi::boot::open_protocol_exclusive::<SimpleFileSystem>(self.filesystem_handle)
.context("unable to open filesystem protocol")?;
let mut fs = FileSystem::new(fs);
let path = self
.sub_path
.to_string(DisplayOnly(false), AllowShortcuts(false))?;
let content = fs.read(Path::new(&path));
content.context("unable to read file contents")
}
}
/// Support code for EFI variables. /// Checks if a [CString16] contains a char `c`.
pub mod variables; /// We need to call to_string() because CString16 doesn't support `contains` with a char.
fn cstring16_contains_char(string: &CString16, c: char) -> bool {
/// Implements a version comparison algorithm according to the BLS specification. string.to_string().contains(c)
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.
@@ -36,12 +61,6 @@ pub fn text_to_device_path(path: &str) -> Result<PoolDevicePath> {
.context("unable to convert text to device path") .context("unable to convert text to device path")
} }
/// Checks if a [CString16] contains a char `c`.
/// We need to call to_string() because CString16 doesn't support `contains` with a char.
fn cstring16_contains_char(string: &CString16, c: char) -> bool {
string.to_string().contains(c)
}
/// Grabs the root part of the `path`. /// Grabs the root part of the `path`.
/// For example, given "PciRoot(0x0)/Pci(0x4,0x0)/NVMe(0x1,00-00-00-00-00-00-00-00)/HD(1,MBR,0xBE1AFDFA,0x3F,0xFBFC1)/\EFI\BOOT\BOOTX64.efi" /// For example, given "PciRoot(0x0)/Pci(0x4,0x0)/NVMe(0x1,00-00-00-00-00-00-00-00)/HD(1,MBR,0xBE1AFDFA,0x3F,0xFBFC1)/\EFI\BOOT\BOOTX64.efi"
/// it will give "PciRoot(0x0)/Pci(0x4,0x0)/NVMe(0x1,00-00-00-00-00-00-00-00)/HD(1,MBR,0xBE1AFDFA,0x3F,0xFBFC1)" /// it will give "PciRoot(0x0)/Pci(0x4,0x0)/NVMe(0x1,00-00-00-00-00-00-00-00)/HD(1,MBR,0xBE1AFDFA,0x3F,0xFBFC1)"
@@ -91,36 +110,6 @@ pub fn device_path_subpath(path: &DevicePath) -> Result<String> {
Ok(path) Ok(path)
} }
/// Represents the components of a resolved path.
pub struct ResolvedPath {
/// The root path of the resolved path. This is the device itself.
/// For example, "PciRoot(0x0)/Pci(0x4,0x0)/NVMe(0x1,00-00-00-00-00-00-00-00)/HD(1,MBR,0xBE1AFDFA,0x3F,0xFBFC1)/"
pub root_path: Box<DevicePath>,
/// The subpath of the resolved path. This is the path to the file.
/// For example, "\EFI\BOOT\BOOTX64.efi"
pub sub_path: Box<DevicePath>,
/// The full path of the resolved path. This is the safest path to use.
/// For example, "PciRoot(0x0)/Pci(0x4,0x0)/NVMe(0x1,00-00-00-00-00-00-00-00)/HD(1,MBR,0xBE1AFDFA,0x3F,0xFBFC1)/\EFI\BOOT\BOOTX64.efi"
pub full_path: Box<DevicePath>,
/// The handle of the filesystem containing the path.
/// This can be used to acquire a [SimpleFileSystem] protocol to read the file.
pub filesystem_handle: Handle,
}
impl ResolvedPath {
/// Read the file specified by this path into a buffer and return it.
pub fn read_file(&self) -> Result<Vec<u8>> {
let fs = uefi::boot::open_protocol_exclusive::<SimpleFileSystem>(self.filesystem_handle)
.context("unable to open filesystem protocol")?;
let mut fs = FileSystem::new(fs);
let path = self
.sub_path
.to_string(DisplayOnly(false), AllowShortcuts(false))?;
let content = fs.read(Path::new(&path));
content.context("unable to read file contents")
}
}
/// Resolve a path specified by `input` to its various components. /// Resolve a path specified by `input` to its various components.
/// Uses `default_root_path` as the base root if one is not specified in the path. /// Uses `default_root_path` as the base root if one is not specified in the path.
/// Returns [ResolvedPath] which contains the resolved components. /// Returns [ResolvedPath] which contains the resolved components.
@@ -183,114 +172,3 @@ pub fn read_file_contents(default_root_path: Option<&DevicePath>, input: &str) -
let resolved = resolve_path(default_root_path, input)?; let resolved = resolve_path(default_root_path, input)?;
resolved.read_file() resolved.read_file()
} }
/// Filter a string-like Option `input` such that an empty string is [None].
pub fn empty_is_none<T: AsRef<str>>(input: Option<T>) -> Option<T> {
input.filter(|input| !input.as_ref().is_empty())
}
/// Combine a sequence of strings into a single string, separated by spaces, ignoring empty strings.
pub fn combine_options<T: AsRef<str>>(options: impl Iterator<Item = T>) -> String {
options
.flat_map(|item| empty_is_none(Some(item)))
.map(|item| item.as_ref().to_string())
.collect::<Vec<_>>()
.join(" ")
}
/// Produce a unique hash for the input.
/// This uses SHA-256, which is unique enough but relatively short.
pub fn unique_hash(input: &str) -> String {
sha256::digest(input.as_bytes())
}
/// Represents the type of partition GUID that can be retrieved.
#[derive(PartialEq, Eq)]
pub enum PartitionGuidForm {
/// The partition GUID is the unique partition GUID.
Partition,
/// The partition GUID is the partition type GUID.
PartitionType,
}
/// Retrieve the partition / partition type GUID of the device root `path`.
/// This only works on GPT partitions. If the root is not a GPT partition, None is returned.
/// If the GUID is all zeros, this will return None.
pub fn partition_guid(path: &DevicePath, form: PartitionGuidForm) -> Result<Option<Guid>> {
// Clone the path so we can pass it to the UEFI stack.
let path = path.to_boxed();
let result = uefi::boot::locate_device_path::<PartitionInfo>(&mut &*path);
let handle = match result {
Ok(handle) => Ok(Some(handle)),
Err(error) => {
// If the error is NOT_FOUND or UNSUPPORTED, we can return None.
// These are non-fatal errors.
if error.status() == Status::NOT_FOUND || error.status() == Status::UNSUPPORTED {
Ok(None)
} else {
Err(error)
}
}
}
.context("unable to locate device path")?;
// If we have the handle, we can try to open the partition info protocol.
if let Some(handle) = handle {
// Open the partition info protocol.
let partition_info = uefi::boot::open_protocol_exclusive::<PartitionInfo>(handle)
.context("unable to open partition info protocol")?;
// Find the unique partition GUID.
// If this is not a GPT partition, this will produce None.
Ok(partition_info
.gpt_partition_entry()
.map(|entry| match form {
// Match the form of the partition GUID.
PartitionGuidForm::Partition => entry.unique_partition_guid,
PartitionGuidForm::PartitionType => entry.partition_type_guid.0,
})
.filter(|guid| !guid.is_zero()))
} else {
Ok(None)
}
}
/// Find a handle that provides the specified `protocol`.
pub fn find_handle(protocol: &Guid) -> Result<Option<Handle>> {
// Locate the requested protocol handle.
match uefi::boot::locate_handle_buffer(SearchType::ByProtocol(protocol)) {
// If a handle is found, the protocol is available.
Ok(handles) => Ok(if handles.is_empty() {
None
} else {
Some(handles[0])
}),
// If an error occurs, check if it is because the protocol is not available.
// If so, return false. Otherwise, return the error.
Err(error) => {
if error.status() == Status::NOT_FOUND {
Ok(None)
} else {
Err(error).context("unable to determine if the protocol is available")
}
}
}
}
/// Convert a byte slice into a CString16.
pub fn utf16_bytes_to_cstring16(bytes: &[u8]) -> Result<CString16> {
// Validate the input bytes are the right length.
if !bytes.len().is_multiple_of(2) {
bail!("utf16 bytes must be a multiple of 2");
}
// Convert the bytes to UTF-16 data.
let data = bytes
// Chunk everything into two bytes.
.chunks_exact(2)
// Reinterpret the bytes as u16 little-endian.
.map(|chunk| u16::from_le_bytes([chunk[0], chunk[1]]))
// Collect the result into a vector.
.collect::<Vec<_>>();
CString16::try_from(data).context("unable to convert utf16 bytes to CString16")
}

View File

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

View File

@@ -1,7 +1,7 @@
// Referenced https://github.com/sheroz/tick_counter (MIT license) as a baseline. // Referenced https://github.com/sheroz/tick_counter (MIT license) as a baseline.
// Architecturally modified to support UEFI and remove x86 (32-bit) support. // Architecturally modified to support UEFI and remove x86 (32-bit) support.
use std::time::Duration; use core::time::Duration;
/// Support for aarch64 timers. /// Support for aarch64 timers.
#[cfg(target_arch = "aarch64")] #[cfg(target_arch = "aarch64")]
@@ -17,7 +17,7 @@ pub enum TickFrequency {
/// The platform provides the tick frequency. /// The platform provides the tick frequency.
Hardware(u64), Hardware(u64),
/// The tick frequency is measured internally. /// The tick frequency is measured internally.
Measured(u64, Duration), Measured(u64),
} }
impl TickFrequency { impl TickFrequency {
@@ -25,7 +25,7 @@ impl TickFrequency {
fn ticks(&self) -> u64 { fn ticks(&self) -> u64 {
match self { match self {
TickFrequency::Hardware(frequency) => *frequency, TickFrequency::Hardware(frequency) => *frequency,
TickFrequency::Measured(frequency, _) => *frequency, TickFrequency::Measured(frequency) => *frequency,
} }
} }

View File

@@ -1,5 +1,5 @@
use crate::platform::timer::TickFrequency; use crate::platform::timer::TickFrequency;
use std::arch::asm; use core::arch::asm;
/// Reads the cntvct_el0 counter and returns the value. /// Reads the cntvct_el0 counter and returns the value.
pub fn ticks() -> u64 { pub fn ticks() -> u64 {
@@ -10,16 +10,6 @@ pub fn ticks() -> u64 {
counter counter
} }
/// We can use the actual ticks value as our start value.
pub fn start() -> u64 {
ticks()
}
/// We can use the actual ticks value as our stop value.
pub fn stop() -> u64 {
ticks()
}
/// Our frequency is provided by cntfrq_el0 on the platform. /// Our frequency is provided by cntfrq_el0 on the platform.
pub fn frequency() -> TickFrequency { pub fn frequency() -> TickFrequency {
let frequency: u64; let frequency: u64;

View File

@@ -0,0 +1,29 @@
use crate::platform::timer::TickFrequency;
use core::time::Duration;
/// We will measure the frequency of the timer based on 1000 microseconds.
/// This will result in a call to BS->Stall(1000) in the end.
const MEASURE_FREQUENCY_DURATION: Duration = Duration::from_micros(1000);
/// Read the number of ticks from the platform timer.
pub fn ticks() -> u64 {
// SAFETY: Reads the platform timer, which is safe in any context.
unsafe { core::arch::x86_64::_rdtsc() }
}
/// Measure the frequency of the platform timer.
/// NOTE: Intentionally, we do not synchronize rdtsc during measurement to match systemd behavior.
fn measure_frequency() -> u64 {
let start = ticks();
uefi::boot::stall(MEASURE_FREQUENCY_DURATION);
let stop = ticks();
let elapsed = stop.wrapping_sub(start) as f64;
(elapsed / MEASURE_FREQUENCY_DURATION.as_secs_f64()) as u64
}
/// Acquire the platform timer frequency.
/// On x86_64, this is slightly expensive, so it should be done once.
pub fn frequency() -> TickFrequency {
let frequency = measure_frequency();
TickFrequency::Measured(frequency)
}

View File

@@ -1,4 +1,3 @@
use crate::utils;
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use uefi::ResultExt; use uefi::ResultExt;
use uefi::boot::ScopedProtocol; use uefi::boot::ScopedProtocol;
@@ -43,8 +42,8 @@ impl PlatformTpm {
/// Returns None if TPM is not available. /// Returns None if TPM is not available.
fn protocol() -> Result<Option<TpmProtocolHandle>> { fn protocol() -> Result<Option<TpmProtocolHandle>> {
// Attempt to acquire the TCG2 protocol handle. If it's not available, return None. // Attempt to acquire the TCG2 protocol handle. If it's not available, return None.
let Some(handle) = let Some(handle) = crate::handle::find_handle(&Tcg2Protocol::GUID)
utils::find_handle(&Tcg2Protocol::GUID).context("unable to determine tpm presence")? .context("unable to determine tpm presence")?
else { else {
return Ok(None); return Ok(None);
}; };
@@ -85,7 +84,9 @@ impl PlatformTpm {
}; };
// Check if the TPM supports `GetActivePcrBanks`, and if it doesn't return zero. // Check if the TPM supports `GetActivePcrBanks`, and if it doesn't return zero.
if handle.version().major < 1 || handle.version().major == 1 && handle.version().minor < 1 { if (handle.version().major < 1)
|| (handle.version().major == 1 && (handle.version().minor < 1))
{
return Ok(0); return Ok(0);
} }

View File

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

View File

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

View File

@@ -1,12 +1,14 @@
use crate::integrations::shim::hook::SecurityHook; use crate::path::ResolvedPath;
use crate::secure::SecureBoot; use crate::secure::SecureBoot;
use crate::utils; use crate::shim::hook::SecurityHook;
use crate::utils::ResolvedPath; use crate::variables::{VariableClass, VariableController};
use crate::utils::variables::{VariableClass, VariableController}; use alloc::boxed::Box;
use alloc::string::ToString;
use alloc::vec::Vec;
use anyhow::{Context, Result, anyhow, bail}; use anyhow::{Context, Result, anyhow, bail};
use core::ffi::c_void;
use core::pin::Pin;
use log::warn; use log::warn;
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};
@@ -87,7 +89,7 @@ impl<'a> ShimInput<'a> {
let path = path let path = path
.to_string(DisplayOnly(false), AllowShortcuts(false)) .to_string(DisplayOnly(false), AllowShortcuts(false))
.context("unable to convert device path to string")?; .context("unable to convert device path to string")?;
let path = utils::resolve_path(None, &path.to_string()) let path = crate::path::resolve_path(None, &path.to_string())
.context("unable to resolve path")?; .context("unable to resolve path")?;
// Read the file path. // Read the file path.
let data = path.read_file()?; let data = path.read_file()?;
@@ -160,14 +162,14 @@ impl ShimSupport {
/// Determines whether the shim is loaded. /// Determines whether the shim is loaded.
pub fn loaded() -> Result<bool> { pub fn loaded() -> Result<bool> {
Ok(utils::find_handle(&Self::SHIM_LOCK_GUID) Ok(crate::handle::find_handle(&Self::SHIM_LOCK_GUID)
.context("unable to find shim lock protocol")? .context("unable to find shim lock protocol")?
.is_some()) .is_some())
} }
/// Determines whether the shim loader is available. /// Determines whether the shim loader is available.
pub fn loader_available() -> Result<bool> { pub fn loader_available() -> Result<bool> {
Ok(utils::find_handle(&Self::SHIM_IMAGE_LOADER_GUID) Ok(crate::handle::find_handle(&Self::SHIM_IMAGE_LOADER_GUID)
.context("unable to find shim image loader protocol")? .context("unable to find shim image loader protocol")?
.is_some()) .is_some())
} }
@@ -175,7 +177,7 @@ impl ShimSupport {
/// Use the shim to validate the `input`, returning [ShimVerificationOutput] when complete. /// Use the shim to validate the `input`, returning [ShimVerificationOutput] when complete.
pub fn verify(input: ShimInput) -> Result<ShimVerificationOutput> { pub fn verify(input: ShimInput) -> Result<ShimVerificationOutput> {
// Acquire the handle to the shim lock protocol. // Acquire the handle to the shim lock protocol.
let handle = utils::find_handle(&Self::SHIM_LOCK_GUID) let handle = crate::handle::find_handle(&Self::SHIM_LOCK_GUID)
.context("unable to find shim lock protocol")? .context("unable to find shim lock protocol")?
.ok_or_else(|| anyhow!("unable to find shim lock protocol"))?; .ok_or_else(|| anyhow!("unable to find shim lock protocol"))?;
// Acquire the protocol exclusively to the shim lock. // Acquire the protocol exclusively to the shim lock.

View File

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

View File

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

View File

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

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

@@ -0,0 +1,144 @@
# 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)
- 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.

View File

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

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

@@ -0,0 +1,143 @@
# 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)
## 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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 MiB

View File

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

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,4 +1,4 @@
[toolchain] [toolchain]
channel = "nightly" channel = "1.91.0"
components = ["rustfmt", "rust-std", "clippy"] components = ["rustfmt", "clippy"]
targets = ["x86_64-unknown-uefi", "aarch64-unknown-uefi"] targets = ["x86_64-unknown-uefi", "aarch64-unknown-uefi"]

View File

@@ -1,166 +0,0 @@
use crate::context::SproutContext;
use crate::utils::framebuffer::Framebuffer;
use crate::utils::read_file_contents;
use anyhow::{Context, Result, bail};
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);
// If the image is zero-sized, then we should bail with an error.
if fit.width == 0 || fit.height == 0 {
bail!("calculated frame size is zero");
}
// 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,4 +0,0 @@
/// Implements support for the bootloader interface specification.
pub mod bootloader_interface;
/// Implements support for the shim loader application for Secure Boot.
pub mod shim;

View File

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

View File

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

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