96 Commits

Author SHA1 Message Date
4129ae4c0e sprout: version 0.0.16 2025-11-01 02:28:51 -04:00
7a7fcc71c0 fix(variables): set bool should have variable class parameter 2025-11-01 02:26:53 -04:00
812036fada chore(doc): fix incorrect comment on list generator 2025-11-01 02:22:10 -04:00
7f122b088e chore(context): add documentation to the stamping algorithm 2025-11-01 02:20:59 -04:00
5217dd0538 chore(doc): update readme 2025-11-01 02:05:08 -04:00
b94c684d52 fix(autoconfigure/linux): remove debug line 2025-11-01 02:01:09 -04:00
fd2e9df3f1 fix(autoconfigure): detect kernel and initramfs case-insensitive, even at the root 2025-11-01 01:58:55 -04:00
f49bbed0d5 fix(utils): for safety, ensure that the root path is not modifiable by the uefi stack 2025-11-01 01:20:45 -04:00
b0081ef9f3 chore(options): fix incorrect comment about values 2025-11-01 01:11:02 -04:00
d9c0dc915d chore(sbat): add note about alignment of sbat.csv 2025-11-01 01:10:27 -04:00
0bee93b607 fix(shim): handle hook uninstallation more gracefully 2025-11-01 01:07:37 -04:00
eace74a6b0 fix(tpm): correct comment about the format of the description data 2025-11-01 00:54:51 -04:00
d1936f7db4 fix(sbat): add newline to template 2025-10-31 15:50:01 -04:00
4866961d2f feat(secure-boot): add support for SBAT section 2025-10-31 15:49:00 -04:00
bbc8f58352 fix(shim): retain the protocol if the shim is loaded at all 2025-10-31 14:56:26 -04:00
b3424fcd8f fix(tpm): correctly write the log name, and change the sprout configuration event name 2025-10-31 02:45:15 -04:00
afc650f944 feat(tpm): implement basic measurement of the bootloader configuration 2025-10-31 02:35:58 -04:00
81cf331158 feat(tpm): initial tpm support code, we just tell systemd about the pcr banks right now 2025-10-31 01:30:07 -04:00
6602e1d69e fix(bootloader-interface): use the correct uefi revision and firmware revision format 2025-10-30 23:58:07 -04:00
7bd93f5aa0 fix(platform/timer): ensure the x86_64 frequency measurement uses wrapping subtraction 2025-10-30 23:51:20 -04:00
f897addc3c fix(filesystem-device-match): has-partition-type-uuid should fetch the partition type guid 2025-10-30 23:48:48 -04:00
8241d6d774 fix(shim/hook): create an immutable slice for the buffer instead of a mutable one 2025-10-30 23:45:08 -04:00
c3e883c121 fix(utils): when retrieving the partition guid, if the guid is zero, return none 2025-10-30 23:42:47 -04:00
f69d4b942b fix(platform/timer): use wrapping subtraction to measure duration of a timer 2025-10-30 23:40:52 -04:00
c1a672afcb fix(bootloader-interface): report the correct firmware revision 2025-10-30 23:25:48 -04:00
a2f017ba30 fix(variables): add null terminator to the end of strings written into variables 2025-10-30 23:15:18 -04:00
0368a170a8 Merge pull request #25 from edera-dev/azenla/shim-support
feat(boot): basic support for secure boot via shim
2025-10-30 23:04:55 -04:00
f593f5a601 feat(boot): basic support for secure boot via shim protocol 2025-10-30 22:56:01 -04:00
92f611e9a8 feat(shim): initial shim support 2025-10-30 21:38:49 -04:00
20932695e3 feat(safety): bail if secure boot is enabled early 2025-10-30 18:57:26 -04:00
40e2d1baef fix(bootloader-interface): autoconfigure should produce auto-* entries to match spec 2025-10-30 15:31:26 -04:00
94caf123ae chore(main): add constant for delay on error 2025-10-30 15:26:44 -04:00
b9b34394cd sprout: version 0.0.15 2025-10-30 15:01:49 -04:00
3c467fef65 Merge pull request #22 from edera-dev/dependabot/docker/docker-updates-d0b0844295
chore(deps): bump rustlang/rust from `7cba2ed` to `3453212` in the docker-updates group
2025-10-30 14:58:36 -04:00
cfe7088970 Merge pull request #23 from edera-dev/azenla/bootloader-interface
feat(integrations): basic bootloader interface support
2025-10-30 14:58:04 -04:00
9d3a022e08 feat(bootloader-interface): add support for marking when the menu is being display 2025-10-30 13:27:58 -04:00
cc90199d61 feat(bootloader-interface): identify ourselves as sprout 2025-10-30 12:50:36 -04:00
cff55322fc feat(bootloader-interface): implement support for LoaderImageIdentifier 2025-10-30 12:44:07 -04:00
d2bef03437 fix(platform/timer): add back note of platform timer reference 2025-10-30 12:30:08 -04:00
37ab0406bb feat(bootloader-interface): implement support for UEFI firmware information 2025-10-30 11:47:35 -04:00
a77be3c282 feat(bootloader-interface): measure time in firmware as well 2025-10-30 02:51:52 -04:00
87d608366f feat(bootloader-interface): add support for loader boot times 2025-10-30 02:36:14 -04:00
e7d2438e5f feat(bls): basic support for boot loader interface 2025-10-28 23:23:12 -04:00
f82d24a206 feat(integrations): implement initial bootloader interface touchpoints 2025-10-28 21:05:22 -04:00
fe714cc411 fix(filesystem-device-match): bail early if no criteria is provided 2025-10-28 17:10:28 -04:00
000e3ea6c7 chore(github): disable blank issues 2025-10-28 16:56:56 -04:00
44de1bebd3 chore(github): add a feature request template 2025-10-28 16:56:06 -04:00
4d62ac2ce2 chore(github): show an ask a question link when creating an issue 2025-10-28 16:52:22 -04:00
dependabot[bot]
8d87fcfd2d chore(deps): bump rustlang/rust in the docker-updates group
Bumps the docker-updates group with 1 update: rustlang/rust.


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

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-28 20:50:21 +00:00
1a2b103675 chore(github): make all workflows (except release) concurrent by sha 2025-10-28 16:49:26 -04:00
5e1bc4659b chore(github): rename all files from yaml => yml 2025-10-28 16:47:25 -04:00
27c82d24d0 chore(github): update bug report issue template 2025-10-28 16:44:54 -04:00
e2aa19174e chore(github): update bug report template to reorder items 2025-10-28 16:43:41 -04:00
fb342e12cb chore(github): initial issue templates 2025-10-28 16:35:12 -04:00
6f60a279c3 sprout: version 0.0.14 2025-10-28 01:47:15 -04:00
2e66d8c72e chore(docs): update readme with secure boot notes and roadmap items 2025-10-28 01:43:07 -04:00
86e08c2400 fix(doc/extractors/filesystem-device-match): the extractor will error if no criteria is provided 2025-10-28 00:19:38 -04:00
852823e2eb chore(doc/bls/entry): clarify why char::is_whitespace is used despite newline matching 2025-10-28 00:12:16 -04:00
734ab84054 chore(doc/context): clarify context finalization limit error message 2025-10-28 00:10:22 -04:00
c8a3408fdd fix(extractors/filesystem-device-match): clarify when to use fallback for empty criteria 2025-10-28 00:09:11 -04:00
deeda650a7 fix(autoconfigure/linux): remove debug line 2025-10-28 00:06:02 -04:00
268a2cb28b fix(media-loader): improve safety in the event protocol interface install fails 2025-10-27 23:56:12 -04:00
0b6523906d fix(doc): filesystem-device-match will not return a filesystem when criteria is not provided 2025-10-27 23:39:55 -04:00
3acd0ec7d8 chore(doc): document media loader safety 2025-10-27 23:24:35 -04:00
fe593efa8c chore(autoconfigure/docs): clarify why we append / to a device root 2025-10-27 23:15:14 -04:00
3058abab23 fix(menu): check for timeout duration overflow 2025-10-27 23:10:05 -04:00
5df717de6d chore(filesystem-device-match): extract partition uuid fetch to function 2025-10-27 23:05:57 -04:00
011e133455 chore(autoconfigure-linux): clarify variable shadowing for initramfs matching 2025-10-27 23:00:55 -04:00
ccd1a8f498 chore(menu): clarify that we do not need to free the key event 2025-10-27 22:59:00 -04:00
527ce4b1b4 sprout: version 0.0.13 2025-10-27 22:44:21 -04:00
ebd3c07bf5 fix(autoconfigure): reinject values after configuration changes 2025-10-27 22:43:37 -04:00
e8b7b967fa chore(docs): change windows setup guide to use autoconfiguration 2025-10-27 21:36:48 -04:00
2bf4013938 feat(autoconfigure): improved linux support and windows support 2025-10-27 19:47:21 -04:00
6819e55e23 Merge pull request #19 from edera-dev/dependabot/docker/docker-updates-d0b0844295
chore(deps): bump rustlang/rust from `141e9a7` to `7cba2ed` in the docker-updates group
2025-10-27 19:03:00 -04:00
50f7bc11aa sprout: version 0.0.12 2025-10-27 18:41:32 -04:00
2200ba74f6 fix(cargo): force dev profiles to use opt-level = 2 to workaround hardware acceleration 2025-10-27 18:35:18 -04:00
7a3db08e1d fix(cargo): remove transitive dependency on tokio 2025-10-27 18:26:53 -04:00
e7f5be30dd feat(autoconfigure): generate names using a unique hash 2025-10-27 18:21:28 -04:00
3bbe6561ef fix(docs): fedora setup guide should use [options] 2025-10-27 17:57:29 -04:00
3b5e110d1e feat(config): rename [defaults] to [options] 2025-10-27 17:56:38 -04:00
26315fb4c4 fix(options): stamp initrd and combine options safely by ignoring empty strings 2025-10-27 17:44:30 -04:00
a76f9770dc fix(chainload): support an empty initrd path, which will result in no initrd 2025-10-27 16:27:39 -04:00
59edd63a12 fix(doc): list generator is not the matrix generator 2025-10-27 16:17:33 -04:00
8a2e8c8127 fix(sprout): correct rustdoc and clarify safety in some places 2025-10-27 16:16:09 -04:00
6086778dc0 fix(menu): free timer event to avoid leak 2025-10-27 16:03:25 -04:00
e729d6a60b feat(sprout): cleanup default logging 2025-10-27 15:44:29 -04:00
d6e8fe0245 feat(autoconfigure): find vmlinuz and initramfs pairs with linux autoconfigure module 2025-10-27 15:41:29 -04:00
99653b5192 fix(menu): hide the entry name from the menu since it can be long with autoconfigure 2025-10-27 12:25:22 -04:00
dependabot[bot]
3ffda86544 chore(deps): bump rustlang/rust in the docker-updates group
Bumps the docker-updates group with 1 update: rustlang/rust.


Updates `rustlang/rust` from `141e9a7` to `7cba2ed`

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-27 12:48:29 +00:00
2a76e4f798 chore(code): move bls autoconfigure to separate module 2025-10-27 04:28:25 -04:00
a10a5cb342 sprout: version 0.0.11 2025-10-27 04:00:37 -04:00
fdc5f0e1d2 chore(docs): mention autoconfiguration support for bls and use it in the docs 2025-10-27 03:52:39 -04:00
f60cf4b365 Merge pull request #18 from edera-dev/azenla/autoconfigure
Autoconfiguration Support
2025-10-27 03:48:51 -04:00
0ca9ff4fec fix(bls-autoconfigure): generate a unique chainload action for each filesystem 2025-10-27 03:45:10 -04:00
1799419bfa fix(autoconfigure): apply the actions properly in the root 2025-10-27 03:37:09 -04:00
facd2000a5 feat(autoconfigure): initial attempt at bls autoconfiguration 2025-10-27 02:38:40 -04:00
67 changed files with 2587 additions and 319 deletions

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

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

115
Cargo.lock generated
View File

@@ -14,6 +14,17 @@ 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]] [[package]]
name = "autocfg" name = "autocfg"
version = "1.5.0" version = "1.5.0"
@@ -32,6 +43,15 @@ version = "2.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
[[package]]
name = "block-buffer"
version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
dependencies = [
"generic-array",
]
[[package]] [[package]]
name = "bytemuck" name = "bytemuck"
version = "1.24.0" version = "1.24.0"
@@ -44,12 +64,27 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" 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"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "cpufeatures"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "crc32fast" name = "crc32fast"
version = "1.5.0" version = "1.5.0"
@@ -59,14 +94,35 @@ dependencies = [
"cfg-if", "cfg-if",
] ]
[[package]]
name = "crypto-common"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
dependencies = [
"generic-array",
"typenum",
]
[[package]]
name = "digest"
version = "0.10.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
"block-buffer",
"crypto-common",
]
[[package]] [[package]]
name = "edera-sprout" name = "edera-sprout"
version = "0.0.10" version = "0.0.16"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"image", "image",
"log", "log",
"serde", "serde",
"sha256",
"toml", "toml",
"uefi", "uefi",
"uefi-raw", "uefi-raw",
@@ -97,12 +153,28 @@ dependencies = [
"miniz_oxide", "miniz_oxide",
] ]
[[package]]
name = "generic-array"
version = "0.14.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2"
dependencies = [
"typenum",
"version_check",
]
[[package]] [[package]]
name = "hashbrown" name = "hashbrown"
version = "0.16.0" version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d"
[[package]]
name = "hex"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]] [[package]]
name = "image" name = "image"
version = "0.25.8" version = "0.25.8"
@@ -126,6 +198,12 @@ dependencies = [
"hashbrown", "hashbrown",
] ]
[[package]]
name = "libc"
version = "0.2.177"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976"
[[package]] [[package]]
name = "log" name = "log"
version = "0.4.28" version = "0.4.28"
@@ -259,6 +337,29 @@ dependencies = [
"serde_core", "serde_core",
] ]
[[package]]
name = "sha2"
version = "0.10.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
dependencies = [
"cfg-if",
"cpufeatures",
"digest",
]
[[package]]
name = "sha256"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f880fc8562bdeb709793f00eb42a2ad0e672c4f883bbe59122b926eca935c8f6"
dependencies = [
"async-trait",
"bytes",
"hex",
"sha2",
]
[[package]] [[package]]
name = "simd-adler32" name = "simd-adler32"
version = "0.3.7" version = "0.3.7"
@@ -314,6 +415,12 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df8b2b54733674ad286d16267dcfc7a71ed5c776e4ac7aa3c3e2561f7c637bf2" checksum = "df8b2b54733674ad286d16267dcfc7a71ed5c776e4ac7aa3c3e2561f7c637bf2"
[[package]]
name = "typenum"
version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
[[package]] [[package]]
name = "ucs2" name = "ucs2"
version = "0.3.3" version = "0.3.3"
@@ -372,6 +479,12 @@ version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "462eeb75aeb73aea900253ce739c8e18a67423fadf006037cd3ff27e82748a06" checksum = "462eeb75aeb73aea900253ce739c8e18a67423fadf006037cd3ff27e82748a06"
[[package]]
name = "version_check"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]] [[package]]
name = "winnow" name = "winnow"
version = "0.7.13" version = "0.7.13"

View File

@@ -2,7 +2,7 @@
name = "edera-sprout" name = "edera-sprout"
description = "Modern UEFI bootloader" description = "Modern UEFI bootloader"
license = "Apache-2.0" license = "Apache-2.0"
version = "0.0.10" version = "0.0.16"
homepage = "https://sprout.edera.dev" homepage = "https://sprout.edera.dev"
repository = "https://github.com/edera-dev/sprout" repository = "https://github.com/edera-dev/sprout"
edition = "2024" edition = "2024"
@@ -13,7 +13,7 @@ toml = "0.9.8"
log = "0.4.28" log = "0.4.28"
[dependencies.image] [dependencies.image]
version = "0.25.6" version = "0.25.8"
default-features = false default-features = false
features = ["png"] features = ["png"]
optional = true optional = true
@@ -22,6 +22,10 @@ optional = true
version = "1.0.228" version = "1.0.228"
features = ["derive"] features = ["derive"]
[dependencies.sha256]
version = "1.6.0"
default-features = false
[dependencies.uefi] [dependencies.uefi]
version = "0.36.0" version = "0.36.0"
features = ["alloc", "logger"] features = ["alloc", "logger"]
@@ -33,6 +37,11 @@ version = "0.12.0"
default = ["splash"] default = ["splash"]
splash = ["dep:image"] 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.
opt-level = 2
[profile.release] [profile.release]
lto = "thin" lto = "thin"
strip = "symbols" strip = "symbols"
@@ -46,6 +55,7 @@ debug = 1
inherits = "dev" inherits = "dev"
strip = "debuginfo" strip = "debuginfo"
debug = 0 debug = 0
opt-level = 2
[patch.crates-io.simd-adler32] [patch.crates-io.simd-adler32]
git = "https://github.com/edera-dev/sprout-patched-deps.git" git = "https://github.com/edera-dev/sprout-patched-deps.git"

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:141e9a7f13f77237dd4d462364c3a1b21cb8a6791d8924c409573e77b788af5e AS build FROM --platform=$BUILDPLATFORM rustlang/rust:nightly-alpine@sha256:34532121803db17008af0cdc4e2e1210466cb257cc9d3840dac42d706640fee5 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

@@ -6,11 +6,11 @@
</div> </div>
Sprout is an **EXPERIMENTAL** programmable UEFI bootloader written in Rust. Sprout is a programmable UEFI bootloader written in Rust.
Sprout is in use at Edera today in development environments and is intended to ship to production soon. It is in use at Edera today in development environments and is intended to ship to production soon.
The name "sprout" is derived from our company name "Edera" which means "ivy." The name "Sprout" is derived from our company name "Edera" which means "ivy."
Given that Sprout is the first thing intended to start on an Edera system, the name was apt. Given that Sprout is the first thing intended to start on an Edera system, the name was apt.
It supports `x86_64` and `ARM64` EFI-capable systems. It is designed to require UEFI and can be chainloaded from an It supports `x86_64` and `ARM64` EFI-capable systems. It is designed to require UEFI and can be chainloaded from an
@@ -21,10 +21,10 @@ Sprout is licensed under Apache 2.0 and is open to modifications and contributio
## Background ## Background
At [Edera] we make compute isolation technology for a wide variety of environments, often ones we do not fully control. At [Edera] we make compute isolation technology for a wide variety of environments, often ones we do not fully control.
Our technology utilizes a hypervisor to boot the host system to provide a new isolation mechanism that works Our technology uses a hypervisor to boot the host system to provide a new isolation mechanism that works
with or without hardware virtualization support. To do this we need to inject our hypervisor at boot time. with or without hardware virtualization support. To do this, we need to inject our hypervisor at boot time.
Unfortunately, GRUB, the most common bootloader on Linux systems today, utilizes a shell-script like Unfortunately, GRUB, the most common bootloader on Linux systems today, uses a shell-script like
configuration system. Both the code that runs to generate a GRUB config and the GRUB config configuration system. Both the code that runs to generate a GRUB config and the GRUB config
itself is fully turing complete. This makes modifying boot configuration difficult and error-prone. itself is fully turing complete. This makes modifying boot configuration difficult and error-prone.
@@ -49,30 +49,33 @@ simplify installation and usage.
## Features ## Features
NOTE: Currently, Sprout is experimental and is not intended for production use. **NOTE**: Sprout is still in beta.
The boot menu mechanism is very rudimentary.
### Current ### Current
- [x] Loadable driver support - [x] Loadable driver support
- [x] [Bootloader specification (BLS)](https://uapi-group.org/specifications/specs/boot_loader_specification/) support - [x] Basic [Bootloader specification (BLS)](https://uapi-group.org/specifications/specs/boot_loader_specification/) support
- [x] Chainload support - [x] Chainload support
- [x] Linux boot support via EFI stub - [x] Linux boot support via EFI stub
- [x] Windows boot support via chainload - [x] Windows boot support via chainload
- [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] [Secure Boot support](https://github.com/edera-dev/sprout/issues/20): partial
### Roadmap ### Roadmap
- [ ] Full-featured boot menu - [ ] [Bootloader interface support](https://github.com/edera-dev/sprout/issues/21)
- [ ] Secure Boot support: work in progress - [ ] [BLS specification conformance](https://github.com/edera-dev/sprout/issues/2)
- [ ] UKI support: partial - [ ] [Full-featured boot menu](https://github.com/edera-dev/sprout/issues/1)
- [ ] multiboot2 support - [ ] [UKI support](https://github.com/edera-dev/sprout/issues/6): partial
- [ ] Linux boot protocol (boot without EFI stub) - [ ] [multiboot2 support](https://github.com/edera-dev/sprout/issues/7)
- [ ] [Linux boot protocol (boot without EFI stub)](https://github.com/edera-dev/sprout/issues/7)
## Concepts ## Concepts
- drivers: loadable EFI modules that can add functionality to the EFI system. - drivers: loadable EFI modules that can add functionality to the EFI system.
- autoconfiguration: code that can automatically generate sprout.toml based on the EFI environment.
- actions: executable code with a configuration that can be run by various other sprout concepts. - actions: executable code with a configuration that can be run by various other sprout concepts.
- generators: code that can generate boot entries based on inputs or runtime code. - generators: code that can generate boot entries based on inputs or runtime code.
- extractors: code that can extract values from the EFI environment. - extractors: code that can extract values from the EFI environment.
@@ -100,6 +103,8 @@ Sprout supports some command line options that can be combined to modify behavio
$ sprout.efi --config=\path\to\config.toml $ sprout.efi --config=\path\to\config.toml
# Boot a specific entry, bypassing the menu. # Boot a specific entry, bypassing the menu.
$ sprout.efi --boot="Boot Xen" $ sprout.efi --boot="Boot Xen"
# Autoconfigure Sprout, without loading a configuration file.
$ sprout.efi --autoconfigure
``` ```
### Boot Linux from ESP ### Boot Linux from ESP
@@ -134,27 +139,11 @@ version = 1
[drivers.ext4] [drivers.ext4]
path = "\\sprout\\drivers\\ext4.efi" path = "\\sprout\\drivers\\ext4.efi"
# extract the full path of the first filesystem # global options.
# that contains \loader as a directory [options]
# into the value called "boot" # enable autoconfiguration by detecting bls enabled
[extractors.boot.filesystem-device-match] # filesystems and generating boot entries for them.
has-item = "\\loader" autoconfigure = true
# use the sprout bls module to scan a bls
# directory for entries and load them as boot
# entries in sprout, using the entry template
# as specified here. the bls action below will
# be passed the extracted values from bls.
[generators.boot.bls]
path = "$boot\\loader"
entry.title = "$title"
entry.actions = ["bls"]
# the action that is used for each bls entry above.
[actions.bls]
chainload.path = "$boot\\$chainload"
chainload.options = ["$options"]
chainload.linux-initrd = "$boot\\$initrd"
``` ```
[Edera]: https://edera.dev [Edera]: https://edera.dev

57
build.rs Normal file
View File

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

View File

@@ -39,27 +39,11 @@ version = 1
[drivers.ext4] [drivers.ext4]
path = "\\sprout\\drivers\\ext4.efi" path = "\\sprout\\drivers\\ext4.efi"
# extract the full path of the first filesystem # global options.
# that contains \loader as a directory [options]
# into the value called "boot" # enable autoconfiguration by detecting bls enabled
[extractors.boot.filesystem-device-match] # filesystems and generating boot entries for them.
has-item = "\\loader" autoconfigure = true
# use the sprout bls module to scan a bls
# directory for entries and load them as boot
# entries in sprout, using the entry template
# as specified here. the bls action below will
# be passed the extracted values from bls.
[generators.boot.bls]
path = "$boot\\loader"
entry.title = "$title"
entry.actions = ["bls"]
# the action that is used for each bls entry above.
[actions.bls]
chainload.path = "$boot\\$chainload"
chainload.options = ["$options"]
chainload.linux-initrd = "$boot\\$initrd"
``` ```
## Step 3, Option 1: Configure GRUB to load Sprout (recommended) ## Step 3, Option 1: Configure GRUB to load Sprout (recommended)

View File

@@ -2,7 +2,7 @@
## Prerequisites ## Prerequisites
- Secure Boot disabled - Secure Boot is disabled or configured to allow Sprout
- UEFI Windows installation - UEFI Windows installation
## Step 1: Base Installation ## Step 1: Base Installation
@@ -33,15 +33,10 @@ Write the following file to `X:\sprout.toml`:
# sprout configuration: version 1 # sprout configuration: version 1
version = 1 version = 1
# add a boot entry for booting Windows # global options.
# which will run the boot-windows action. [options]
[entries.windows] # enable autoconfiguration to detect Windows.
title = "Windows" autoconfigure = true
actions = ["boot-windows"]
# use the chainload action to boot the Windows bootloader.
[actions.boot-windows]
chainload.path = "\\EFI\\Microsoft\\Boot\\bootmgfw.efi"
``` ```
## Step 4: Configure EFI Firmware to boot Sprout ## Step 4: Configure EFI Firmware to boot Sprout

View File

@@ -65,13 +65,8 @@ set -- "${@}" \
-drive "if=pflash,file=${FINAL_DIR}/ovmf-boot.fd,format=raw,readonly=on" \ -drive "if=pflash,file=${FINAL_DIR}/ovmf-boot.fd,format=raw,readonly=on" \
-device nvme,drive=disk1,serial=cafebabe -device nvme,drive=disk1,serial=cafebabe
if [ "${DISK_BOOT}" = "1" ]; then set -- "${@}" \
set -- "${@}" \ -drive "if=none,file=${FINAL_DIR}/sprout.img,format=raw,id=disk1,readonly=on"
-drive "if=none,file=${FINAL_DIR}/sprout.img,format=raw,id=disk1,readonly=on"
else
set -- "${@}" \
-drive "if=none,file=fat:rw:${FINAL_DIR}/efi,format=raw,id=disk1"
fi
set -- "${@}" -name "sprout ${TARGET_ARCH}" set -- "${@}" -name "sprout ${TARGET_ARCH}"

View File

@@ -7,12 +7,13 @@ RUN export DEBIAN_FRONTEND=noninteractive && apt-get update && apt-get install -
WORKDIR /work WORKDIR /work
COPY sprout.efi /work/${EFI_NAME}.EFI COPY sprout.efi /work/${EFI_NAME}.EFI
COPY sprout.toml /work/SPROUT.TOML COPY sprout.toml /work/SPROUT.TOML
COPY kernel.efi /work/KERNEL.EFI COPY kernel.efi /work/VMLINUZ
COPY shell.efi /work/SHELL.EFI COPY shell.efi /work/SHELL.EFI
COPY xen.efi /work/XEN.EFI COPY xen.efi /work/XEN.EFI
COPY xen.cfg /work/XEN.CFG COPY xen.cfg /work/XEN.CFG
COPY initramfs /work/INITRAMFS COPY initramfs /work/INITRAMFS
COPY edera-splash.png /work/EDERA-SPLASH.PNG COPY edera-splash.png /work/EDERA-SPLASH.PNG
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 && \
parted --script sprout.img mkpart primary fat32 1MiB 100% > /dev/null 2>&1 && \ parted --script sprout.img mkpart primary fat32 1MiB 100% > /dev/null 2>&1 && \
@@ -20,14 +21,17 @@ RUN truncate -s128MiB sprout.img && \
mkfs.vfat -F32 -n EFI sprout.img && \ mkfs.vfat -F32 -n EFI sprout.img && \
mmd -i sprout.img ::/EFI && \ mmd -i sprout.img ::/EFI && \
mmd -i sprout.img ::/EFI/BOOT && \ mmd -i sprout.img ::/EFI/BOOT && \
mmd -i sprout.img ::/LOADER && \
mmd -i sprout.img ::/LOADER/ENTRIES && \
mcopy -i sprout.img ${EFI_NAME}.EFI ::/EFI/BOOT/ && \ mcopy -i sprout.img ${EFI_NAME}.EFI ::/EFI/BOOT/ && \
mcopy -i sprout.img KERNEL.EFI ::/EFI/BOOT/ && \ mcopy -i sprout.img VMLINUZ ::/VMLINUZ && \
mcopy -i sprout.img SHELL.EFI ::/EFI/BOOT/ && \ mcopy -i sprout.img SHELL.EFI ::/EFI/BOOT/ && \
mcopy -i sprout.img XEN.EFI ::/EFI/BOOT/ && \ mcopy -i sprout.img XEN.EFI ::/EFI/BOOT/ && \
mcopy -i sprout.img XEN.CFG ::/EFI/BOOT/ && \ mcopy -i sprout.img XEN.CFG ::/EFI/BOOT/ && \
mcopy -i sprout.img SPROUT.TOML ::/ && \ mcopy -i sprout.img SPROUT.TOML ::/ && \
mcopy -i sprout.img EDERA-SPLASH.PNG ::/ && \ 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/ && \
mv sprout.img /sprout.img mv sprout.img /sprout.img
FROM scratch AS final FROM scratch AS final

View File

@@ -108,6 +108,7 @@ 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/assets/edera-splash.png" "${FINAL_DIR}/edera-splash.png"
cp "hack/dev/configs/bls.conf" "${FINAL_DIR}/bls.conf"
mkdir -p "${FINAL_DIR}/efi/EFI/BOOT" mkdir -p "${FINAL_DIR}/efi/EFI/BOOT"
cp "${FINAL_DIR}/sprout.efi" "${FINAL_DIR}/efi/EFI/BOOT/${EFI_NAME}.EFI" cp "${FINAL_DIR}/sprout.efi" "${FINAL_DIR}/efi/EFI/BOOT/${EFI_NAME}.EFI"

View File

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

View File

@@ -0,0 +1,4 @@
version = 1
[options]
autoconfigure = true

View File

@@ -0,0 +1,4 @@
title Boot Linux
linux /vmlinuz
options console=hvc0
initrd /initramfs

View File

@@ -1,7 +1,7 @@
version = 1 version = 1
[defaults] [options]
entry = "edera" default-entry = "edera"
menu-timeout = 0 menu-timeout = 0
[extractors.boot.filesystem-device-match] [extractors.boot.filesystem-device-match]

View File

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

View File

@@ -1,7 +1,7 @@
version = 1 version = 1
[defaults] [options]
entry = "shell" default-entry = "shell"
menu-timeout = 0 menu-timeout = 0
[extractors.boot.filesystem-device-match] [extractors.boot.filesystem-device-match]

View File

@@ -1,7 +1,7 @@
version = 1 version = 1
[defaults] [options]
entry = "xen" default-entry = "xen"
menu-timeout = 0 menu-timeout = 0
[extractors.boot.filesystem-device-match] [extractors.boot.filesystem-device-match]

View File

@@ -19,7 +19,7 @@ pub mod splash;
/// that you can specify via other concepts. /// that you can specify via other concepts.
/// ///
/// Actions are the main work that Sprout gets done, like booting Linux. /// Actions are the main work that Sprout gets done, like booting Linux.
#[derive(Serialize, Deserialize, Default, Clone)] #[derive(Serialize, Deserialize, Debug, Default, Clone)]
pub struct ActionDeclaration { pub struct ActionDeclaration {
/// Chainload to another EFI application. /// Chainload to another EFI application.
/// This allows you to load any EFI application, either to boot an operating system /// This allows you to load any EFI application, either to boot an operating system

View File

@@ -1,16 +1,18 @@
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 crate::utils::media_loader::MediaLoaderHandle;
use crate::utils::media_loader::constants::linux::LINUX_EFI_INITRD_MEDIA_GUID; use crate::utils::media_loader::constants::linux::LINUX_EFI_INITRD_MEDIA_GUID;
use anyhow::{Context, Result, bail}; use anyhow::{Context, Result, bail};
use log::{error, info}; use log::error;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::rc::Rc; use std::rc::Rc;
use uefi::CString16; use uefi::CString16;
use uefi::proto::loaded_image::LoadedImage; use uefi::proto::loaded_image::LoadedImage;
/// The configuration of the chainload action. /// The configuration of the chainload action.
#[derive(Serialize, Deserialize, Default, Clone)] #[derive(Serialize, Deserialize, Debug, Default, Clone)]
pub struct ChainloadConfiguration { pub struct ChainloadConfiguration {
/// The path to the image to chainload. /// The path to the image to chainload.
/// This can be a Linux EFI stub (vmlinuz usually) or a standard EFI executable. /// This can be a Linux EFI stub (vmlinuz usually) or a standard EFI executable.
@@ -34,37 +36,28 @@ pub fn chainload(context: Rc<SproutContext>, configuration: &ChainloadConfigurat
// Resolve the path to the image to chainload. // Resolve the path to the image to chainload.
let resolved = utils::resolve_path( let resolved = utils::resolve_path(
context.root().loaded_image_path()?, Some(context.root().loaded_image_path()?),
&context.stamp(&configuration.path), &context.stamp(&configuration.path),
) )
.context("unable to resolve chainload path")?; .context("unable to resolve chainload path")?;
// Load the image to chainload. // Load the image to chainload using the shim support integration.
let image = uefi::boot::load_image( // It will determine if the image needs to be loaded via the shim or can be loaded directly.
sprout_image, let image = ShimSupport::load(sprout_image, ShimInput::ResolvedPath(&resolved))?;
uefi::boot::LoadImageSource::FromDevicePath {
device_path: &resolved.full_path,
boot_policy: uefi::proto::BootPolicy::ExactMatch,
},
)
.context("unable to load image")?;
// Open the LoadedImage protocol of the image to chainload. // Open the LoadedImage protocol of the image to chainload.
let mut loaded_image_protocol = uefi::boot::open_protocol_exclusive::<LoadedImage>(image) let mut loaded_image_protocol = uefi::boot::open_protocol_exclusive::<LoadedImage>(image)
.context("unable to open loaded image protocol")?; .context("unable to open loaded image protocol")?;
// Stamp and concatenate the options to pass to the image. // Stamp and combine the options to pass to the image.
let options = configuration let options =
.options utils::combine_options(configuration.options.iter().map(|item| context.stamp(item)));
.iter()
.map(|item| context.stamp(item))
.collect::<Vec<_>>()
.join(" ");
// Pass the options to the image, if any are provided. // Pass the options to the image, if any are provided.
// The holder must drop at the end of this function to ensure the options are not leaked, // The holder must drop at the end of this function to ensure the options are not leaked,
// and the holder here ensures it outlives the if block here, as a pointer has to be // and the holder here ensures it outlives the if block here, as a pointer has to be
// passed to the image. This has been hand-validated to be safe. // passed to the image.
// SAFETY: The options outlive the usage of the image, and the image is not used after this.
let mut options_holder: Option<Box<CString16>> = None; let mut options_holder: Option<Box<CString16>> = None;
if !options.is_empty() { if !options.is_empty() {
let options = Box::new( let options = Box::new(
@@ -72,8 +65,6 @@ pub fn chainload(context: Rc<SproutContext>, configuration: &ChainloadConfigurat
.context("unable to convert chainloader options to CString16")?, .context("unable to convert chainloader options to CString16")?,
); );
info!("options: {}", options);
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");
} }
@@ -88,26 +79,35 @@ pub fn chainload(context: Rc<SproutContext>, configuration: &ChainloadConfigurat
options_holder = Some(options); options_holder = Some(options);
} }
// Stamp the initrd path, if provided.
let initrd = configuration
.linux_initrd
.as_ref()
.map(|item| context.stamp(item));
// The initrd can be None or empty, so we need to collapse that into a single Option.
let initrd = utils::empty_is_none(initrd);
// If an initrd is provided, register it with the EFI stack.
let mut initrd_handle = None; let mut initrd_handle = None;
if let Some(ref linux_initrd) = configuration.linux_initrd { if let Some(linux_initrd) = initrd {
let initrd_path = context.stamp(linux_initrd); let content =
let content = utils::read_file_contents(context.root().loaded_image_path()?, &initrd_path) utils::read_file_contents(Some(context.root().loaded_image_path()?), &linux_initrd)
.context("unable to read 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")?;
initrd_handle = Some(handle); initrd_handle = Some(handle);
} }
// Retrieve the base and size of the loaded image to display. // Mark execution of an entry in the bootloader interface.
let (base, size) = loaded_image_protocol.info(); BootloaderInterface::mark_exec(context.root().timer())
info!("loaded image: base={:#x} size={:#x}", base.addr(), size); .context("unable to mark execution of boot entry in bootloader interface")?;
// Start the loaded image. // Start the loaded image.
// This call might return, or it may pass full control to another image that will never return. // This call might return, or it may pass full control to another image that will never return.
// Capture the result to ensure we can return an error if the image fails to start, but only // Capture the result to ensure we can return an error if the image fails to start, but only
// after the optional initrd has been unregistered. // after the optional initrd has been unregistered.
let result = uefi::boot::start_image(image).context("unable to start image"); let result = uefi::boot::start_image(image);
// Unregister the initrd if it was registered. // Unregister the initrd if it was registered.
if let Some(initrd_handle) = initrd_handle if let Some(initrd_handle) = initrd_handle

View File

@@ -22,7 +22,7 @@ use crate::{
/// The configuration of the edera action which boots the Edera hypervisor. /// The configuration of the edera action which boots the Edera hypervisor.
/// Edera is based on Xen but modified significantly with a Rust stack. /// 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. /// Sprout is a component of the Edera stack and provides the boot functionality of Xen.
#[derive(Serialize, Deserialize, Default, Clone)] #[derive(Serialize, Deserialize, Debug, Default, Clone)]
pub struct EderaConfiguration { pub struct EderaConfiguration {
/// The path to the Xen hypervisor EFI image. /// The path to the Xen hypervisor EFI image.
pub xen: String, pub xen: String,
@@ -40,7 +40,23 @@ pub struct EderaConfiguration {
} }
/// 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(configuration: &EderaConfiguration) -> String { fn build_xen_config(context: Rc<SproutContext>, configuration: &EderaConfiguration) -> String {
// Stamp xen options and combine them.
let xen_options = utils::combine_options(
configuration
.xen_options
.iter()
.map(|item| context.stamp(item)),
);
// Stamp kernel options and combine them.
let kernel_options = utils::combine_options(
configuration
.kernel_options
.iter()
.map(|item| context.stamp(item)),
);
// xen config file format is ini-like // xen config file format is ini-like
[ [
// global section // global section
@@ -50,10 +66,10 @@ fn build_xen_config(configuration: &EderaConfiguration) -> String {
// configuration section for sprout // configuration section for sprout
"[sprout]".to_string(), "[sprout]".to_string(),
// xen options // xen options
format!("options={}", configuration.xen_options.join(" ")), format!("options={}", xen_options),
// kernel options, stub replaces the kernel path // kernel options, stub replaces the kernel path
// the kernel is provided via media loader // the kernel is provided via media loader
format!("kernel=stub {}", configuration.kernel_options.join(" ")), format!("kernel=stub {}", kernel_options),
// required or else the last line will be ignored // required or else the last line will be ignored
"".to_string(), "".to_string(),
] ]
@@ -82,7 +98,7 @@ fn register_media_loader_file(
// Stamp the path to the file. // Stamp the path to the file.
let path = context.stamp(path); let path = context.stamp(path);
// Read the file contents. // Read the file contents.
let content = utils::read_file_contents(context.root().loaded_image_path()?, &path) let content = utils::read_file_contents(Some(context.root().loaded_image_path()?), &path)
.context(format!("unable to read {} file", what))?; .context(format!("unable to read {} file", what))?;
// Register the media loader. // Register the media loader.
let handle = MediaLoaderHandle::register(guid, content.into_boxed_slice()) let handle = MediaLoaderHandle::register(guid, content.into_boxed_slice())
@@ -94,7 +110,7 @@ fn register_media_loader_file(
/// `configuration` and `context`. This action uses Edera-specific Xen EFI stub functionality. /// `configuration` and `context`. This action uses Edera-specific Xen EFI stub functionality.
pub fn edera(context: Rc<SproutContext>, configuration: &EderaConfiguration) -> Result<()> { pub fn edera(context: Rc<SproutContext>, configuration: &EderaConfiguration) -> Result<()> {
// Build the Xen config file content for this configuration. // Build the Xen config file content for this configuration.
let config = build_xen_config(configuration); let config = build_xen_config(context.clone(), configuration);
// Register the media loader for the config. // Register the media loader for the config.
let config = register_media_loader_text(XEN_EFI_CONFIG_MEDIA_GUID, "config", config) let config = register_media_loader_text(XEN_EFI_CONFIG_MEDIA_GUID, "config", config)
@@ -113,7 +129,7 @@ pub fn edera(context: Rc<SproutContext>, configuration: &EderaConfiguration) ->
let mut media_loaders = vec![config, kernel]; let mut media_loaders = vec![config, kernel];
// Register the initrd if it is provided. // Register the initrd if it is provided.
if let Some(ref initrd) = configuration.initrd { if let Some(initrd) = utils::empty_is_none(configuration.initrd.as_ref()) {
let initrd = let initrd =
register_media_loader_file(&context, XEN_EFI_RAMDISK_MEDIA_GUID, "initrd", initrd) register_media_loader_file(&context, XEN_EFI_RAMDISK_MEDIA_GUID, "initrd", initrd)
.context("unable to register initrd media loader")?; .context("unable to register initrd media loader")?;
@@ -131,7 +147,7 @@ pub fn edera(context: Rc<SproutContext>, configuration: &EderaConfiguration) ->
) )
.context("unable to chainload to xen"); .context("unable to chainload to xen");
// Unregister the media loaders on error. // Unregister the media loaders when an error happens.
for media_loader in media_loaders { for media_loader in media_loaders {
if let Err(error) = media_loader.unregister() { if let Err(error) = media_loader.unregister() {
error!("unable to unregister media loader: {}", error); error!("unable to unregister media loader: {}", error);

View File

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

View File

@@ -12,10 +12,12 @@ use std::time::Duration;
use uefi::boot::ScopedProtocol; use uefi::boot::ScopedProtocol;
use uefi::proto::console::gop::GraphicsOutput; use uefi::proto::console::gop::GraphicsOutput;
/// We set the default splash time to zero, as this makes it so any logging shows up
/// on top of the splash and does not hold up the boot process.
const DEFAULT_SPLASH_TIME: u32 = 0; const DEFAULT_SPLASH_TIME: u32 = 0;
/// The configuration of the splash action. /// The configuration of the splash action.
#[derive(Serialize, Deserialize, Default, Clone)] #[derive(Serialize, Deserialize, Debug, Default, Clone)]
pub struct SplashConfiguration { pub struct SplashConfiguration {
/// The path to the image to display. /// The path to the image to display.
/// Currently, only PNG images are supported. /// Currently, only PNG images are supported.
@@ -143,7 +145,7 @@ pub fn splash(context: Rc<SproutContext>, configuration: &SplashConfiguration) -
// Stamp the image path value. // Stamp the image path value.
let image = context.stamp(&configuration.image); let image = context.stamp(&configuration.image);
// Read the image contents. // Read the image contents.
let image = read_file_contents(context.root().loaded_image_path()?, &image)?; let image = read_file_contents(Some(context.root().loaded_image_path()?), &image)?;
// Decode the image as a PNG. // Decode the image as a PNG.
let image = ImageReader::with_format(Cursor::new(image), ImageFormat::Png) let image = ImageReader::with_format(Cursor::new(image), ImageFormat::Png)
.decode() .decode()

57
src/autoconfigure.rs Normal file
View File

@@ -0,0 +1,57 @@
use crate::config::RootConfiguration;
use anyhow::{Context, Result};
use uefi::fs::FileSystem;
use uefi::proto::device_path::DevicePath;
use uefi::proto::media::fs::SimpleFileSystem;
/// bls: autodetect and configure BLS-enabled filesystems.
pub mod bls;
/// linux: autodetect and configure Linux kernels.
/// This autoconfiguration module should not be activated
/// on BLS-enabled filesystems as it may make duplicate entries.
pub mod linux;
/// windows: autodetect and configure Windows boot configurations.
pub mod windows;
/// Generate a [RootConfiguration] based on the environment.
/// Intakes a `config` to use as the basis of the autoconfiguration.
pub fn autoconfigure(config: &mut RootConfiguration) -> Result<()> {
// Find all the filesystems that are on the system.
let filesystem_handles =
uefi::boot::find_handles::<SimpleFileSystem>().context("unable to scan filesystems")?;
// For each filesystem that was detected, scan it for supported autoconfig mechanisms.
for handle in filesystem_handles {
// Acquire the device path root for the filesystem.
let root = {
uefi::boot::open_protocol_exclusive::<DevicePath>(handle)
.context("unable to get root for filesystem")?
.to_boxed()
};
// Open the filesystem that was detected.
let filesystem = uefi::boot::open_protocol_exclusive::<SimpleFileSystem>(handle)
.context("unable to open filesystem")?;
// Trade the filesystem protocol for the uefi filesystem helper.
let mut filesystem = FileSystem::new(filesystem);
// Scan the filesystem for BLS supported configurations.
let bls_found = bls::scan(&mut filesystem, &root, config)
.context("unable to scan for bls configurations")?;
// If BLS was not found, scan for Linux configurations.
if !bls_found {
linux::scan(&mut filesystem, &root, config)
.context("unable to scan for linux configurations")?;
}
// Always look for Windows configurations.
windows::scan(&mut filesystem, &root, config)
.context("unable to scan for windows configurations")?;
}
Ok(())
}

101
src/autoconfigure/bls.rs Normal file
View File

@@ -0,0 +1,101 @@
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 anyhow::{Context, Result};
use uefi::cstr16;
use uefi::fs::{FileSystem, Path};
use uefi::proto::device_path::DevicePath;
use uefi::proto::device_path::text::{AllowShortcuts, DisplayOnly};
/// The name prefix of the BLS chainload action that will be used
/// by the BLS generator to chainload entries.
const BLS_CHAINLOAD_ACTION_PREFIX: &str = "bls-chainload-";
/// Scan the specified `filesystem` for BLS configurations.
pub fn scan(
filesystem: &mut FileSystem,
root: &DevicePath,
config: &mut RootConfiguration,
) -> Result<bool> {
// BLS has a loader.conf file that can specify its own auto-entries mechanism.
let bls_loader_conf_path = Path::new(cstr16!("\\loader\\loader.conf"));
// BLS also has an entries directory that can specify explicit entries.
let bls_entries_path = Path::new(cstr16!("\\loader\\entries"));
// Convert the device path root to a string we can use in the configuration.
let mut root = root
.to_string(DisplayOnly(false), AllowShortcuts(false))
.context("unable to convert device root to string")?
.to_string();
// Add a trailing forward-slash to the root to ensure the device root is completed.
root.push('/');
// Generate a unique hash of the root path.
let root_unique_hash = utils::unique_hash(&root);
// Whether we have a loader.conf file.
let has_loader_conf = filesystem
.try_exists(bls_loader_conf_path)
.context("unable to check for BLS loader.conf file")?;
// Whether we have an entries directory.
// We actually iterate the entries to see if there are any.
let has_entries_dir = filesystem
.read_dir(bls_entries_path)
.ok()
.and_then(|mut iterator| iterator.next())
.map(|entry| entry.is_ok())
.unwrap_or(false);
// Detect if a BLS supported configuration is on this filesystem.
// We check both loader.conf and entries directory as only one of them is required.
if !(has_loader_conf || has_entries_dir) {
return Ok(false);
}
// Generate a unique name for the BLS chainload action.
let chainload_action_name = format!("{}{}", BLS_CHAINLOAD_ACTION_PREFIX, root_unique_hash,);
// BLS is now detected, generate a configuration for it.
let generator = BlsConfiguration {
entry: EntryDeclaration {
title: "$title".to_string(),
actions: vec![chainload_action_name.clone()],
..Default::default()
},
path: format!("{}\\loader", root),
};
// Generate a unique name for the BLS generator and insert the generator into the configuration.
config.generators.insert(
format!("auto-bls-{}", root_unique_hash),
GeneratorDeclaration {
bls: Some(generator),
..Default::default()
},
);
// Generate a chainload configuration for BLS.
// BLS will provide these values to us.
let chainload = ChainloadConfiguration {
path: format!("{}\\$chainload", root),
options: vec!["$options".to_string()],
linux_initrd: Some(format!("{}\\$initrd", root)),
};
// Insert the chainload action into the configuration.
config.actions.insert(
chainload_action_name,
ActionDeclaration {
chainload: Some(chainload),
..Default::default()
},
);
// We had a BLS supported configuration, so return true.
Ok(true)
}

237
src/autoconfigure/linux.rs Normal file
View File

@@ -0,0 +1,237 @@
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 anyhow::{Context, Result};
use std::collections::BTreeMap;
use uefi::CString16;
use uefi::fs::{FileSystem, Path, PathBuf};
use uefi::proto::device_path::DevicePath;
use uefi::proto::device_path::text::{AllowShortcuts, DisplayOnly};
/// The name prefix of the Linux chainload action that will be used to boot Linux.
const LINUX_CHAINLOAD_ACTION_PREFIX: &str = "linux-chainload-";
/// The locations to scan for kernel pairs.
/// We will check for symlinks and if this directory is a symlink, we will skip it.
/// The empty string represents the root of the filesystem.
const SCAN_LOCATIONS: &[&str] = &["\\boot", "\\"];
/// Prefixes of kernel files to scan for.
const KERNEL_PREFIXES: &[&str] = &["vmlinuz"];
/// Prefixes of initramfs files to match to.
const INITRAMFS_PREFIXES: &[&str] = &["initramfs", "initrd", "initrd.img"];
/// Pair of kernel and initramfs.
/// This is what scanning a directory is meant to find.
struct KernelPair {
/// The path to a kernel.
kernel: String,
/// The path to an initramfs, if any.
initramfs: Option<String>,
}
/// Scan the specified `filesystem` at `path` for [KernelPair] results.
fn scan_directory(filesystem: &mut FileSystem, path: &str) -> Result<Vec<KernelPair>> {
// All the discovered kernel pairs.
let mut pairs = Vec::new();
// We have to special-case the root directory due to path logic in the uefi crate.
let is_root = path.is_empty() || path == "\\";
// Construct a filesystem path from the path string.
let path = CString16::try_from(path).context("unable to convert path to CString16")?;
let path = Path::new(&path);
let path = path.to_path_buf();
// Check if the path exists and is a directory.
let exists = filesystem
.metadata(&path)
.ok()
.map(|metadata| metadata.is_directory())
.unwrap_or(false);
// If the path does not exist, return an empty list.
if !exists {
return Ok(pairs);
}
// Open a directory iterator on the path to scan.
// Ignore errors here as in some scenarios this might fail due to symlinks.
let Some(directory) = filesystem.read_dir(&path).ok() else {
return Ok(pairs);
};
// Create a new path used for joining file names below.
// All attempts to derive paths for the files in the directory should use this instead.
// The uefi crate does not handle push correctly for the root directory.
// It will add a second slash, which will cause our path logic to fail.
let path_for_join = if is_root {
PathBuf::new()
} else {
path.clone()
};
// For each item in the directory, find a kernel.
for item in directory {
let item = item.context("unable to read directory item")?;
// Skip over any items that are not regular files.
if !item.is_regular_file() {
continue;
}
// Convert the name from a CString16 to a String.
let name = item.file_name().to_string();
// Convert the name to lowercase to make all of this case-insensitive.
let name_for_match = name.to_lowercase();
// Find a kernel prefix that matches, if any.
// This is case-insensitive to ensure we pick up all possibilities.
let Some(prefix) = KERNEL_PREFIXES.iter().find(|prefix| {
name_for_match == **prefix || name_for_match.starts_with(&format!("{}-", prefix))
}) else {
// Skip over anything that doesn't match a kernel prefix.
continue;
};
// Acquire the suffix of the name, this will be used to match an initramfs.
let suffix = &name[prefix.len()..];
// Find a matching initramfs, if any.
let mut initramfs_prefix_iter = INITRAMFS_PREFIXES.iter();
let matched_initramfs_path = loop {
let Some(prefix) = initramfs_prefix_iter.next() else {
break None;
};
// Construct an initramfs path.
let initramfs = format!("{}{}", prefix, suffix);
let initramfs = CString16::try_from(initramfs.as_str())
.context("unable to convert initramfs name to CString16")?;
let mut initramfs_path = path_for_join.clone();
initramfs_path.push(Path::new(&initramfs));
// Check if the initramfs path exists, if it does, break out of the loop.
if filesystem
.try_exists(&initramfs_path)
.context("unable to check if initramfs path exists")?
{
break Some(initramfs_path);
}
};
// Construct a kernel path from the kernel name.
let mut kernel = path_for_join.clone();
kernel.push(Path::new(&item.file_name()));
let kernel = kernel.to_string();
let initramfs = matched_initramfs_path.map(|initramfs_path| initramfs_path.to_string());
// Produce a kernel pair.
let pair = KernelPair { kernel, initramfs };
pairs.push(pair);
}
Ok(pairs)
}
/// Scan the specified `filesystem` for Linux kernels and matching initramfs.
pub fn scan(
filesystem: &mut FileSystem,
root: &DevicePath,
config: &mut RootConfiguration,
) -> Result<bool> {
let mut pairs = Vec::new();
// Convert the device path root to a string we can use in the configuration.
let mut root = root
.to_string(DisplayOnly(false), AllowShortcuts(false))
.context("unable to convert device root to string")?
.to_string();
// Add a trailing forward-slash to the root to ensure the device root is completed.
root.push('/');
// Generate a unique hash of the root path.
let root_unique_hash = utils::unique_hash(&root);
// Scan all locations for kernel pairs, adding them to the list.
for location in SCAN_LOCATIONS {
let scanned = scan_directory(filesystem, location)
.with_context(|| format!("unable to scan directory {}", location))?;
pairs.extend(scanned);
}
// If no kernel pairs were found, return false.
if pairs.is_empty() {
return Ok(false);
}
// Generate a unique name for the linux chainload action.
let chainload_action_name = format!("{}{}", LINUX_CHAINLOAD_ACTION_PREFIX, root_unique_hash,);
// Kernel pairs are detected, generate a list configuration for it.
let generator = ListConfiguration {
entry: EntryDeclaration {
title: "Boot Linux $name".to_string(),
actions: vec![chainload_action_name.clone()],
..Default::default()
},
values: pairs
.into_iter()
.map(|pair| {
BTreeMap::from_iter(vec![
("name".to_string(), pair.kernel.clone()),
("kernel".to_string(), format!("{}{}", root, pair.kernel)),
(
"initrd".to_string(),
pair.initramfs
.map(|initramfs| format!("{}{}", root, initramfs))
.unwrap_or_default(),
),
])
})
.collect(),
};
// Generate a unique name for the Linux generator and insert the generator into the configuration.
config.generators.insert(
format!("auto-linux-{}", root_unique_hash),
GeneratorDeclaration {
list: Some(generator),
..Default::default()
},
);
// Insert a default value for the linux-options if it doesn't exist.
if !config.values.contains_key("linux-options") {
config
.values
.insert("linux-options".to_string(), "".to_string());
}
// Generate a chainload configuration for the list generator.
// The list will provide these values to us.
// Note that we don't need an extra \\ in the paths here.
// The root already contains a trailing slash.
let chainload = ChainloadConfiguration {
path: "$kernel".to_string(),
options: vec!["$linux-options".to_string()],
linux_initrd: Some("$initrd".to_string()),
};
// Insert the chainload action into the configuration.
config.actions.insert(
chainload_action_name,
ActionDeclaration {
chainload: Some(chainload),
..Default::default()
},
);
// We had a Linux kernel, so return true to indicate something was found.
Ok(true)
}

View File

@@ -0,0 +1,80 @@
use crate::actions::ActionDeclaration;
use crate::actions::chainload::ChainloadConfiguration;
use crate::config::RootConfiguration;
use crate::entries::EntryDeclaration;
use crate::utils;
use anyhow::{Context, Result};
use uefi::CString16;
use uefi::fs::{FileSystem, Path};
use uefi::proto::device_path::DevicePath;
use uefi::proto::device_path::text::{AllowShortcuts, DisplayOnly};
/// The name prefix of the Windows chainload action that will be used to boot Windows.
const WINDOWS_CHAINLOAD_ACTION_PREFIX: &str = "windows-chainload-";
/// Windows boot manager path.
const BOOTMGR_FW_PATH: &str = "\\EFI\\Microsoft\\Boot\\bootmgfw.efi";
/// Scan the specified `filesystem` for Windows configurations.
pub fn scan(
filesystem: &mut FileSystem,
root: &DevicePath,
config: &mut RootConfiguration,
) -> Result<bool> {
// Convert the boot manager firmware path to a path.
let bootmgr_fw_path =
CString16::try_from(BOOTMGR_FW_PATH).context("unable to convert path to CString16")?;
let bootmgr_fw_path = Path::new(&bootmgr_fw_path);
// Check if the boot manager firmware path exists, if it doesn't, return false.
if !filesystem
.try_exists(bootmgr_fw_path)
.context("unable to check if bootmgr firmware path exists")?
{
return Ok(false);
}
// Convert the device path root to a string we can use in the configuration.
let mut root = root
.to_string(DisplayOnly(false), AllowShortcuts(false))
.context("unable to convert device root to string")?
.to_string();
// Add a trailing forward-slash to the root to ensure the device root is completed.
root.push('/');
// Generate a unique hash of the root path.
let root_unique_hash = utils::unique_hash(&root);
// Generate a unique name for the Windows chainload action.
let chainload_action_name = format!("{}{}", WINDOWS_CHAINLOAD_ACTION_PREFIX, root_unique_hash,);
// Generate an entry name for Windows.
let entry_name = format!("auto-windows-{}", root_unique_hash,);
// Create an entry for Windows and insert it into the configuration.
let entry = EntryDeclaration {
title: "Boot Windows".to_string(),
actions: vec![chainload_action_name.clone()],
values: Default::default(),
};
config.entries.insert(entry_name, entry);
// Generate a chainload configuration for Windows.
let chainload = ChainloadConfiguration {
path: format!("{}{}", root, bootmgr_fw_path),
options: vec![],
..Default::default()
};
// Insert the chainload action into the configuration.
config.actions.insert(
chainload_action_name,
ActionDeclaration {
chainload: Some(chainload),
..Default::default()
},
);
// We have a Windows boot entry, so return true to indicate something was found.
Ok(true)
}

View File

@@ -18,7 +18,7 @@ pub const LATEST_VERSION: u32 = 1;
pub const DEFAULT_MENU_TIMEOUT_SECONDS: u64 = 10; pub const DEFAULT_MENU_TIMEOUT_SECONDS: u64 = 10;
/// The Sprout configuration format. /// The Sprout configuration format.
#[derive(Serialize, Deserialize, Default, Clone)] #[derive(Serialize, Deserialize, Debug, Default, Clone)]
pub struct RootConfiguration { pub struct RootConfiguration {
/// The version of the configuration. This should always be declared /// The version of the configuration. This should always be declared
/// and be the latest version that is supported. If not specified, it is assumed /// and be the latest version that is supported. If not specified, it is assumed
@@ -27,7 +27,7 @@ pub struct RootConfiguration {
pub version: u32, pub version: u32,
/// Default options for Sprout. /// Default options for Sprout.
#[serde(default)] #[serde(default)]
pub defaults: DefaultsConfiguration, pub options: OptionsConfiguration,
/// Values to be inserted into the root sprout context. /// Values to be inserted into the root sprout context.
#[serde(default)] #[serde(default)]
pub values: BTreeMap<String, String>, pub values: BTreeMap<String, String>,
@@ -65,15 +65,19 @@ pub struct RootConfiguration {
pub phases: PhasesConfiguration, pub phases: PhasesConfiguration,
} }
/// Default configuration for Sprout, used when the corresponding options are not specified. /// Options configuration for Sprout, used when the corresponding options are not specified.
#[derive(Serialize, Deserialize, Default, Clone)] #[derive(Serialize, Deserialize, Debug, Default, Clone)]
pub struct DefaultsConfiguration { pub struct OptionsConfiguration {
/// The entry to boot without showing the boot menu. /// The entry to boot without showing the boot menu.
/// If not specified, a boot menu is shown. /// If not specified, a boot menu is shown.
pub entry: Option<String>, #[serde(rename = "default-entry", default)]
pub default_entry: Option<String>,
/// The timeout of the boot menu. /// The timeout of the boot menu.
#[serde(rename = "menu-timeout", default = "default_menu_timeout")] #[serde(rename = "menu-timeout", default = "default_menu_timeout")]
pub menu_timeout: u64, pub menu_timeout: u64,
/// Enables autoconfiguration of Sprout based on the environment.
#[serde(default)]
pub autoconfigure: bool,
} }
fn latest_version() -> u32 { fn latest_version() -> u32 {

View File

@@ -1,5 +1,6 @@
use crate::config::{RootConfiguration, latest_version}; use crate::config::{RootConfiguration, latest_version};
use crate::options::SproutOptions; use crate::options::SproutOptions;
use crate::platform::tpm::PlatformTpm;
use crate::utils; use crate::utils;
use anyhow::{Context, Result, bail}; use anyhow::{Context, Result, bail};
use log::info; use log::info;
@@ -19,8 +20,17 @@ fn load_raw_config(options: &SproutOptions) -> Result<Vec<u8>> {
info!("configuration file: {}", options.config); info!("configuration file: {}", options.config);
// Read the contents of the sprout config file. // Read the contents of the sprout config file.
let content = utils::read_file_contents(&path, &options.config) let content = utils::read_file_contents(Some(&path), &options.config)
.context("unable to read sprout config file")?; .context("unable to read sprout config file")?;
// Measure the sprout.toml into the TPM, if needed and possible.
PlatformTpm::log_event(
PlatformTpm::PCR_BOOT_LOADER_CONFIG,
&content,
"sprout: configuration file",
)
.context("unable to measure the sprout.toml file into the TPM")?;
// Return the contents of the sprout config file. // Return the contents of the sprout config file.
Ok(content) Ok(content)
} }

View File

@@ -1,5 +1,6 @@
use crate::actions::ActionDeclaration; use crate::actions::ActionDeclaration;
use crate::options::SproutOptions; use crate::options::SproutOptions;
use crate::platform::timer::PlatformTimer;
use anyhow::anyhow; use anyhow::anyhow;
use anyhow::{Result, bail}; use anyhow::{Result, bail};
use std::cmp::Reverse; use std::cmp::Reverse;
@@ -12,22 +13,29 @@ const CONTEXT_FINALIZE_ITERATION_LIMIT: usize = 100;
/// Declares a root context for Sprout. /// Declares a root context for Sprout.
/// This contains data that needs to be shared across Sprout. /// This contains data that needs to be shared across Sprout.
#[derive(Default)]
pub struct RootContext { pub struct RootContext {
/// The actions that are available in Sprout. /// The actions that are available in Sprout.
actions: BTreeMap<String, ActionDeclaration>, actions: BTreeMap<String, ActionDeclaration>,
/// The device path of the loaded Sprout image. /// The device path of the loaded Sprout image.
loaded_image_path: Option<Box<DevicePath>>, loaded_image_path: Option<Box<DevicePath>>,
/// Platform timer started at the beginning of the boot process.
timer: PlatformTimer,
/// The global options of Sprout. /// The global options of Sprout.
options: SproutOptions, options: SproutOptions,
} }
impl RootContext { impl RootContext {
/// Creates a new root context with the `loaded_image_device_path` which will be stored /// Creates a new root context with the `loaded_image_device_path` which will be stored
/// in the context for easy access. /// in the context for easy access. We also provide a `timer` which is used to measure elapsed
pub fn new(loaded_image_device_path: Box<DevicePath>, options: SproutOptions) -> Self { /// time for the bootloader.
pub fn new(
loaded_image_device_path: Box<DevicePath>,
timer: PlatformTimer,
options: SproutOptions,
) -> Self {
Self { Self {
actions: BTreeMap::new(), actions: BTreeMap::new(),
timer,
loaded_image_path: Some(loaded_image_device_path), loaded_image_path: Some(loaded_image_device_path),
options, options,
} }
@@ -43,6 +51,11 @@ impl RootContext {
&mut self.actions &mut self.actions
} }
/// Access the platform timer that is started at the beginning of the boot process.
pub fn timer(&self) -> &PlatformTimer {
&self.timer
}
/// Access the device path of the loaded Sprout image. /// Access the device path of the loaded Sprout image.
pub fn loaded_image_path(&self) -> Result<&DevicePath> { pub fn loaded_image_path(&self) -> Result<&DevicePath> {
self.loaded_image_path self.loaded_image_path
@@ -83,6 +96,11 @@ impl SproutContext {
self.root.as_ref() self.root.as_ref()
} }
/// Access the root context to modify it, if possible.
pub fn root_mut(&mut self) -> Option<&mut RootContext> {
Rc::get_mut(&mut self.root)
}
/// Retrieve the value specified by `key` from this context or its parents. /// Retrieve the value specified by `key` from this context or its parents.
/// Returns `None` if the value is not found. /// Returns `None` if the value is not found.
pub fn get(&self, key: impl AsRef<str>) -> Option<&String> { pub fn get(&self, key: impl AsRef<str>) -> Option<&String> {
@@ -113,7 +131,10 @@ impl SproutContext {
pub fn all_values(&self) -> BTreeMap<String, String> { pub fn all_values(&self) -> BTreeMap<String, String> {
let mut values = BTreeMap::new(); let mut values = BTreeMap::new();
for key in self.all_keys() { for key in self.all_keys() {
values.insert(key.clone(), self.get(key).cloned().unwrap_or_default()); // Acquire the value from the context. Since retrieving all the keys will give us
// a full view of the context, we can be sure that the key exists.
let value = self.get(&key).cloned().unwrap_or_default();
values.insert(key.clone(), value);
} }
values values
} }
@@ -160,13 +181,13 @@ impl SproutContext {
let mut current_values = self.all_values(); let mut current_values = self.all_values();
// To ensure that there is no possible infinite loop, we need to check // To ensure that there is no possible infinite loop, we need to check
// the number of iterations. If it exceeds 100, we bail. // the number of iterations. If it exceeds CONTEXT_FINALIZE_ITERATION_LIMIT, we bail.
let mut iterations: usize = 0; let mut iterations: usize = 0;
loop { loop {
iterations += 1; iterations += 1;
if iterations > CONTEXT_FINALIZE_ITERATION_LIMIT { if iterations > CONTEXT_FINALIZE_ITERATION_LIMIT {
bail!("infinite loop detected in context finalization"); bail!("maximum number of replacement iterations reached while finalizing context");
} }
let mut did_change = false; let mut did_change = false;
@@ -198,6 +219,14 @@ impl SproutContext {
/// Stamps the `text` value with the specified `values` map. The returned value indicates /// Stamps the `text` value with the specified `values` map. The returned value indicates
/// whether the `text` has been changed and the value that was stamped and changed. /// whether the `text` has been changed and the value that was stamped and changed.
///
/// Stamping works like this:
/// - Start with the input text.
/// - Sort all the keys in reverse length order (longest keys first)
/// - For each key, if the key is not empty, replace $KEY in the text.
/// - Each follow-up iteration acts upon the last iterations result.
/// - We keep track if the text changes during the replacement.
/// - We return both whether the text changed during any iteration and the final result.
fn stamp_values(values: &BTreeMap<String, String>, text: impl AsRef<str>) -> (bool, String) { fn stamp_values(values: &BTreeMap<String, String>, text: impl AsRef<str>) -> (bool, String) {
let mut result = text.as_ref().to_string(); let mut result = text.as_ref().to_string();
let mut did_change = false; let mut did_change = false;
@@ -235,4 +264,10 @@ impl SproutContext {
pub fn stamp(&self, text: impl AsRef<str>) -> String { pub fn stamp(&self, text: impl AsRef<str>) -> String {
Self::stamp_values(&self.all_values(), text.as_ref()).1 Self::stamp_values(&self.all_values(), text.as_ref()).1
} }
/// Unloads a [SproutContext] back into an owned context. This
/// may not succeed if something else is holding onto the value.
pub fn unload(self: Rc<SproutContext>) -> Option<SproutContext> {
Rc::into_inner(self)
}
} }

View File

@@ -1,4 +1,5 @@
use crate::context::SproutContext; use crate::context::SproutContext;
use crate::integrations::shim::{ShimInput, ShimSupport};
use crate::utils; use crate::utils;
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use log::info; use log::info;
@@ -6,13 +7,12 @@ use serde::{Deserialize, Serialize};
use std::collections::BTreeMap; use std::collections::BTreeMap;
use std::rc::Rc; use std::rc::Rc;
use uefi::boot::SearchType; use uefi::boot::SearchType;
use uefi::proto::device_path::LoadedImageDevicePath;
/// Declares a driver configuration. /// Declares a driver configuration.
/// Drivers allow extending the functionality of Sprout. /// Drivers allow extending the functionality of Sprout.
/// Drivers are loaded at runtime and can provide extra functionality like filesystem support. /// 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. /// Drivers are loaded by their name, which is used to reference them in other concepts.
#[derive(Serialize, Deserialize, Default, Clone)] #[derive(Serialize, Deserialize, Debug, Default, Clone)]
pub struct DriverDeclaration { pub struct DriverDeclaration {
/// The filesystem path to the driver. /// The filesystem path to the driver.
/// This file should be an EFI executable that can be located and executed. /// This file should be an EFI executable that can be located and executed.
@@ -23,30 +23,17 @@ pub struct DriverDeclaration {
fn load_driver(context: Rc<SproutContext>, driver: &DriverDeclaration) -> Result<()> { fn load_driver(context: Rc<SproutContext>, driver: &DriverDeclaration) -> Result<()> {
// Acquire the handle and device path of the loaded image. // Acquire the handle and device path of the loaded image.
let sprout_image = uefi::boot::image_handle(); let sprout_image = uefi::boot::image_handle();
let image_device_path_protocol =
uefi::boot::open_protocol_exclusive::<LoadedImageDevicePath>(sprout_image)
.context("unable to open loaded image device path protocol")?;
// Get the device path root of the sprout image. // Resolve the path to the driver image.
let mut full_path = utils::device_path_root(&image_device_path_protocol)?; let resolved = utils::resolve_path(
Some(context.root().loaded_image_path()?),
// Push the path of the driver from the root. &context.stamp(&driver.path),
full_path.push_str(&context.stamp(&driver.path));
info!("driver path: {}", full_path);
// Convert the path to a device path.
let device_path = utils::text_to_device_path(&full_path)?;
// Load the driver image.
let image = uefi::boot::load_image(
sprout_image,
uefi::boot::LoadImageSource::FromDevicePath {
device_path: &device_path,
boot_policy: uefi::proto::BootPolicy::ExactMatch,
},
) )
.context("unable to load image")?; .context("unable to resolve path to driver")?;
// Load the driver image using the shim support integration.
// It will determine if the image needs to be loaded via the shim or can be loaded directly.
let image = ShimSupport::load(sprout_image, ShimInput::ResolvedPath(&resolved))?;
// Start the driver image, this is expected to return control to sprout. // Start the driver image, this is expected to return control to sprout.
// There is no guarantee that the driver will actually return control as it is // There is no guarantee that the driver will actually return control as it is

View File

@@ -7,7 +7,7 @@ use std::rc::Rc;
/// ///
/// Entries are the user-facing concept of Sprout, making it possible /// Entries are the user-facing concept of Sprout, making it possible
/// to run a set of actions with a specific context. /// to run a set of actions with a specific context.
#[derive(Serialize, Deserialize, Default, Clone)] #[derive(Serialize, Deserialize, Debug, Default, Clone)]
pub struct EntryDeclaration { pub struct EntryDeclaration {
/// The title of the entry which will be display in the boot menu. /// The title of the entry which will be display in the boot menu.
/// This is the pre-stamped value. /// This is the pre-stamped value.
@@ -28,6 +28,7 @@ pub struct BootableEntry {
context: Rc<SproutContext>, context: Rc<SproutContext>,
declaration: EntryDeclaration, declaration: EntryDeclaration,
default: bool, default: bool,
pin_name: bool,
} }
impl BootableEntry { impl BootableEntry {
@@ -44,6 +45,7 @@ impl BootableEntry {
context, context,
declaration, declaration,
default: false, default: false,
pin_name: false,
} }
} }
@@ -72,6 +74,11 @@ impl BootableEntry {
self.default self.default
} }
/// Fetch whether the entry is pinned, which prevents prefixing.
pub fn is_pin_name(&self) -> bool {
self.pin_name
}
/// Swap out the context of the entry. /// Swap out the context of the entry.
pub fn swap_context(&mut self, context: Rc<SproutContext>) { pub fn swap_context(&mut self, context: Rc<SproutContext>) {
self.context = context; self.context = context;
@@ -87,6 +94,11 @@ impl BootableEntry {
self.default = true; self.default = true;
} }
/// Mark this entry as being pinned, which prevents prefixing.
pub fn mark_pin_name(&mut self) {
self.pin_name = true;
}
/// Prepend the name of the entry with `prefix`. /// Prepend the name of the entry with `prefix`.
pub fn prepend_name_prefix(&mut self, prefix: &str) { pub fn prepend_name_prefix(&mut self, prefix: &str) {
self.name.insert_str(0, prefix); self.name.insert_str(0, prefix);

View File

@@ -10,7 +10,7 @@ pub mod filesystem_device_match;
/// Declares an extractor configuration. /// Declares an extractor configuration.
/// Extractors allow calculating values at runtime /// Extractors allow calculating values at runtime
/// using built-in sprout modules. /// using built-in sprout modules.
#[derive(Serialize, Deserialize, Default, Clone)] #[derive(Serialize, Deserialize, Debug, Default, Clone)]
pub struct ExtractorDeclaration { pub struct ExtractorDeclaration {
/// The filesystem device match extractor. /// The filesystem device match extractor.
/// This extractor finds a filesystem using some search criteria and returns /// This extractor finds a filesystem using some search criteria and returns

View File

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

View File

@@ -1,6 +1,7 @@
use crate::context::SproutContext; use crate::context::SproutContext;
use crate::entries::BootableEntry; use crate::entries::BootableEntry;
use crate::generators::bls::BlsConfiguration; use crate::generators::bls::BlsConfiguration;
use crate::generators::list::ListConfiguration;
use crate::generators::matrix::MatrixConfiguration; use crate::generators::matrix::MatrixConfiguration;
use anyhow::Result; use anyhow::Result;
use anyhow::bail; use anyhow::bail;
@@ -8,11 +9,12 @@ use serde::{Deserialize, Serialize};
use std::rc::Rc; use std::rc::Rc;
pub mod bls; pub mod bls;
pub mod list;
pub mod matrix; pub mod matrix;
/// Declares a generator configuration. /// Declares a generator configuration.
/// Generators allow generating entries at runtime based on a set of data. /// Generators allow generating entries at runtime based on a set of data.
#[derive(Serialize, Deserialize, Default, Clone)] #[derive(Serialize, Deserialize, Debug, Default, Clone)]
pub struct GeneratorDeclaration { pub struct GeneratorDeclaration {
/// Matrix generator configuration. /// Matrix generator configuration.
/// Matrix allows you to specify multiple value-key values as arrays. /// Matrix allows you to specify multiple value-key values as arrays.
@@ -32,6 +34,9 @@ pub struct GeneratorDeclaration {
/// It will generate a sprout entry for every supported BLS entry. /// It will generate a sprout entry for every supported BLS entry.
#[serde(default)] #[serde(default)]
pub bls: Option<BlsConfiguration>, pub bls: Option<BlsConfiguration>,
/// List generator configuration.
/// Allows you to specify a list of values to generate an entry from.
pub list: Option<ListConfiguration>,
} }
/// Runs the generator specified by the `generator` option. /// Runs the generator specified by the `generator` option.
@@ -45,6 +50,8 @@ pub fn generate(
matrix::generate(context, matrix) matrix::generate(context, matrix)
} else if let Some(bls) = &generator.bls { } else if let Some(bls) = &generator.bls {
bls::generate(context, bls) bls::generate(context, bls)
} else if let Some(list) = &generator.list {
list::generate(context, list)
} else { } else {
bail!("unknown generator configuration"); bail!("unknown generator configuration");
} }

View File

@@ -20,7 +20,7 @@ const BLS_TEMPLATE_PATH: &str = "\\loader";
/// The configuration of the BLS generator. /// The configuration of the BLS generator.
/// The BLS uses the Bootloader Specification to produce /// The BLS uses the Bootloader Specification to produce
/// entries from an input template. /// entries from an input template.
#[derive(Serialize, Deserialize, Default, Clone)] #[derive(Serialize, Deserialize, Debug, Default, Clone)]
pub struct BlsConfiguration { pub struct BlsConfiguration {
/// The entry to use for as a template. /// The entry to use for as a template.
pub entry: EntryDeclaration, pub entry: EntryDeclaration,
@@ -49,7 +49,7 @@ pub fn generate(context: Rc<SproutContext>, bls: &BlsConfiguration) -> Result<Ve
let path = context.stamp(&bls.path); let path = context.stamp(&bls.path);
// Resolve the path to the BLS directory. // Resolve the path to the BLS directory.
let bls_resolved = utils::resolve_path(context.root().loaded_image_path()?, &path) let bls_resolved = utils::resolve_path(Some(context.root().loaded_image_path()?), &path)
.context("unable to resolve bls path")?; .context("unable to resolve bls path")?;
// Construct a filesystem path to the BLS entries directory. // Construct a filesystem path to the BLS entries directory.
@@ -83,13 +83,16 @@ pub fn generate(context: Rc<SproutContext>, bls: &BlsConfiguration) -> Result<Ve
} }
// Get the file name of the filesystem item. // Get the file name of the filesystem item.
let name = entry.file_name().to_string(); let mut name = entry.file_name().to_string();
// Ignore files that are not .conf files. // Ignore files that are not .conf files.
if !name.ends_with(".conf") { if !name.to_lowercase().ends_with(".conf") {
continue; continue;
} }
// Remove the .conf extension.
name.truncate(name.len() - 5);
// Create a mutable path so we can append the file name to produce the full path. // Create a mutable path so we can append the file name to produce the full path.
let mut full_entry_path = entries_path.to_path_buf(); let mut full_entry_path = entries_path.to_path_buf();
full_entry_path.push(entry.file_name()); full_entry_path.push(entry.file_name());
@@ -125,13 +128,21 @@ pub fn generate(context: Rc<SproutContext>, bls: &BlsConfiguration) -> Result<Ve
context.set("options", options); context.set("options", options);
context.set("initrd", initrd); context.set("initrd", initrd);
// Add the entry to the list with a frozen context. // Produce a new bootable entry.
entries.push(BootableEntry::new( let mut entry = BootableEntry::new(
name, name,
bls.entry.title.clone(), bls.entry.title.clone(),
context.freeze(), context.freeze(),
bls.entry.clone(), bls.entry.clone(),
)); );
// Pin the entry name to prevent prefixing.
// This is needed as the bootloader interface requires the name to be
// the same as the entry file name, minus the .conf extension.
entry.mark_pin_name();
// Add the entry to the list with a frozen context.
entries.push(entry);
} }
Ok(entries) Ok(entries)

View File

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

52
src/generators/list.rs Normal file
View File

@@ -0,0 +1,52 @@
use crate::context::SproutContext;
use crate::entries::{BootableEntry, EntryDeclaration};
use anyhow::Result;
use serde::{Deserialize, Serialize};
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`.
pub fn generate(
context: Rc<SproutContext>,
list: &ListConfiguration,
) -> Result<Vec<BootableEntry>> {
let mut entries = Vec::new();
// For each combination, create a new context and entry.
for (index, combination) in list.values.iter().enumerate() {
let mut context = context.fork();
// Insert the combination into the context.
context.insert(combination);
let context = context.freeze();
// Stamp the entry title and actions from the template.
let mut entry = list.entry.clone();
entry.actions = entry
.actions
.into_iter()
.map(|action| context.stamp(action))
.collect();
// Push the entry into the list with the new context.
entries.push(BootableEntry::new(
index.to_string(),
entry.title.clone(),
context,
entry,
));
}
Ok(entries)
}

View File

@@ -1,5 +1,6 @@
use crate::context::SproutContext; use crate::context::SproutContext;
use crate::entries::{BootableEntry, EntryDeclaration}; use crate::entries::{BootableEntry, EntryDeclaration};
use crate::generators::list;
use anyhow::Result; use anyhow::Result;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::BTreeMap; use std::collections::BTreeMap;
@@ -8,7 +9,7 @@ use std::rc::Rc;
/// Matrix generator configuration. /// Matrix generator configuration.
/// The matrix generator produces multiple entries based /// The matrix generator produces multiple entries based
/// on input values multiplicatively. /// on input values multiplicatively.
#[derive(Serialize, Deserialize, Default, Clone)] #[derive(Serialize, Deserialize, Debug, Default, Clone)]
pub struct MatrixConfiguration { pub struct MatrixConfiguration {
/// The template entry to use for each generated entry. /// The template entry to use for each generated entry.
#[serde(default)] #[serde(default)]
@@ -57,30 +58,12 @@ pub fn generate(
) -> Result<Vec<BootableEntry>> { ) -> Result<Vec<BootableEntry>> {
// Produce all the combinations of the input values. // Produce all the combinations of the input values.
let combinations = build_matrix(&matrix.values); let combinations = build_matrix(&matrix.values);
let mut entries = Vec::new(); // Use the list generator to generate entries for each combination.
list::generate(
// For each combination, create a new context and entry. context,
for (index, combination) in combinations.into_iter().enumerate() { &list::ListConfiguration {
let mut context = context.fork(); entry: matrix.entry.clone(),
// Insert the combination into the context. values: combinations,
context.insert(&combination); },
let context = context.freeze(); )
// Stamp the entry title and actions from the template.
let mut entry = matrix.entry.clone();
entry.actions = entry
.actions
.into_iter()
.map(|action| context.stamp(action))
.collect();
// Push the entry into the list with the new context.
entries.push(BootableEntry::new(
index.to_string(),
entry.title.clone(),
context,
entry,
));
}
Ok(entries)
} }

4
src/integrations.rs Normal file
View File

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

View File

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

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

@@ -0,0 +1,293 @@
use crate::integrations::shim::hook::SecurityHook;
use crate::utils;
use crate::utils::ResolvedPath;
use crate::utils::variables::{VariableClass, VariableController};
use anyhow::{Context, Result, anyhow, bail};
use log::warn;
use std::ffi::c_void;
use uefi::Handle;
use uefi::boot::LoadImageSource;
use uefi::proto::device_path::text::{AllowShortcuts, DisplayOnly};
use uefi::proto::device_path::{DevicePath, FfiDevicePath};
use uefi::proto::unsafe_protocol;
use uefi_raw::table::runtime::VariableVendor;
use uefi_raw::{Guid, Status, guid};
/// Security hook support.
mod hook;
/// Support for the shim loader application for Secure Boot.
pub struct ShimSupport;
/// Input to the shim mechanisms.
pub enum ShimInput<'a> {
/// Data loaded into a buffer and ready to be verified, owned.
OwnedDataBuffer(Option<&'a ResolvedPath>, Vec<u8>),
/// Data loaded into a buffer and ready to be verified.
DataBuffer(Option<&'a ResolvedPath>, &'a [u8]),
/// Low-level data buffer provided by the security hook.
SecurityHookBuffer(Option<*const FfiDevicePath>, &'a [u8]),
/// Low-level owned data buffer provided by the security hook.
SecurityHookOwnedBuffer(Option<*const FfiDevicePath>, Vec<u8>),
/// Low-level path provided by the security hook.
SecurityHookPath(*const FfiDevicePath),
/// Data is provided as a resolved path. We will need to load the data to verify it.
/// The output will them return the loaded data.
ResolvedPath(&'a ResolvedPath),
}
impl<'a> ShimInput<'a> {
/// Accesses the buffer behind the shim input, if available.
pub fn buffer(&self) -> Option<&[u8]> {
match self {
ShimInput::OwnedDataBuffer(_, data) => Some(data),
ShimInput::SecurityHookOwnedBuffer(_, data) => Some(data),
ShimInput::SecurityHookBuffer(_, data) => Some(data),
ShimInput::SecurityHookPath(_) => None,
ShimInput::DataBuffer(_, data) => Some(data),
ShimInput::ResolvedPath(_) => None,
}
}
/// Accesses the full device path to the input.
pub fn file_path(&self) -> Option<&DevicePath> {
match self {
ShimInput::OwnedDataBuffer(path, _) => path.as_ref().map(|it| it.full_path.as_ref()),
ShimInput::DataBuffer(path, _) => path.as_ref().map(|it| it.full_path.as_ref()),
ShimInput::SecurityHookBuffer(path, _) => {
path.map(|it| unsafe { DevicePath::from_ffi_ptr(it) })
}
ShimInput::SecurityHookPath(path) => unsafe { Some(DevicePath::from_ffi_ptr(*path)) },
ShimInput::ResolvedPath(path) => Some(path.full_path.as_ref()),
ShimInput::SecurityHookOwnedBuffer(path, _) => {
path.map(|it| unsafe { DevicePath::from_ffi_ptr(it) })
}
}
}
/// Converts this input into an owned data buffer, where the data is loaded.
/// For ResolvedPath, this will read the file.
pub fn into_owned_data_buffer(self) -> Result<ShimInput<'a>> {
match self {
ShimInput::OwnedDataBuffer(root, data) => Ok(ShimInput::OwnedDataBuffer(root, data)),
ShimInput::DataBuffer(root, data) => {
Ok(ShimInput::OwnedDataBuffer(root, data.to_vec()))
}
ShimInput::SecurityHookPath(ffi_path) => {
// Acquire the file path.
let Some(path) = self.file_path() else {
bail!("unable to convert security hook path to device path");
};
// Convert the underlying path to a string.
let path = path
.to_string(DisplayOnly(false), AllowShortcuts(false))
.context("unable to convert device path to string")?;
let path = utils::resolve_path(None, &path.to_string())
.context("unable to resolve path")?;
// Read the file path.
let data = path.read_file()?;
Ok(ShimInput::SecurityHookOwnedBuffer(Some(ffi_path), data))
}
ShimInput::SecurityHookBuffer(_, _) => {
bail!("unable to convert security hook buffer to owned data buffer")
}
ShimInput::ResolvedPath(path) => {
Ok(ShimInput::OwnedDataBuffer(Some(path), path.read_file()?))
}
ShimInput::SecurityHookOwnedBuffer(path, data) => {
Ok(ShimInput::SecurityHookOwnedBuffer(path, data))
}
}
}
}
/// Output of the shim verification function.
/// Since the shim needs to load the data from disk, we will optimize by using that as the data
/// to actually boot.
pub enum ShimVerificationOutput {
/// The verification failed.
VerificationFailed,
/// The data provided to the verifier was already a buffer.
VerifiedDataNotLoaded,
/// Verifying the data resulted in loading the data from the source.
/// This contains the data that was loaded, so it won't need to be loaded again.
VerifiedDataBuffer(Vec<u8>),
}
/// The shim lock protocol as defined by the shim loader application.
#[unsafe_protocol(ShimSupport::SHIM_LOCK_GUID)]
struct ShimLockProtocol {
/// Verify the data in `buffer` with the size `buffer_size` to determine if it is valid.
pub shim_verify: unsafe extern "efiapi" fn(buffer: *mut c_void, buffer_size: u32) -> Status,
/// Unused function that is defined by the shim.
_generate_header: *mut c_void,
/// Unused function that is defined by the shim.
_read_header: *mut c_void,
}
impl ShimSupport {
/// Variable controller for the shim lock.
const SHIM_LOCK_VARIABLES: VariableController =
VariableController::new(VariableVendor(Self::SHIM_LOCK_GUID));
/// GUID for the shim lock protocol.
const SHIM_LOCK_GUID: Guid = guid!("605dab50-e046-4300-abb6-3dd810dd8b23");
/// GUID for the shim image loader protocol.
const SHIM_IMAGE_LOADER_GUID: Guid = guid!("1f492041-fadb-4e59-9e57-7cafe73a55ab");
/// Determines whether the shim is loaded.
pub fn loaded() -> Result<bool> {
Ok(utils::find_handle(&Self::SHIM_LOCK_GUID)
.context("unable to find shim lock protocol")?
.is_some())
}
/// Determines whether the shim loader is available.
pub fn loader_available() -> Result<bool> {
Ok(utils::find_handle(&Self::SHIM_IMAGE_LOADER_GUID)
.context("unable to find shim image loader protocol")?
.is_some())
}
/// Use the shim to validate the `input`, returning [ShimVerificationOutput] when complete.
pub fn verify(input: ShimInput) -> Result<ShimVerificationOutput> {
// Acquire the handle to the shim lock protocol.
let handle = utils::find_handle(&Self::SHIM_LOCK_GUID)
.context("unable to find shim lock protocol")?
.ok_or_else(|| anyhow!("unable to find shim lock protocol"))?;
// Acquire the protocol exclusively to the shim lock.
let protocol = uefi::boot::open_protocol_exclusive::<ShimLockProtocol>(handle)
.context("unable to open shim lock protocol")?;
// If the input type is a device path, we need to load the data.
let maybe_loaded_data = match input {
ShimInput::OwnedDataBuffer(_, _data) => {
bail!("owned data buffer is not supported in the verification function");
}
ShimInput::SecurityHookBuffer(_, _) => None,
ShimInput::SecurityHookOwnedBuffer(_, _) => None,
ShimInput::DataBuffer(_, _) => None,
ShimInput::ResolvedPath(path) => Some(path.read_file()?),
ShimInput::SecurityHookPath(_) => None,
};
// Convert the input to a buffer.
// If the input provides the data buffer, we will use that.
// Otherwise, we will use the data loaded by this function.
let buffer = match &input {
ShimInput::OwnedDataBuffer(_root, data) => data,
ShimInput::DataBuffer(_root, data) => *data,
ShimInput::ResolvedPath(_path) => maybe_loaded_data
.as_deref()
.context("expected data buffer to be loaded already")?,
ShimInput::SecurityHookBuffer(_, data) => data,
ShimInput::SecurityHookOwnedBuffer(_, data) => data,
ShimInput::SecurityHookPath(_) => {
bail!("security hook path input not supported in the verification function")
}
};
// Check if the buffer is too large to verify.
if buffer.len() > u32::MAX as usize {
bail!("buffer is too large to verify with shim lock protocol");
}
// Call the shim verify function.
// SAFETY: The shim verify function is specified by the shim lock protocol.
// Calling this function is considered safe because the shim verify function is
// guaranteed to be defined by the environment if we are able to acquire the protocol.
let status =
unsafe { (protocol.shim_verify)(buffer.as_ptr() as *mut c_void, buffer.len() as u32) };
// If the verification failed, return the verification failure output.
if !status.is_success() {
return Ok(ShimVerificationOutput::VerificationFailed);
}
// If verification succeeded, return the validation output,
// which might include the loaded data.
Ok(maybe_loaded_data
.map(ShimVerificationOutput::VerifiedDataBuffer)
.unwrap_or(ShimVerificationOutput::VerifiedDataNotLoaded))
}
/// Load the image specified by the `input` and returns an image handle.
pub fn load(current_image: Handle, input: ShimInput) -> Result<Handle> {
// Determine whether the shim is loaded.
let shim_loaded = Self::loaded().context("unable to determine if shim is loaded")?;
// Determine whether the shim loader is available.
let shim_loader_available =
Self::loader_available().context("unable to determine if shim loader is available")?;
// Determines whether LoadImage in Boot Services must be patched.
// Version 16 of the shim doesn't require extra effort to load Secure Boot binaries.
// If the image loader is installed, we can skip over the security hook.
let requires_security_hook = shim_loaded && !shim_loader_available;
// If the security hook is required, we will bail for now.
if requires_security_hook {
// Install the security hook, if possible. If it's not, this is necessary to continue,
// so we should bail.
let installed = SecurityHook::install().context("unable to install security hook")?;
if !installed {
bail!("unable to install security hook required for this platform");
}
}
// If the shim is loaded, we will need to retain the shim protocol to allow
// loading multiple images.
if shim_loaded {
// Retain the shim protocol after loading the image.
Self::retain()?;
}
// Converts the shim input to an owned data buffer.
let input = input
.into_owned_data_buffer()
.context("unable to convert input to loaded data buffer")?;
// Constructs a LoadImageSource from the input.
let source = LoadImageSource::FromBuffer {
buffer: input.buffer().context("unable to get buffer from input")?,
file_path: input.file_path(),
};
// Loads the image using Boot Services LoadImage function.
let result = uefi::boot::load_image(current_image, source).context("unable to load image");
// If the security override is required, we will uninstall the security hook.
if requires_security_hook {
let uninstall_result = SecurityHook::uninstall();
// Ensure we don't mask load image errors if uninstalling fails.
if result.is_err()
&& let Err(uninstall_error) = &uninstall_result
{
// Warn on the error since the load image error is more important.
warn!("unable to uninstall security hook: {}", uninstall_error);
} else {
// Otherwise, ensure we handle the original uninstallation result.
uninstall_result?;
}
}
result
}
/// Set the ShimRetainProtocol variable to indicate that shim should retain the protocols
/// for the full lifetime of boot services.
pub fn retain() -> Result<()> {
Self::SHIM_LOCK_VARIABLES
.set_bool(
"ShimRetainProtocol",
true,
VariableClass::BootAndRuntimeTemporary,
)
.context("unable to retain shim protocol")?;
Ok(())
}
}

View File

@@ -0,0 +1,214 @@
use crate::integrations::shim::{ShimInput, ShimSupport, ShimVerificationOutput};
use crate::utils;
use anyhow::{Context, Result, bail};
use log::warn;
use std::sync::{LazyLock, Mutex};
use uefi::proto::device_path::FfiDevicePath;
use uefi::proto::unsafe_protocol;
use uefi::{Guid, guid};
use uefi_raw::Status;
/// GUID for the EFI_SECURITY_ARCH protocol.
const SECURITY_ARCH_GUID: Guid = guid!("a46423e3-4617-49f1-b9ff-d1bfa9115839");
/// GUID for the EFI_SECURITY_ARCH2 protocol.
const SECURITY_ARCH2_GUID: Guid = guid!("94ab2f58-1438-4ef1-9152-18941a3a0e68");
/// EFI_SECURITY_ARCH protocol definition.
#[unsafe_protocol(SECURITY_ARCH_GUID)]
pub struct SecurityArchProtocol {
/// Determines the file authentication state.
pub file_authentication_state: unsafe extern "efiapi" fn(
this: *const SecurityArchProtocol,
status: u32,
path: *mut FfiDevicePath,
) -> Status,
}
/// EFI_SECURITY_ARCH2 protocol definition.
#[unsafe_protocol(SECURITY_ARCH2_GUID)]
pub struct SecurityArch2Protocol {
/// Determines the file authentication.
pub file_authentication: unsafe extern "efiapi" fn(
this: *const SecurityArch2Protocol,
path: *mut FfiDevicePath,
file_buffer: *mut u8,
file_size: usize,
boot_policy: bool,
) -> Status,
}
/// Global state for the security hook.
struct SecurityHookState {
original_hook: SecurityArchProtocol,
original_hook2: SecurityArch2Protocol,
}
/// Global state for the security hook.
/// This is messy, but it is safe given the mutex.
static GLOBAL_HOOK_STATE: LazyLock<Mutex<Option<SecurityHookState>>> =
LazyLock::new(|| Mutex::new(None));
/// Security hook helper.
pub struct SecurityHook;
impl SecurityHook {
/// Shared verifier logic for both hook types.
fn verify(input: ShimInput) -> Status {
// Verify the input.
match ShimSupport::verify(input) {
Ok(output) => match output {
// If the verification failed, return the access-denied status.
ShimVerificationOutput::VerificationFailed => Status::ACCESS_DENIED,
// If the verification succeeded, return the success status.
ShimVerificationOutput::VerifiedDataNotLoaded => Status::SUCCESS,
ShimVerificationOutput::VerifiedDataBuffer(_) => Status::SUCCESS,
},
// If an error occurs, log the error since we can't return a better error.
// Then return the access-denied status.
Err(error) => {
warn!("unable to verify image: {}", error);
Status::ACCESS_DENIED
}
}
}
/// File authentication state verifier for the EFI_SECURITY_ARCH protocol.
/// Takes the `path` and determines the verification.
unsafe extern "efiapi" fn arch_file_authentication_state(
_this: *const SecurityArchProtocol,
_status: u32,
path: *mut FfiDevicePath,
) -> Status {
// Verify the path is not null.
if path.is_null() {
return Status::INVALID_PARAMETER;
}
// Construct a shim input from the path.
let input = ShimInput::SecurityHookPath(path);
// Verify the input.
Self::verify(input)
}
/// File authentication verifier for the EFI_SECURITY_ARCH2 protocol.
/// Takes the `path` and a file buffer to determine the verification.
unsafe extern "efiapi" fn arch2_file_authentication(
_this: *const SecurityArch2Protocol,
path: *mut FfiDevicePath,
file_buffer: *mut u8,
file_size: usize,
boot_policy: bool,
) -> Status {
// Verify the path and file buffer are not null.
if path.is_null() || file_buffer.is_null() {
return Status::INVALID_PARAMETER;
}
// If the boot policy is true, we can't continue as we don't support that.
if boot_policy {
return Status::INVALID_PARAMETER;
}
// Construct a slice out of the file buffer and size.
let buffer = unsafe { std::slice::from_raw_parts(file_buffer, file_size) };
// Construct a shim input from the path.
let input = ShimInput::SecurityHookBuffer(Some(path), buffer);
// Verify the input.
Self::verify(input)
}
/// Install the security hook if needed.
pub fn install() -> Result<bool> {
// Find the security arch protocol. If we can't find it, we will return false.
let Some(hook_arch) = utils::find_handle(&SECURITY_ARCH_GUID)
.context("unable to check security arch existence")?
else {
return Ok(false);
};
// Find the security arch2 protocol. If we can't find it, we will return false.
let Some(hook_arch2) = utils::find_handle(&SECURITY_ARCH2_GUID)
.context("unable to check security arch2 existence")?
else {
return Ok(false);
};
// Open the security arch protocol.
let mut arch_protocol =
uefi::boot::open_protocol_exclusive::<SecurityArchProtocol>(hook_arch)
.context("unable to open security arch protocol")?;
// Open the security arch2 protocol.
let mut arch_protocol2 =
uefi::boot::open_protocol_exclusive::<SecurityArch2Protocol>(hook_arch2)
.context("unable to open security arch2 protocol")?;
// Construct the global state to store.
let state = SecurityHookState {
original_hook: SecurityArchProtocol {
file_authentication_state: arch_protocol.file_authentication_state,
},
original_hook2: SecurityArch2Protocol {
file_authentication: arch_protocol2.file_authentication,
},
};
// Acquire the lock to the global state and replace it.
let Ok(mut global_state) = GLOBAL_HOOK_STATE.lock() else {
bail!("unable to acquire global hook state lock");
};
global_state.replace(state);
// Install the hooks into the UEFI stack.
arch_protocol.file_authentication_state = Self::arch_file_authentication_state;
arch_protocol2.file_authentication = Self::arch2_file_authentication;
Ok(true)
}
/// Uninstalls the global security hook, if installed.
pub fn uninstall() -> Result<()> {
// Find the security arch protocol. If we can't find it, we will do nothing.
let Some(hook_arch) = utils::find_handle(&SECURITY_ARCH_GUID)
.context("unable to check security arch existence")?
else {
return Ok(());
};
// Find the security arch2 protocol. If we can't find it, we will do nothing.
let Some(hook_arch2) = utils::find_handle(&SECURITY_ARCH2_GUID)
.context("unable to check security arch2 existence")?
else {
return Ok(());
};
// Open the security arch protocol.
let mut arch_protocol =
uefi::boot::open_protocol_exclusive::<SecurityArchProtocol>(hook_arch)
.context("unable to open security arch protocol")?;
// Open the security arch2 protocol.
let mut arch_protocol2 =
uefi::boot::open_protocol_exclusive::<SecurityArch2Protocol>(hook_arch2)
.context("unable to open security arch2 protocol")?;
// Acquire the lock to the global state.
let Ok(mut global_state) = GLOBAL_HOOK_STATE.lock() else {
bail!("unable to acquire global hook state lock");
};
// Take the state and replace the original functions.
let Some(state) = global_state.take() else {
return Ok(());
};
// Reinstall the original functions.
arch_protocol.file_authentication_state = state.original_hook.file_authentication_state;
arch_protocol2.file_authentication = state.original_hook2.file_authentication;
Ok(())
}
}

View File

@@ -1,22 +1,34 @@
#![doc = include_str!("../README.md")] #![doc = include_str!("../README.md")]
#![feature(uefi_std)] #![feature(uefi_std)]
extern crate core;
/// The delay to wait for when an error occurs in Sprout.
const DELAY_ON_ERROR: Duration = Duration::from_secs(10);
use crate::config::RootConfiguration;
use crate::context::{RootContext, SproutContext}; use crate::context::{RootContext, SproutContext};
use crate::entries::BootableEntry; use crate::entries::BootableEntry;
use crate::integrations::bootloader_interface::BootloaderInterface;
use crate::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 anyhow::{Context, Result}; use crate::platform::timer::PlatformTimer;
use log::info; use crate::platform::tpm::PlatformTpm;
use crate::secure::SecureBoot;
use crate::utils::PartitionGuidForm;
use anyhow::{Context, Result, bail};
use log::{error, info, warn};
use std::collections::BTreeMap; use std::collections::BTreeMap;
use std::ops::Deref; use std::ops::Deref;
use std::time::Duration; use std::time::Duration;
use uefi::proto::device_path::LoadedImageDevicePath; use uefi::proto::device_path::LoadedImageDevicePath;
use uefi::proto::device_path::text::{AllowShortcuts, DisplayOnly};
/// 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;
/// autoconfigure: Autoconfigure Sprout based on the detected environment.
pub mod autoconfigure;
/// config: Sprout configuration mechanism. /// config: Sprout configuration mechanism.
pub mod config; pub mod config;
@@ -35,12 +47,24 @@ 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.
pub mod integrations;
/// phases: Hooks into specific parts of the boot process. /// phases: Hooks into specific parts of the boot process.
pub mod phases; pub mod phases;
/// sbat: Secure Boot Attestation section.
pub mod sbat;
/// secure: Secure Boot support.
pub mod secure;
/// setup: Code that initializes the UEFI environment for Sprout. /// setup: Code that initializes the UEFI environment for Sprout.
pub mod setup; pub mod setup;
@@ -50,36 +74,78 @@ 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 main entrypoint of sprout. /// Run Sprout, returning an error if one occurs.
/// It is possible this function will not return if actions that are executed fn run() -> Result<()> {
/// exit boot services or do not return control to sprout. // For safety reasons, we will note that Secure Boot is in beta on Sprout.
fn main() -> Result<()> { if SecureBoot::enabled().context("unable to determine Secure Boot status")? {
// Initialize the basic UEFI environment. warn!("Secure Boot is enabled. Sprout Secure Boot is in beta.");
setup::init()?; }
// Start the platform timer.
let timer = PlatformTimer::start();
// Mark the initialization of Sprout in the bootloader interface.
BootloaderInterface::mark_init(&timer)
.context("unable to mark initialization in bootloader interface")?;
// Tell the bootloader interface what firmware we are running on.
BootloaderInterface::set_firmware_info()
.context("unable to set firmware info in bootloader interface")?;
// Tell the bootloader interface what loader is being used.
BootloaderInterface::set_loader_info()
.context("unable to set loader info in bootloader interface")?;
// Acquire the number of active PCR banks on the TPM.
// If no TPM is available, this will return zero.
let active_pcr_banks = PlatformTpm::active_pcr_banks()?;
// Tell the bootloader interface what the number of active PCR banks is.
BootloaderInterface::set_tpm2_active_pcr_banks(active_pcr_banks)
.context("unable to set tpm2 active PCR banks in bootloader interface")?;
// Parse the options to the sprout executable. // Parse the options to the sprout executable.
let options = SproutOptions::parse().context("unable to parse options")?; let options = SproutOptions::parse().context("unable to parse options")?;
// Load the configuration of sprout. // If --autoconfigure is specified, we use a stub configuration.
// At this point, the configuration has been validated and the specified let mut config = if options.autoconfigure {
// version is checked to ensure compatibility. info!("autoconfiguration enabled, configuration file will be ignored");
let config = config::loader::load(&options)?; RootConfiguration::default()
} else {
// Load the configuration of sprout.
// At this point, the configuration has been validated and the specified
// version is checked to ensure compatibility.
config::loader::load(&options)?
};
// Load the root context. // Grab the sprout.efi loaded image path.
// This is done in a block to ensure the release of the LoadedImageDevicePath protocol. // This is done in a block to ensure the release of the LoadedImageDevicePath protocol.
let mut root = { let loaded_image_path = {
let current_image_device_path_protocol = uefi::boot::open_protocol_exclusive::< let current_image_device_path_protocol = uefi::boot::open_protocol_exclusive::<
LoadedImageDevicePath, LoadedImageDevicePath,
>(uefi::boot::image_handle()) >(uefi::boot::image_handle())
.context("unable to get loaded image device path")?; .context("unable to get loaded image device path")?;
let loaded_image_path = current_image_device_path_protocol.deref().to_boxed(); current_image_device_path_protocol.deref().to_boxed()
info!(
"loaded image path: {}",
loaded_image_path.to_string(DisplayOnly(false), AllowShortcuts(false))?
);
RootContext::new(loaded_image_path, options)
}; };
// Grab the partition GUID of the ESP that sprout was loaded from.
let loaded_image_partition_guid =
utils::partition_guid(&loaded_image_path, PartitionGuidForm::Partition)
.context("unable to retrieve loaded image partition guid")?;
// Set the partition GUID of the ESP that sprout was loaded from in the bootloader interface.
if let Some(loaded_image_partition_guid) = loaded_image_partition_guid {
// Tell the system about the partition GUID.
BootloaderInterface::set_partition_guid(&loaded_image_partition_guid)
.context("unable to set partition guid in bootloader interface")?;
}
// Tell the bootloader interface what the loaded image path is.
BootloaderInterface::set_loader_path(&loaded_image_path)
.context("unable to set loader path in bootloader interface")?;
// Create the root context.
let mut root = RootContext::new(loaded_image_path, timer, options);
// Insert the configuration actions into the root context. // Insert the configuration actions into the root context.
root.actions_mut().extend(config.actions.clone()); root.actions_mut().extend(config.actions.clone());
@@ -98,6 +164,34 @@ fn main() -> Result<()> {
// Load all configured drivers. // Load all configured drivers.
drivers::load(context.clone(), &config.drivers).context("unable to load drivers")?; drivers::load(context.clone(), &config.drivers).context("unable to load drivers")?;
// If --autoconfigure is specified or the loaded configuration has autoconfigure enabled,
// trigger the autoconfiguration mechanism.
if context.root().options().autoconfigure || config.options.autoconfigure {
autoconfigure::autoconfigure(&mut config).context("unable to autoconfigure")?;
}
// Unload the context so that it can be modified.
let Some(mut context) = context.unload() else {
bail!("context safety violation while trying to unload context");
};
// Perform root context modification in a block to release the modification when complete.
{
// Modify the root context to include the autoconfigured actions.
let Some(root) = context.root_mut() else {
bail!("context safety violation while trying to modify root context");
};
// Extend the root context with the autoconfigured actions.
root.actions_mut().extend(config.actions);
// Insert any modified root values.
context.insert(&config.values);
}
// Refreeze the context to ensure that further operations can share the context.
let context = context.freeze();
// Run all the extractors declared in the configuration. // Run all the extractors declared in the configuration.
let mut extracted = BTreeMap::new(); let mut extracted = BTreeMap::new();
for (name, extractor) in &config.extractors { for (name, extractor) in &config.extractors {
@@ -131,13 +225,17 @@ fn main() -> Result<()> {
for (name, generator) in config.generators { for (name, generator) in config.generators {
let context = context.fork().freeze(); let context = context.fork().freeze();
// We will prefix all entries with [name]-. // We will prefix all entries with [name]-, provided the name is not pinned.
let prefix = format!("{}-", name); let prefix = format!("{}-", name);
// Add all the entries generated by the generator to the entry list. // Add all the entries generated by the generator to the entry list.
// The generator specifies the context associated with the entry. // The generator specifies the context associated with the entry.
for mut entry in generators::generate(context.clone(), &generator)? { for mut entry in generators::generate(context.clone(), &generator)? {
entry.prepend_name_prefix(&prefix); // If the entry name is not pinned, prepend the name prefix.
if !entry.is_pin_name() {
entry.prepend_name_prefix(&prefix);
}
entries.push(entry); entries.push(entry);
} }
} }
@@ -157,7 +255,7 @@ fn main() -> Result<()> {
entry.restamp_title(); entry.restamp_title();
// Mark this entry as the default entry if it is declared as such. // Mark this entry as the default entry if it is declared as such.
if let Some(ref default_entry) = config.defaults.entry { if let Some(ref default_entry) = config.options.default_entry {
// If the entry matches the default entry, mark it as the default entry. // If the entry matches the default entry, mark it as the default entry.
if entry.is_match(default_entry) { if entry.is_match(default_entry) {
entry.mark_default(); entry.mark_default();
@@ -172,6 +270,21 @@ fn main() -> Result<()> {
entry.mark_default(); entry.mark_default();
} }
// Iterate over all the entries and tell the bootloader interface what the entries are.
for entry in &entries {
// If the entry is the default entry, tell the bootloader interface it is the default.
if entry.is_default() {
// Tell the bootloader interface what the default entry is.
BootloaderInterface::set_default_entry(entry.name().to_string())
.context("unable to set default entry in bootloader interface")?;
break;
}
}
// Tell the bootloader interface what entries are available.
BootloaderInterface::set_entries(entries.iter().map(|entry| entry.name()))
.context("unable to set entries in bootloader interface")?;
// Execute the late phase. // Execute the late phase.
phase(context.clone(), &config.phases.late).context("unable to execute late phase")?; phase(context.clone(), &config.phases.late).context("unable to execute late phase")?;
@@ -186,7 +299,7 @@ fn main() -> Result<()> {
.root() .root()
.options() .options()
.menu_timeout .menu_timeout
.unwrap_or(config.defaults.menu_timeout); .unwrap_or(config.options.menu_timeout);
let menu_timeout = Duration::from_secs(menu_timeout); let menu_timeout = Duration::from_secs(menu_timeout);
// Use the forced boot entry if possible, otherwise pick the first entry using a boot menu. // Use the forced boot entry if possible, otherwise pick the first entry using a boot menu.
@@ -195,9 +308,14 @@ fn main() -> Result<()> {
.context(format!("unable to find entry: {force_boot_entry}"))? .context(format!("unable to find entry: {force_boot_entry}"))?
} else { } else {
// Delegate to the menu to select an entry to boot. // Delegate to the menu to select an entry to boot.
menu::select(menu_timeout, &entries).context("unable to select entry via boot menu")? menu::select(&timer, menu_timeout, &entries)
.context("unable to select entry via boot menu")?
}; };
// Tell the bootloader interface what the selected entry is.
BootloaderInterface::set_selected_entry(entry.name().to_string())
.context("unable to set selected entry in bootloader interface")?;
// Execute all the actions for the selected entry. // Execute all the actions for the selected entry.
for action in &entry.declaration().actions { for action in &entry.declaration().actions {
let action = entry.context().stamp(action); let action = entry.context().stamp(action);
@@ -205,6 +323,28 @@ fn main() -> Result<()> {
.context(format!("unable to execute action '{}'", action))?; .context(format!("unable to execute action '{}'", action))?;
} }
Ok(())
}
/// The main entrypoint of sprout.
/// It is possible this function will not return if actions that are executed
/// exit boot services or do not return control to sprout.
fn main() -> Result<()> {
// Initialize the basic UEFI environment.
setup::init()?;
// Run Sprout, then handle the error.
let result = run();
if let Err(ref error) = result {
// Print an error trace.
error!("sprout encountered an error");
for (index, stack) in error.chain().enumerate() {
error!("[{}]: {}", index, stack);
}
// Sleep to allow the user to read the error.
uefi::boot::stall(DELAY_ON_ERROR);
}
// 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(()) Ok(())

View File

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

View File

@@ -11,6 +11,8 @@ const DEFAULT_CONFIG_PATH: &str = "\\sprout.toml";
/// The parsed options of sprout. /// The parsed options of sprout.
#[derive(Debug)] #[derive(Debug)]
pub struct SproutOptions { pub struct SproutOptions {
/// Configures Sprout automatically based on the environment.
pub autoconfigure: bool,
/// Path to a configuration file to load. /// Path to a configuration file to load.
pub config: String, pub config: String,
/// Entry to boot without showing the boot menu. /// Entry to boot without showing the boot menu.
@@ -25,6 +27,7 @@ pub struct SproutOptions {
impl Default for SproutOptions { impl Default for SproutOptions {
fn default() -> Self { fn default() -> Self {
Self { Self {
autoconfigure: false,
config: DEFAULT_CONFIG_PATH.to_string(), config: DEFAULT_CONFIG_PATH.to_string(),
boot: None, boot: None,
force_menu: false, force_menu: false,
@@ -86,6 +89,11 @@ impl OptionsRepresentable for SproutOptions {
for (key, value) in options { for (key, value) in options {
match key.as_str() { match key.as_str() {
"autoconfigure" => {
// Enable autoconfiguration.
result.autoconfigure = true;
}
"config" => { "config" => {
// The configuration file to load. // The configuration file to load.
result.config = value.context("--config option requires a value")?; result.config = value.context("--config option requires a value")?;

View File

@@ -1,4 +1,5 @@
use anyhow::{Context, Result, bail}; use anyhow::{Context, Result, bail};
use log::info;
use std::collections::BTreeMap; use std::collections::BTreeMap;
/// The type of option. This disambiguates different behavior /// The type of option. This disambiguates different behavior
@@ -95,7 +96,7 @@ pub trait OptionsRepresentable {
let maybe_next = iterator.peek(); let maybe_next = iterator.peek();
// If the next value isn't another option, set the value to the next value. // If the next value isn't another option, set the value to the next value.
// Otherwise, it is an empty string. // Otherwise, it is None.
value = if let Some(next) = maybe_next value = if let Some(next) = maybe_next
&& !next.starts_with("--") && !next.starts_with("--")
{ {
@@ -113,9 +114,9 @@ pub trait OptionsRepresentable {
// Handle the --help flag case. // Handle the --help flag case.
if description.form == OptionForm::Help { if description.form == OptionForm::Help {
// Generic configured options output. // Generic configured options output.
println!("Configured Options:"); info!("Configured Options:");
for (name, description) in &configured { for (name, description) in &configured {
println!( info!(
" --{}{}: {}", " --{}{}: {}",
name, name,
if description.form == OptionForm::Value { if description.form == OptionForm::Value {

View File

@@ -7,7 +7,7 @@ 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.
#[derive(Serialize, Deserialize, Default, Clone)] #[derive(Serialize, Deserialize, Debug, Default, Clone)]
pub struct PhasesConfiguration { pub struct PhasesConfiguration {
/// The early phase is run before drivers are loaded. /// The early phase is run before drivers are loaded.
#[serde(default)] #[serde(default)]
@@ -23,7 +23,7 @@ pub struct PhasesConfiguration {
/// Configures a single phase of the boot process. /// Configures a single phase of the boot process.
/// There can be multiple phase configurations that are /// There can be multiple phase configurations that are
/// executed sequentially. /// executed sequentially.
#[derive(Serialize, Deserialize, Default, Clone)] #[derive(Serialize, Deserialize, Debug, Default, Clone)]
pub struct PhaseConfiguration { pub struct PhaseConfiguration {
/// The actions to run when the phase is executed. /// The actions to run when the phase is executed.
#[serde(default)] #[serde(default)]

4
src/platform.rs Normal file
View File

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

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

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

View File

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

View File

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

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

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

11
src/sbat.rs Normal file
View File

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

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

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

14
src/secure.rs Normal file
View File

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

View File

@@ -1,10 +1,13 @@
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use std::ops::Deref; use std::ops::Deref;
use uefi::boot::SearchType;
use uefi::fs::{FileSystem, Path}; use uefi::fs::{FileSystem, Path};
use uefi::proto::device_path::text::{AllowShortcuts, DevicePathFromText, DisplayOnly}; use uefi::proto::device_path::text::{AllowShortcuts, DevicePathFromText, DisplayOnly};
use uefi::proto::device_path::{DevicePath, PoolDevicePath}; use uefi::proto::device_path::{DevicePath, PoolDevicePath};
use uefi::proto::media::fs::SimpleFileSystem; use uefi::proto::media::fs::SimpleFileSystem;
use uefi::{CString16, Handle}; use uefi::proto::media::partition::PartitionInfo;
use uefi::{CString16, Guid, Handle};
use uefi_raw::Status;
/// Support code for the EFI framebuffer. /// Support code for the EFI framebuffer.
pub mod framebuffer; pub mod framebuffer;
@@ -12,6 +15,9 @@ pub mod framebuffer;
/// Support code for the media loader protocol. /// Support code for the media loader protocol.
pub mod media_loader; pub mod media_loader;
/// Support code for EFI variables.
pub mod variables;
/// Parses the input `path` as a [DevicePath]. /// Parses the input `path` as a [DevicePath].
/// Uses the [DevicePathFromText] protocol exclusively, and will fail if it cannot acquire the protocol. /// Uses the [DevicePathFromText] protocol exclusively, and will fail if it cannot acquire the protocol.
pub fn text_to_device_path(path: &str) -> Result<PoolDevicePath> { pub fn text_to_device_path(path: &str) -> Result<PoolDevicePath> {
@@ -98,10 +104,24 @@ pub struct ResolvedPath {
pub filesystem_handle: Handle, pub filesystem_handle: Handle,
} }
impl ResolvedPath {
/// Read the file specified by this path into a buffer and return it.
pub fn read_file(&self) -> Result<Vec<u8>> {
let fs = uefi::boot::open_protocol_exclusive::<SimpleFileSystem>(self.filesystem_handle)
.context("unable to open filesystem protocol")?;
let mut fs = FileSystem::new(fs);
let path = self
.sub_path
.to_string(DisplayOnly(false), AllowShortcuts(false))?;
let content = fs.read(Path::new(&path));
content.context("unable to read file contents")
}
}
/// Resolve a path specified by `input` to its various components. /// Resolve a path specified by `input` to its various components.
/// Uses `default_root_path` as the base root if one is not specified in the path. /// Uses `default_root_path` as the base root if one is not specified in the path.
/// Returns [ResolvedPath] which contains the resolved components. /// Returns [ResolvedPath] which contains the resolved components.
pub fn resolve_path(default_root_path: &DevicePath, input: &str) -> Result<ResolvedPath> { pub fn resolve_path(default_root_path: Option<&DevicePath>, input: &str) -> Result<ResolvedPath> {
let mut path = text_to_device_path(input).context("unable to convert text to path")?; let mut path = text_to_device_path(input).context("unable to convert text to path")?;
let path_has_device = path let path_has_device = path
.node_iter() .node_iter()
@@ -117,6 +137,9 @@ pub fn resolve_path(default_root_path: &DevicePath, input: &str) -> Result<Resol
if !input.starts_with('\\') { if !input.starts_with('\\') {
input.insert(0, '\\'); input.insert(0, '\\');
} }
let default_root_path = default_root_path.context("unable to get default root path")?;
input.insert_str( input.insert_str(
0, 0,
device_path_root(default_root_path) device_path_root(default_root_path)
@@ -131,8 +154,11 @@ pub fn resolve_path(default_root_path: &DevicePath, input: &str) -> Result<Resol
let root_path = text_to_device_path(root.as_str()) let root_path = text_to_device_path(root.as_str())
.context("unable to convert root to path")? .context("unable to convert root to path")?
.to_boxed(); .to_boxed();
let mut root_path = root_path.as_ref(); let root_path = root_path.as_ref();
let handle = uefi::boot::locate_device_path::<SimpleFileSystem>(&mut root_path)
// locate_device_path modifies the path, so we need to clone it.
let root_path_modifiable = root_path.to_owned();
let handle = uefi::boot::locate_device_path::<SimpleFileSystem>(&mut &*root_path_modifiable)
.context("unable to locate filesystem device path")?; .context("unable to locate filesystem device path")?;
let subpath = device_path_subpath(path.deref()).context("unable to get device subpath")?; let subpath = device_path_subpath(path.deref()).context("unable to get device subpath")?;
Ok(ResolvedPath { Ok(ResolvedPath {
@@ -150,14 +176,99 @@ pub fn resolve_path(default_root_path: &DevicePath, input: &str) -> Result<Resol
/// This acquires exclusive protocol access to the [SimpleFileSystem] protocol of the resolved /// This acquires exclusive protocol access to the [SimpleFileSystem] protocol of the resolved
/// filesystem handle, so care must be taken to call this function outside a scope with /// filesystem handle, so care must be taken to call this function outside a scope with
/// the filesystem handle protocol acquired. /// the filesystem handle protocol acquired.
pub fn read_file_contents(default_root_path: &DevicePath, input: &str) -> Result<Vec<u8>> { pub fn read_file_contents(default_root_path: Option<&DevicePath>, input: &str) -> Result<Vec<u8>> {
let resolved = resolve_path(default_root_path, input)?; let resolved = resolve_path(default_root_path, input)?;
let fs = uefi::boot::open_protocol_exclusive::<SimpleFileSystem>(resolved.filesystem_handle) resolved.read_file()
.context("unable to open filesystem protocol")?; }
let mut fs = FileSystem::new(fs);
let path = resolved /// Filter a string-like Option `input` such that an empty string is [None].
.sub_path pub fn empty_is_none<T: AsRef<str>>(input: Option<T>) -> Option<T> {
.to_string(DisplayOnly(false), AllowShortcuts(false))?; input.filter(|input| !input.as_ref().is_empty())
let content = fs.read(Path::new(&path)); }
content.context("unable to read file contents")
/// 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")
}
}
}
} }

View File

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

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

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