123 Commits

Author SHA1 Message Date
524d0871f3 sprout: version 0.0.17 2025-11-02 16:28:07 -05:00
f0628f77e2 fix(shim): repair x86_64 shim verification by using the SYSV calling convention 2025-11-02 05:57:24 -05:00
cc37c2b26a fix(shim): use pinned boxed slices to represent data that needs to be passed to uefi stack 2025-11-02 05:08:03 -05:00
8d403d74c9 fix(shim/hook): clarify const-ness of some parameters 2025-11-02 04:16:05 -05:00
cc4bc6efcc fix(shim/hook): when using older hook protocol, read the data into an owned buffer 2025-11-02 02:48:41 -05:00
d4bcfcd9b1 fix(shim): reflect the const pointer-ness of the verifiable data 2025-11-02 02:45:25 -05:00
c34462b812 fix(options): accidental infinite loop due when not running on Dell firmware 2025-11-02 02:21:29 -05:00
79471f6862 fix(quirk): skip initial options that start with a tilde to work around dell firmware 2025-11-02 02:13:39 -05:00
9c31dba6fa fix(shim): only call into shim if it is available AND secure boot is enabled 2025-11-02 01:52:21 -05:00
84d60e09be fix(bootloader-interface): when there are no entries, don't attempt to set LoaderEntries 2025-11-02 01:38:07 -05:00
eabb612330 fix(shim/hook): call original hook function if the shim verify fails 2025-11-02 01:07:16 -05:00
1a6ed0af99 fix(shim): avoid masking the underlying error when shim verify fails 2025-11-02 00:27:45 -04:00
3b4a66879f fix(autoconfigure/linux): sort kernels by version, newer kernels first 2025-11-01 22:06:48 -04:00
01b3706914 fix(bls): quirk for when the version field is present already in the title 2025-11-01 21:40:39 -04:00
5033bc7bf4 fix(menu): only show the title, not the name of the entry 2025-11-01 21:36:17 -04:00
f6441b5694 feat(bls): add version comparison to ensure entries are always sorted properly 2025-11-01 21:34:55 -04:00
03d0e40141 fix(bls): add support for appending version to the title 2025-11-01 21:03:02 -04:00
3a54970386 feat(bls): initial support for sorting of entries, not using version comparison 2025-11-01 21:00:54 -04:00
8a5dc33b5a fix(options): add missing --autoconfigure flag 2025-11-01 20:07:32 -04:00
757d10ec65 fix(timer): add check for zero frequency 2025-11-01 19:18:04 -04:00
3b32f6c3ce fix(platform/timer/x86_64): if frequency is zero, panic 2025-11-01 19:08:47 -04:00
6b1d220490 fix(bls): skip over any files named ".conf" to avoid empty names 2025-11-01 18:59:41 -04:00
f7558fd024 fix(menu): prevent masking of wait for event errors if closing the timer event fails 2025-11-01 18:58:24 -04:00
d1fd13163f fix(filesystem-device-match): make behavior of ignoring filesystem errors more explicit 2025-11-01 18:51:07 -04:00
a998832f6b fix(utils): improve safety of media loader and utf-16 handling 2025-11-01 18:49:10 -04:00
992520c201 chore(main): collapse duplicate code for menu hidden or disabled 2025-11-01 18:38:08 -04:00
e9cba9da33 fix(menu): handle event freeing if wait for event fails 2025-11-01 18:37:07 -04:00
0f8f12c70f fix(bootloader-interface): fix menu time marking 2025-11-01 18:35:07 -04:00
1c732a1c43 feat(bootloader-interface): add support for LoaderEntryDefault and LoaderEntryOneShot 2025-11-01 18:04:06 -04:00
08b9e2570e fix(bootloader-interface): disable setting of LoaderEntryDefault since this is intended to be user set 2025-11-01 17:50:54 -04:00
f361570b0e feat(bootloader-interface): add support for LoaderConfigTimeout and LoaderConfigTimeoutOneShot 2025-11-01 17:47:41 -04:00
679b0c0290 feat(bootloader-interface): signal support for XBOOTLDR 2025-11-01 17:09:49 -04:00
f9dd56c8e7 feat(bootloader-interface): add support for LoaderFeatures 2025-11-01 03:24:14 -04:00
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
63 changed files with 3260 additions and 383 deletions

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

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

116
Cargo.lock generated
View File

@@ -14,6 +14,17 @@ version = "1.0.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
[[package]]
name = "async-trait"
version = "0.1.89"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "autocfg"
version = "1.5.0"
@@ -32,6 +43,15 @@ version = "2.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "bytemuck"
version = "1.24.0"
@@ -44,12 +64,27 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
[[package]]
name = "bytes"
version = "1.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
[[package]]
name = "cfg-if"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "cpufeatures"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
dependencies = [
"libc",
]
[[package]]
name = "crc32fast"
version = "1.5.0"
@@ -59,14 +94,36 @@ dependencies = [
"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]]
name = "edera-sprout"
version = "0.0.11"
version = "0.0.17"
dependencies = [
"anyhow",
"bitflags",
"image",
"log",
"serde",
"sha256",
"toml",
"uefi",
"uefi-raw",
@@ -97,12 +154,28 @@ dependencies = [
"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]]
name = "hashbrown"
version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d"
[[package]]
name = "hex"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "image"
version = "0.25.8"
@@ -126,6 +199,12 @@ dependencies = [
"hashbrown",
]
[[package]]
name = "libc"
version = "0.2.177"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976"
[[package]]
name = "log"
version = "0.4.28"
@@ -259,6 +338,29 @@ dependencies = [
"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]]
name = "simd-adler32"
version = "0.3.7"
@@ -314,6 +416,12 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df8b2b54733674ad286d16267dcfc7a71ed5c776e4ac7aa3c3e2561f7c637bf2"
[[package]]
name = "typenum"
version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
[[package]]
name = "ucs2"
version = "0.3.3"
@@ -372,6 +480,12 @@ version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "462eeb75aeb73aea900253ce739c8e18a67423fadf006037cd3ff27e82748a06"
[[package]]
name = "version_check"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "winnow"
version = "0.7.13"

View File

@@ -2,18 +2,19 @@
name = "edera-sprout"
description = "Modern UEFI bootloader"
license = "Apache-2.0"
version = "0.0.11"
version = "0.0.17"
homepage = "https://sprout.edera.dev"
repository = "https://github.com/edera-dev/sprout"
edition = "2024"
[dependencies]
anyhow = "1.0.100"
bitflags = "2.10.0"
toml = "0.9.8"
log = "0.4.28"
[dependencies.image]
version = "0.25.6"
version = "0.25.8"
default-features = false
features = ["png"]
optional = true
@@ -22,6 +23,10 @@ optional = true
version = "1.0.228"
features = ["derive"]
[dependencies.sha256]
version = "1.6.0"
default-features = false
[dependencies.uefi]
version = "0.36.0"
features = ["alloc", "logger"]
@@ -33,6 +38,11 @@ version = "0.12.0"
default = ["splash"]
splash = ["dep:image"]
[profile.dev]
# We have to compile for opt-level = 2 due to optimization passes
# which don't handle the UEFI target properly.
opt-level = 2
[profile.release]
lto = "thin"
strip = "symbols"
@@ -46,6 +56,7 @@ debug = 1
inherits = "dev"
strip = "debuginfo"
debug = 0
opt-level = 2
[patch.crates-io.simd-adler32]
git = "https://github.com/edera-dev/sprout-patched-deps.git"

View File

@@ -2,7 +2,7 @@
ARG RUST_PROFILE=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
ARG RUST_PROFILE
RUN adduser -S -s /bin/sh build

View File

@@ -6,11 +6,11 @@
</div>
Sprout is an **EXPERIMENTAL** programmable UEFI bootloader written in Rust.
Sprout is a programmable UEFI bootloader written in Rust.
Sprout is in use at Edera today in development environments and is intended to ship to production soon.
It is in use at Edera today in development environments and is intended to ship to production soon.
The name "sprout" is derived from our company name "Edera" which means "ivy."
The name "Sprout" is derived from our company name "Edera" which means "ivy."
Given that Sprout is the first thing intended to start on an Edera system, the name was apt.
It supports `x86_64` and `ARM64` EFI-capable systems. It is designed to require UEFI and can be chainloaded from an
@@ -21,10 +21,10 @@ Sprout is licensed under Apache 2.0 and is open to modifications and contributio
## Background
At [Edera] we make compute isolation technology for a wide variety of environments, often ones we do not fully control.
Our technology utilizes a hypervisor to boot the host system to provide a new isolation mechanism that works
with or without hardware virtualization support. To do this we need to inject our hypervisor at boot time.
Our technology uses a hypervisor to boot the host system to provide a new isolation mechanism that works
with or without hardware virtualization support. To do this, we need to inject our hypervisor at boot time.
Unfortunately, GRUB, the most common bootloader on Linux systems today, utilizes a shell-script like
Unfortunately, GRUB, the most common bootloader on Linux systems today, uses a shell-script like
configuration system. Both the code that runs to generate a GRUB config and the GRUB config
itself is fully turing complete. This makes modifying boot configuration difficult and error-prone.
@@ -49,31 +49,33 @@ simplify installation and usage.
## Features
NOTE: Currently, Sprout is experimental and is not intended for production use.
The boot menu mechanism is very rudimentary.
**NOTE**: Sprout is still in beta.
### Current
- [x] Loadable driver support
- [x] [Bootloader specification (BLS)](https://uapi-group.org/specifications/specs/boot_loader_specification/) support
- [x] Basic [Bootloader specification (BLS)](https://uapi-group.org/specifications/specs/boot_loader_specification/) support
- [x] Chainload support
- [x] Linux boot support via EFI stub
- [x] Windows boot support via chainload
- [x] Load Linux initrd from disk
- [x] Basic boot menu
- [x] BLS autoconfiguration support
- [x] [Secure Boot support](https://github.com/edera-dev/sprout/issues/20): partial
### Roadmap
- [ ] Full-featured boot menu
- [ ] Secure Boot support: work in progress
- [ ] UKI support: partial
- [ ] multiboot2 support
- [ ] Linux boot protocol (boot without EFI stub)
- [ ] [Bootloader interface support](https://github.com/edera-dev/sprout/issues/21)
- [ ] [BLS specification conformance](https://github.com/edera-dev/sprout/issues/2)
- [ ] [Full-featured boot menu](https://github.com/edera-dev/sprout/issues/1)
- [ ] [UKI support](https://github.com/edera-dev/sprout/issues/6): partial
- [ ] [multiboot2 support](https://github.com/edera-dev/sprout/issues/7)
- [ ] [Linux boot protocol (boot without EFI stub)](https://github.com/edera-dev/sprout/issues/7)
## Concepts
- drivers: loadable EFI modules that can add functionality to the EFI system.
- autoconfiguration: code that can automatically generate sprout.toml based on the EFI environment.
- actions: executable code with a configuration that can be run by various other sprout concepts.
- generators: code that can generate boot entries based on inputs or runtime code.
- extractors: code that can extract values from the EFI environment.
@@ -138,7 +140,7 @@ version = 1
path = "\\sprout\\drivers\\ext4.efi"
# global options.
[defaults]
[options]
# enable autoconfiguration by detecting bls enabled
# filesystems and generating boot entries for them.
autoconfigure = true

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

@@ -40,7 +40,7 @@ version = 1
path = "\\sprout\\drivers\\ext4.efi"
# global options.
[defaults]
[options]
# enable autoconfiguration by detecting bls enabled
# filesystems and generating boot entries for them.
autoconfigure = true

View File

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

View File

@@ -7,7 +7,7 @@ RUN export DEBIAN_FRONTEND=noninteractive && apt-get update && apt-get install -
WORKDIR /work
COPY sprout.efi /work/${EFI_NAME}.EFI
COPY sprout.toml /work/SPROUT.TOML
COPY kernel.efi /work/KERNEL.EFI
COPY kernel.efi /work/VMLINUZ
COPY shell.efi /work/SHELL.EFI
COPY xen.efi /work/XEN.EFI
COPY xen.cfg /work/XEN.CFG
@@ -24,7 +24,7 @@ RUN truncate -s128MiB sprout.img && \
mmd -i sprout.img ::/LOADER && \
mmd -i sprout.img ::/LOADER/ENTRIES && \
mcopy -i sprout.img ${EFI_NAME}.EFI ::/EFI/BOOT/ && \
mcopy -i sprout.img KERNEL.EFI ::/EFI/BOOT/ && \
mcopy -i sprout.img VMLINUZ ::/VMLINUZ && \
mcopy -i sprout.img SHELL.EFI ::/EFI/BOOT/ && \
mcopy -i sprout.img XEN.EFI ::/EFI/BOOT/ && \
mcopy -i sprout.img XEN.CFG ::/EFI/BOOT/ && \

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,9 +1,11 @@
use crate::context::SproutContext;
use crate::integrations::bootloader_interface::BootloaderInterface;
use crate::integrations::shim::{ShimInput, ShimSupport};
use crate::utils;
use crate::utils::media_loader::MediaLoaderHandle;
use crate::utils::media_loader::constants::linux::LINUX_EFI_INITRD_MEDIA_GUID;
use anyhow::{Context, Result, bail};
use log::{error, info};
use log::error;
use serde::{Deserialize, Serialize};
use std::rc::Rc;
use uefi::CString16;
@@ -34,37 +36,28 @@ pub fn chainload(context: Rc<SproutContext>, configuration: &ChainloadConfigurat
// Resolve the path to the image to chainload.
let resolved = utils::resolve_path(
context.root().loaded_image_path()?,
Some(context.root().loaded_image_path()?),
&context.stamp(&configuration.path),
)
.context("unable to resolve chainload path")?;
// Load the image to chainload.
let image = uefi::boot::load_image(
sprout_image,
uefi::boot::LoadImageSource::FromDevicePath {
device_path: &resolved.full_path,
boot_policy: uefi::proto::BootPolicy::ExactMatch,
},
)
.context("unable to load image")?;
// Load the image to chainload using the shim support integration.
// It will determine if the image needs to be loaded via the shim or can be loaded directly.
let image = ShimSupport::load(sprout_image, ShimInput::ResolvedPath(&resolved))?;
// Open the LoadedImage protocol of the image to chainload.
let mut loaded_image_protocol = uefi::boot::open_protocol_exclusive::<LoadedImage>(image)
.context("unable to open loaded image protocol")?;
// Stamp and concatenate the options to pass to the image.
let options = configuration
.options
.iter()
.map(|item| context.stamp(item))
.collect::<Vec<_>>()
.join(" ");
// Stamp and combine the options to pass to the image.
let options =
utils::combine_options(configuration.options.iter().map(|item| context.stamp(item)));
// 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,
// 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;
if !options.is_empty() {
let options = Box::new(
@@ -72,8 +65,6 @@ pub fn chainload(context: Rc<SproutContext>, configuration: &ChainloadConfigurat
.context("unable to convert chainloader options to CString16")?,
);
info!("options: {}", options);
if options.num_bytes() > u32::MAX as usize {
bail!("chainloader options too large");
}
@@ -88,10 +79,19 @@ pub fn chainload(context: Rc<SproutContext>, configuration: &ChainloadConfigurat
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;
if let Some(ref linux_initrd) = configuration.linux_initrd {
let initrd_path = context.stamp(linux_initrd);
let content = utils::read_file_contents(context.root().loaded_image_path()?, &initrd_path)
if let Some(linux_initrd) = initrd {
let content =
utils::read_file_contents(Some(context.root().loaded_image_path()?), &linux_initrd)
.context("unable to read linux initrd")?;
let handle =
MediaLoaderHandle::register(LINUX_EFI_INITRD_MEDIA_GUID, content.into_boxed_slice())
@@ -99,11 +99,15 @@ pub fn chainload(context: Rc<SproutContext>, configuration: &ChainloadConfigurat
initrd_handle = Some(handle);
}
// Mark execution of an entry in the bootloader interface.
BootloaderInterface::mark_exec(context.root().timer())
.context("unable to mark execution of boot entry in bootloader interface")?;
// Start the loaded image.
// This call might return, or it may pass full control to another image that will never return.
// Capture the result to ensure we can return an error if the image fails to start, but only
// 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.
if let Some(initrd_handle) = initrd_handle

View File

@@ -40,7 +40,23 @@ pub struct EderaConfiguration {
}
/// 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
[
// global section
@@ -50,10 +66,10 @@ fn build_xen_config(configuration: &EderaConfiguration) -> String {
// configuration section for sprout
"[sprout]".to_string(),
// xen options
format!("options={}", configuration.xen_options.join(" ")),
format!("options={}", xen_options),
// kernel options, stub replaces the kernel path
// 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
"".to_string(),
]
@@ -82,7 +98,7 @@ fn register_media_loader_file(
// Stamp the path to the file.
let path = context.stamp(path);
// Read the file contents.
let content = utils::read_file_contents(context.root().loaded_image_path()?, &path)
let content = utils::read_file_contents(Some(context.root().loaded_image_path()?), &path)
.context(format!("unable to read {} file", what))?;
// Register the media loader.
let handle = MediaLoaderHandle::register(guid, content.into_boxed_slice())
@@ -94,7 +110,7 @@ fn register_media_loader_file(
/// `configuration` and `context`. This action uses Edera-specific Xen EFI stub functionality.
pub fn edera(context: Rc<SproutContext>, configuration: &EderaConfiguration) -> Result<()> {
// 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.
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];
// 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 =
register_media_loader_file(&context, XEN_EFI_RAMDISK_MEDIA_GUID, "initrd", initrd)
.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");
// Unregister the media loaders on error.
// Unregister the media loaders when an error happens.
for media_loader in media_loaders {
if let Err(error) = media_loader.unregister() {
error!("unable to unregister media loader: {}", error);

View File

@@ -1,7 +1,7 @@
use crate::context::SproutContext;
use crate::utils::framebuffer::Framebuffer;
use crate::utils::read_file_contents;
use anyhow::{Context, Result};
use anyhow::{Context, Result, bail};
use image::imageops::{FilterType, resize};
use image::math::Rect;
use image::{DynamicImage, ImageBuffer, ImageFormat, ImageReader, Rgba};
@@ -12,6 +12,8 @@ use std::time::Duration;
use uefi::boot::ScopedProtocol;
use uefi::proto::console::gop::GraphicsOutput;
/// We set the default splash time to zero, as this makes it so any logging shows up
/// on top of the splash and does not hold up the boot process.
const DEFAULT_SPLASH_TIME: u32 = 0;
/// The configuration of the splash action.
@@ -116,6 +118,11 @@ fn draw(image: DynamicImage) -> Result<()> {
// Fit the image to the display frame.
let fit = fit_to_frame(&image, display_frame);
// If the image is zero-sized, then we should bail with an error.
if fit.width == 0 || fit.height == 0 {
bail!("calculated frame size is zero");
}
// Resize the image to fit the display frame.
let image = resize_to_fit(&image, fit);
@@ -143,7 +150,7 @@ pub fn splash(context: Rc<SproutContext>, configuration: &SplashConfiguration) -
// Stamp the image path value.
let image = context.stamp(&configuration.image);
// Read the image contents.
let image = read_file_contents(context.root().loaded_image_path()?, &image)?;
let image = read_file_contents(Some(context.root().loaded_image_path()?), &image)?;
// Decode the image as a PNG.
let image = ImageReader::with_format(Cursor::new(image), ImageFormat::Png)
.decode()

View File

@@ -1,101 +1,19 @@
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 anyhow::{Context, Result};
use uefi::cstr16;
use uefi::fs::{FileSystem, Path};
use uefi::fs::FileSystem;
use uefi::proto::device_path::DevicePath;
use uefi::proto::device_path::text::{AllowShortcuts, DisplayOnly};
use uefi::proto::media::fs::SimpleFileSystem;
/// 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-";
/// bls: autodetect and configure BLS-enabled filesystems.
pub mod bls;
/// Scan the specified `filesystem` for BLS configurations.
fn scan_for_bls(
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"));
/// 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;
// 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 slash to the root to ensure the path is valid.
root.push('/');
// 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);
// 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!("autoconfigure-bls-{}", root),
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)
}
/// 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.
@@ -121,8 +39,18 @@ pub fn autoconfigure(config: &mut RootConfiguration) -> Result<()> {
let mut filesystem = FileSystem::new(filesystem);
// Scan the filesystem for BLS supported configurations.
// If we find any, we will add a BLS generator to the configuration.
scan_for_bls(&mut filesystem, &root, config).context("unable to scan filesystem")?;
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)
}

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

@@ -0,0 +1,241 @@
use crate::actions::ActionDeclaration;
use crate::actions::chainload::ChainloadConfiguration;
use crate::config::RootConfiguration;
use crate::entries::EntryDeclaration;
use crate::generators::GeneratorDeclaration;
use crate::generators::list::ListConfiguration;
use crate::utils;
use crate::utils::vercmp;
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);
}
// Sort the kernel pairs by kernel version, if it has one, newer kernels first.
pairs.sort_by(|a, b| vercmp::compare_versions(&a.kernel, &b.kernel).reverse());
// Generate a unique name for the linux chainload action.
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

@@ -27,7 +27,7 @@ pub struct RootConfiguration {
pub version: u32,
/// Default options for Sprout.
#[serde(default)]
pub defaults: DefaultsConfiguration,
pub options: OptionsConfiguration,
/// Values to be inserted into the root sprout context.
#[serde(default)]
pub values: BTreeMap<String, String>,
@@ -65,16 +65,18 @@ pub struct RootConfiguration {
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, Debug, Default, Clone)]
pub struct DefaultsConfiguration {
pub struct OptionsConfiguration {
/// The entry to boot without showing the boot menu.
/// 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.
#[serde(rename = "menu-timeout", default = "default_menu_timeout")]
pub menu_timeout: u64,
/// Enables autoconfiguration of Sprout based on the environment.
#[serde(default)]
pub autoconfigure: bool,
}

View File

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

View File

@@ -1,5 +1,6 @@
use crate::actions::ActionDeclaration;
use crate::options::SproutOptions;
use crate::platform::timer::PlatformTimer;
use anyhow::anyhow;
use anyhow::{Result, bail};
use std::cmp::Reverse;
@@ -12,22 +13,29 @@ const CONTEXT_FINALIZE_ITERATION_LIMIT: usize = 100;
/// Declares a root context for Sprout.
/// This contains data that needs to be shared across Sprout.
#[derive(Default)]
pub struct RootContext {
/// The actions that are available in Sprout.
actions: BTreeMap<String, ActionDeclaration>,
/// The device path of the loaded Sprout image.
loaded_image_path: Option<Box<DevicePath>>,
/// Platform timer started at the beginning of the boot process.
timer: PlatformTimer,
/// The global options of Sprout.
options: SproutOptions,
}
impl RootContext {
/// Creates a new root context with the `loaded_image_device_path` which will be stored
/// in the context for easy access.
pub fn new(loaded_image_device_path: Box<DevicePath>, options: SproutOptions) -> Self {
/// in the context for easy access. We also provide a `timer` which is used to measure elapsed
/// time for the bootloader.
pub fn new(
loaded_image_device_path: Box<DevicePath>,
timer: PlatformTimer,
options: SproutOptions,
) -> Self {
Self {
actions: BTreeMap::new(),
timer,
loaded_image_path: Some(loaded_image_device_path),
options,
}
@@ -43,6 +51,11 @@ impl RootContext {
&mut self.actions
}
/// Access the platform timer that is started at the beginning of the boot process.
pub fn timer(&self) -> &PlatformTimer {
&self.timer
}
/// Access the device path of the loaded Sprout image.
pub fn loaded_image_path(&self) -> Result<&DevicePath> {
self.loaded_image_path
@@ -118,7 +131,10 @@ impl SproutContext {
pub fn all_values(&self) -> BTreeMap<String, String> {
let mut values = BTreeMap::new();
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
}
@@ -165,13 +181,13 @@ impl SproutContext {
let mut current_values = self.all_values();
// To ensure that there is no possible infinite loop, we need to check
// the number of iterations. If it exceeds 100, we bail.
// the number of iterations. If it exceeds CONTEXT_FINALIZE_ITERATION_LIMIT, we bail.
let mut iterations: usize = 0;
loop {
iterations += 1;
if iterations > CONTEXT_FINALIZE_ITERATION_LIMIT {
bail!("infinite loop detected in context finalization");
bail!("maximum number of replacement iterations reached while finalizing context");
}
let mut did_change = false;
@@ -203,6 +219,14 @@ impl SproutContext {
/// Stamps the `text` value with the specified `values` map. The returned value indicates
/// whether the `text` has been changed and the value that was stamped and changed.
///
/// Stamping works like this:
/// - Start with the input text.
/// - Sort all the keys in reverse length order (longest keys first)
/// - For each key, if the key is not empty, replace $KEY in the text.
/// - Each follow-up iteration acts upon the last iterations result.
/// - We keep track if the text changes during the replacement.
/// - We return both whether the text changed during any iteration and the final result.
fn stamp_values(values: &BTreeMap<String, String>, text: impl AsRef<str>) -> (bool, String) {
let mut result = text.as_ref().to_string();
let mut did_change = false;

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,7 @@
use crate::context::SproutContext;
use crate::entries::BootableEntry;
use crate::generators::bls::BlsConfiguration;
use crate::generators::list::ListConfiguration;
use crate::generators::matrix::MatrixConfiguration;
use anyhow::Result;
use anyhow::bail;
@@ -8,6 +9,7 @@ use serde::{Deserialize, Serialize};
use std::rc::Rc;
pub mod bls;
pub mod list;
pub mod matrix;
/// Declares a generator configuration.
@@ -32,6 +34,9 @@ pub struct GeneratorDeclaration {
/// It will generate a sprout entry for every supported BLS entry.
#[serde(default)]
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.
@@ -45,6 +50,8 @@ pub fn generate(
matrix::generate(context, matrix)
} else if let Some(bls) = &generator.bls {
bls::generate(context, bls)
} else if let Some(list) = &generator.list {
list::generate(context, list)
} else {
bail!("unknown generator configuration");
}

View File

@@ -2,8 +2,10 @@ use crate::context::SproutContext;
use crate::entries::{BootableEntry, EntryDeclaration};
use crate::generators::bls::entry::BlsEntry;
use crate::utils;
use crate::utils::vercmp;
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::cmp::Ordering;
use std::rc::Rc;
use std::str::FromStr;
use uefi::cstr16;
@@ -40,6 +42,60 @@ fn quirk_initrd_remove_tuned(input: String) -> String {
input.replace("$tuned_initrd", "").trim().to_string()
}
/// Sorts two entries according to the BLS sort system.
/// Reference: https://uapi-group.org/specifications/specs/boot_loader_specification/#sorting
fn sort_entries(a: &(BlsEntry, BootableEntry), b: &(BlsEntry, BootableEntry)) -> Ordering {
// Grab the components of both entries.
let (a_bls, a_boot) = a;
let (b_bls, b_boot) = b;
// Grab the sort keys from both entries.
let a_sort_key = a_bls.sort_key();
let b_sort_key = b_bls.sort_key();
// Compare the sort keys of both entries.
match a_sort_key.cmp(&b_sort_key) {
// If A and B sort keys are equal, sort by machine-id.
Ordering::Equal => {
// Grab the machine-id from both entries.
let a_machine_id = a_bls.machine_id();
let b_machine_id = b_bls.machine_id();
// Compare the machine-id of both entries.
match a_machine_id.cmp(&b_machine_id) {
// If both machine-id values are equal, sort by version.
Ordering::Equal => {
// Grab the version from both entries.
let a_version = a_bls.version();
let b_version = b_bls.version();
// Compare the version of both entries, sorting newer versions first.
match vercmp::compare_versions_optional(
a_version.as_deref(),
b_version.as_deref(),
)
.reverse()
{
// If both versions are equal, sort by file name in reverse order.
Ordering::Equal => {
// Grab the file name from both entries.
let a_name = a_boot.name();
let b_name = b_boot.name();
// Compare the file names of both entries, sorting newer entries first.
vercmp::compare_versions(a_name, b_name).reverse()
}
other => other,
}
}
other => other,
}
}
other => other,
}
}
/// Generates entries from the BLS entries directory using the specified `bls` configuration and
/// `context`. The BLS conversion is best-effort and will ignore any unsupported entries.
pub fn generate(context: Rc<SproutContext>, bls: &BlsConfiguration) -> Result<Vec<BootableEntry>> {
@@ -49,7 +105,7 @@ pub fn generate(context: Rc<SproutContext>, bls: &BlsConfiguration) -> Result<Ve
let path = context.stamp(&bls.path);
// Resolve the path to the BLS directory.
let bls_resolved = utils::resolve_path(context.root().loaded_image_path()?, &path)
let bls_resolved = utils::resolve_path(Some(context.root().loaded_image_path()?), &path)
.context("unable to resolve bls path")?;
// Construct a filesystem path to the BLS entries directory.
@@ -83,13 +139,21 @@ pub fn generate(context: Rc<SproutContext>, bls: &BlsConfiguration) -> Result<Ve
}
// Get the file name of the filesystem item.
let name = entry.file_name().to_string();
let mut name = entry.file_name().to_string();
// Ignore files that are not .conf files.
if !name.to_lowercase().ends_with(".conf") {
continue;
}
// Remove the .conf extension.
name.truncate(name.len() - 5);
// Skip over files that are named just ".conf" as they are not valid entry files.
if name.is_empty() {
continue;
}
// Create a mutable path so we can append the file name to produce the full path.
let mut full_entry_path = entries_path.to_path_buf();
full_entry_path.push(entry.file_name());
@@ -113,26 +177,51 @@ pub fn generate(context: Rc<SproutContext>, bls: &BlsConfiguration) -> Result<Ve
// Produce a new sprout context for the entry with the extracted values.
let mut context = context.fork();
let title = entry.title().unwrap_or_else(|| name.clone());
let title_base = entry.title().unwrap_or_else(|| name.clone());
let chainload = entry.chainload_path().unwrap_or_default();
let options = entry.options().unwrap_or_default();
let version = entry.version().unwrap_or_default();
let machine_id = entry.machine_id().unwrap_or_default();
// Put the initrd through a quirk modifier to support Fedora.
let initrd = quirk_initrd_remove_tuned(entry.initrd_path().unwrap_or_default());
context.set("title", title);
// Combine the title with the version if a version is present, except if it already contains it.
// Sometimes BLS will have a version in the title already, and this makes it unique.
let title_full = if !version.is_empty() && !title_base.contains(&version) {
format!("{} {}", title_base, version)
} else {
title_base.clone()
};
context.set("title-base", title_base);
context.set("title", title_full);
context.set("chainload", chainload);
context.set("options", options);
context.set("initrd", initrd);
context.set("version", version);
context.set("machine-id", machine_id);
// Add the entry to the list with a frozen context.
entries.push(BootableEntry::new(
// Produce a new bootable entry.
let mut boot = BootableEntry::new(
name,
bls.entry.title.clone(),
context.freeze(),
bls.entry.clone(),
));
);
// Pin the entry name to prevent prefixing.
// This is needed as the bootloader interface requires the name to be
// the same as the entry file name, minus the .conf extension.
boot.mark_pin_name();
// Add the BLS entry to the list, along with the bootable entry.
entries.push((entry, boot));
}
Ok(entries)
// Sort all the entries according to the BLS sort system.
entries.sort_by(sort_entries);
// Collect all the bootable entries and return them.
Ok(entries.into_iter().map(|(_, boot)| boot).collect())
}

View File

@@ -15,6 +15,12 @@ pub struct BlsEntry {
pub initrd: Option<String>,
/// The path to an EFI image.
pub efi: Option<String>,
/// The sort key for the entry.
pub sort_key: Option<String>,
/// The version of the entry.
pub version: Option<String>,
/// The machine id of the entry.
pub machine_id: Option<String>,
}
/// Parser for a BLS entry.
@@ -30,6 +36,9 @@ impl FromStr for BlsEntry {
let mut linux: Option<String> = None;
let mut initrd: Option<String> = None;
let mut efi: Option<String> = None;
let mut sort_key: Option<String> = None;
let mut version: Option<String> = None;
let mut machine_id: Option<String> = None;
// Iterate over each line in the input and parse it.
for line in input.lines() {
@@ -41,7 +50,8 @@ impl FromStr for BlsEntry {
continue;
}
// Split the line once by whitespace.
// Split the line once by whitespace. This technically includes newlines but since
// the lines iterator is used, there should never be a newline here.
let Some((key, value)) = line.split_once(char::is_whitespace) else {
continue;
};
@@ -73,6 +83,18 @@ impl FromStr for BlsEntry {
efi = Some(value.trim().to_string());
}
"sort-key" => {
sort_key = Some(value.trim().to_string());
}
"version" => {
version = Some(value.trim().to_string());
}
"machine-id" => {
machine_id = Some(value.trim().to_string());
}
// Ignore any other key.
_ => {
continue;
@@ -87,6 +109,9 @@ impl FromStr for BlsEntry {
linux,
initrd,
efi,
sort_key,
version,
machine_id,
})
}
}
@@ -124,4 +149,19 @@ impl BlsEntry {
pub fn title(&self) -> Option<String> {
self.title.clone()
}
/// Fetches the sort key of the entry, if any.
pub fn sort_key(&self) -> Option<String> {
self.sort_key.clone()
}
/// Fetches the version of the entry, if any.
pub fn version(&self) -> Option<String> {
self.version.clone()
}
/// Fetches the machine id of the entry, if any.
pub fn machine_id(&self) -> Option<String> {
self.machine_id.clone()
}
}

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::entries::{BootableEntry, EntryDeclaration};
use crate::generators::list;
use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
@@ -57,30 +58,12 @@ pub fn generate(
) -> Result<Vec<BootableEntry>> {
// Produce all the combinations of the input values.
let combinations = build_matrix(&matrix.values);
let mut entries = Vec::new();
// For each combination, create a new context and entry.
for (index, combination) in combinations.into_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 = 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(),
// Use the list generator to generate entries for each combination.
list::generate(
context,
entry,
));
}
Ok(entries)
&list::ListConfiguration {
entry: matrix.entry.clone(),
values: combinations,
},
)
}

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

View File

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

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

@@ -0,0 +1,316 @@
use crate::integrations::shim::hook::SecurityHook;
use crate::secure::SecureBoot;
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 std::pin::Pin;
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>, Pin<Box<[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>, Pin<Box<[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,
Box::into_pin(data.to_vec().into_boxed_slice()),
)),
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),
Box::into_pin(data.to_vec().into_boxed_slice()),
))
}
ShimInput::SecurityHookBuffer(_, _) => {
bail!("unable to convert security hook buffer to owned data buffer")
}
ShimInput::ResolvedPath(path) => {
// Read the file path.
let data = path.read_file()?;
Ok(ShimInput::OwnedDataBuffer(
Some(path),
Box::into_pin(data.to_vec().into_boxed_slice()),
))
}
ShimInput::SecurityHookOwnedBuffer(path, data) => {
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(Status),
/// 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.
/// NOTE: On x86_64, this function uses SYSV calling conventions. On aarch64 it uses the
/// efiapi calling convention. This is truly wild, but you can verify it yourself by
/// looking at: https://github.com/rhboot/shim/blob/15.8/shim.h#L207-L212
/// There is no calling convention declared like there should be.
#[cfg(target_arch = "x86_64")]
pub shim_verify: unsafe extern "sysv64" fn(buffer: *const c_void, buffer_size: u32) -> Status,
#[cfg(target_arch = "aarch64")]
pub shim_verify: unsafe extern "efiapi" fn(buffer: *const c_void, buffer_size: u32) -> Status,
/// Unused function that is defined by the shim.
_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 *const c_void, buffer.len() as u32)
};
// If the verification failed, return the verification failure output.
if !status.is_success() {
return Ok(ShimVerificationOutput::VerificationFailed(status));
}
// 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 Secure Boot is enabled.
let secure_boot =
SecureBoot::enabled().context("unable to determine if secure boot is enabled")?;
// Determine whether the shim is loaded.
let shim_loaded = Self::loaded().context("unable to determine if shim is loaded")?;
// Determine whether the shim loader is available.
let shim_loader_available =
Self::loader_available().context("unable to determine if shim loader is available")?;
// Determines whether LoadImage in Boot Services must be patched.
// Version 16 of the shim doesn't require extra effort to load Secure Boot binaries.
// If the image loader is installed, we can skip over the security hook.
let requires_security_hook = secure_boot && shim_loaded && !shim_loader_available;
// If the security hook is required, we will bail for now.
if requires_security_hook {
// Install the security hook, if possible. If it's not, this is necessary to continue,
// so we should bail.
let installed = SecurityHook::install().context("unable to install security hook")?;
if !installed {
bail!("unable to install security hook required for this platform");
}
}
// If the shim is loaded, we will need to retain the shim protocol to allow
// loading multiple images.
if shim_loaded {
// Retain the shim protocol after loading the image.
Self::retain()?;
}
// Converts the shim input to an owned data buffer.
let input = input
.into_owned_data_buffer()
.context("unable to convert input to loaded data buffer")?;
// Constructs a LoadImageSource from the input.
let source = LoadImageSource::FromBuffer {
buffer: input.buffer().context("unable to get buffer from input")?,
file_path: input.file_path(),
};
// Loads the image using Boot Services LoadImage function.
let result = uefi::boot::load_image(current_image, source).context("unable to load image");
// If the security override is required, we will uninstall the security hook.
if requires_security_hook {
let uninstall_result = SecurityHook::uninstall();
// Ensure we don't mask load image errors if uninstalling fails.
if result.is_err()
&& let Err(uninstall_error) = &uninstall_result
{
// Warn on the error since the load image error is more important.
warn!("unable to uninstall security hook: {}", uninstall_error);
} else {
// Otherwise, ensure we handle the original uninstallation result.
uninstall_result?;
}
}
result
}
/// Set the ShimRetainProtocol variable to indicate that shim should retain the protocols
/// for the full lifetime of boot services.
pub fn retain() -> Result<()> {
Self::SHIM_LOCK_VARIABLES
.set_bool(
"ShimRetainProtocol",
true,
VariableClass::BootAndRuntimeTemporary,
)
.context("unable to retain shim protocol")?;
Ok(())
}
}

View File

@@ -0,0 +1,293 @@
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: *const 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: *const FfiDevicePath,
file_buffer: *const 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.
#[must_use]
fn verify(input: ShimInput) -> bool {
// Verify the input and convert the result to a status.
let status = match ShimSupport::verify(input) {
Ok(output) => match output {
// If the verification failed, return the access-denied status.
ShimVerificationOutput::VerificationFailed(status) => status,
// 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
}
};
// If the status is not a success, log the status.
if !status.is_success() {
warn!("shim verification failed: {}", status);
}
// Return whether the status is a success.
// If it's not a success, the original hook should be called.
status.is_success()
}
/// File authentication state verifier for the EFI_SECURITY_ARCH protocol.
/// Takes the `path` and determines the verification.
unsafe extern "efiapi" fn arch_file_authentication_state(
this: *const SecurityArchProtocol,
status: u32,
path: *const 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);
// Convert the input to an owned data buffer.
let input = match input.into_owned_data_buffer() {
Ok(input) => input,
// If an error occurs, log the error and return the not found status.
Err(error) => {
warn!("unable to read data to be authenticated: {}", error);
return Status::NOT_FOUND;
}
};
// Verify the input, if it fails, call the original hook.
if !Self::verify(input) {
// Acquire the global hook state to grab the original hook.
let function = match GLOBAL_HOOK_STATE.lock() {
// We have acquired the lock, so we can find the original hook.
Ok(state) => match state.as_ref() {
// The hook state is available, so we can acquire the original hook.
Some(state) => state.original_hook.file_authentication_state,
// The hook state is not available, so we can't call the original hook.
None => {
warn!("global hook state is not available, unable to call original hook");
return Status::LOAD_ERROR;
}
},
Err(error) => {
warn!(
"unable to acquire global hook state lock to call original hook: {}",
error,
);
return Status::LOAD_ERROR;
}
};
// Call the original hook function to see what it reports.
// SAFETY: This function is safe to call as it is stored by us and is required
// in the UEFI specification.
unsafe { function(this, status, path) }
} else {
Status::SUCCESS
}
}
/// File authentication verifier for the EFI_SECURITY_ARCH2 protocol.
/// Takes the `path` and a file buffer to determine the verification.
unsafe extern "efiapi" fn arch2_file_authentication(
this: *const SecurityArch2Protocol,
path: *const FfiDevicePath,
file_buffer: *const 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, if it fails, call the original hook.
if !Self::verify(input) {
// Acquire the global hook state to grab the original hook.
let function = match GLOBAL_HOOK_STATE.lock() {
// We have acquired the lock, so we can find the original hook.
Ok(state) => match state.as_ref() {
// The hook state is available, so we can acquire the original hook.
Some(state) => state.original_hook2.file_authentication,
// The hook state is not available, so we can't call the original hook.
None => {
warn!("global hook state is not available, unable to call original hook");
return Status::LOAD_ERROR;
}
},
Err(error) => {
warn!(
"unable to acquire global hook state lock to call original hook: {}",
error
);
return Status::LOAD_ERROR;
}
};
// Call the original hook function to see what it reports.
// SAFETY: This function is safe to call as it is stored by us and is required
// in the UEFI specification.
unsafe { function(this, path, file_buffer, file_size, boot_policy) }
} else {
Status::SUCCESS
}
}
/// Install the security hook if needed.
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,19 +1,26 @@
#![doc = include_str!("../README.md")]
#![feature(uefi_std)]
/// The delay to wait for when an error occurs in Sprout.
const DELAY_ON_ERROR: Duration = Duration::from_secs(10);
use crate::config::RootConfiguration;
use crate::context::{RootContext, SproutContext};
use crate::entries::BootableEntry;
use crate::integrations::bootloader_interface::{BootloaderInterface, BootloaderInterfaceTimeout};
use crate::options::SproutOptions;
use crate::options::parser::OptionsRepresentable;
use crate::phases::phase;
use crate::platform::timer::PlatformTimer;
use crate::platform::tpm::PlatformTpm;
use crate::secure::SecureBoot;
use crate::utils::PartitionGuidForm;
use anyhow::{Context, Result, bail};
use log::info;
use log::{error, info, warn};
use std::collections::BTreeMap;
use std::ops::Deref;
use std::time::Duration;
use uefi::proto::device_path::LoadedImageDevicePath;
use uefi::proto::device_path::text::{AllowShortcuts, DisplayOnly};
/// actions: Code that can be configured and executed by Sprout.
pub mod actions;
@@ -39,12 +46,24 @@ pub mod extractors;
/// generators: Runtime code that can generate entries with specific values.
pub mod generators;
/// platform: Integration or support code for specific hardware platforms.
pub mod platform;
/// menu: Display a boot menu to select an entry to boot.
pub mod menu;
/// integrations: Code that interacts with other systems.
pub mod integrations;
/// phases: Hooks into specific parts of the boot process.
pub mod phases;
/// sbat: Secure Boot Attestation section.
pub mod sbat;
/// secure: Secure Boot support.
pub mod secure;
/// setup: Code that initializes the UEFI environment for Sprout.
pub mod setup;
@@ -54,12 +73,34 @@ pub mod options;
/// utils: Utility functions that are used by other parts of Sprout.
pub mod utils;
/// 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, returning an error if one occurs.
fn run() -> Result<()> {
// For safety reasons, we will note that Secure Boot is in beta on Sprout.
if SecureBoot::enabled().context("unable to determine Secure Boot status")? {
warn!("Secure Boot is enabled. Sprout Secure Boot is in beta.");
}
// Start the platform timer.
let timer = PlatformTimer::start();
// Mark the initialization of Sprout in the bootloader interface.
BootloaderInterface::mark_init(&timer)
.context("unable to mark initialization in bootloader interface")?;
// Tell the bootloader interface what firmware we are running on.
BootloaderInterface::set_firmware_info()
.context("unable to set firmware info in bootloader interface")?;
// Tell the bootloader interface what loader is being used.
BootloaderInterface::set_loader_info()
.context("unable to set loader info in bootloader interface")?;
// Acquire the number of active PCR banks on the TPM.
// If no TPM is available, this will return zero.
let active_pcr_banks = PlatformTpm::active_pcr_banks()?;
// Tell the bootloader interface what the number of active PCR banks is.
BootloaderInterface::set_tpm2_active_pcr_banks(active_pcr_banks)
.context("unable to set tpm2 active PCR banks in bootloader interface")?;
// Parse the options to the sprout executable.
let options = SproutOptions::parse().context("unable to parse options")?;
@@ -75,21 +116,35 @@ fn main() -> Result<()> {
config::loader::load(&options)?
};
// Load the root context.
// Grab the sprout.efi loaded image path.
// This is done in a block to ensure the release of the LoadedImageDevicePath protocol.
let mut root = {
let loaded_image_path = {
let current_image_device_path_protocol = uefi::boot::open_protocol_exclusive::<
LoadedImageDevicePath,
>(uefi::boot::image_handle())
.context("unable to get loaded image device path")?;
let loaded_image_path = current_image_device_path_protocol.deref().to_boxed();
info!(
"loaded image path: {}",
loaded_image_path.to_string(DisplayOnly(false), AllowShortcuts(false))?
);
RootContext::new(loaded_image_path, options)
current_image_device_path_protocol.deref().to_boxed()
};
// Grab the partition GUID of the ESP that sprout was loaded from.
let loaded_image_partition_guid =
utils::partition_guid(&loaded_image_path, PartitionGuidForm::Partition)
.context("unable to retrieve loaded image partition guid")?;
// Set the partition GUID of the ESP that sprout was loaded from in the bootloader interface.
if let Some(loaded_image_partition_guid) = loaded_image_partition_guid {
// Tell the system about the partition GUID.
BootloaderInterface::set_partition_guid(&loaded_image_partition_guid)
.context("unable to set partition guid in bootloader interface")?;
}
// Tell the bootloader interface what the loaded image path is.
BootloaderInterface::set_loader_path(&loaded_image_path)
.context("unable to set loader path in bootloader interface")?;
// Create the root context.
let mut root = RootContext::new(loaded_image_path, timer, options);
// Insert the configuration actions into the root context.
root.actions_mut().extend(config.actions.clone());
@@ -110,7 +165,7 @@ fn main() -> Result<()> {
// If --autoconfigure is specified or the loaded configuration has autoconfigure enabled,
// trigger the autoconfiguration mechanism.
if context.root().options().autoconfigure || config.defaults.autoconfigure {
if context.root().options().autoconfigure || config.options.autoconfigure {
autoconfigure::autoconfigure(&mut config).context("unable to autoconfigure")?;
}
@@ -128,6 +183,9 @@ fn main() -> Result<()> {
// 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.
@@ -166,13 +224,17 @@ fn main() -> Result<()> {
for (name, generator) in config.generators {
let context = context.fork().freeze();
// We will prefix all entries with [name]-.
// We will prefix all entries with [name]-, provided the name is not pinned.
let prefix = format!("{}-", name);
// Add all the entries generated by the generator to the entry list.
// The generator specifies the context associated with the entry.
for mut entry in generators::generate(context.clone(), &generator)? {
// If the entry name is not pinned, prepend the name prefix.
if !entry.is_pin_name() {
entry.prepend_name_prefix(&prefix);
}
entries.push(entry);
}
}
@@ -192,7 +254,7 @@ fn main() -> Result<()> {
entry.restamp_title();
// 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 entry.is_match(default_entry) {
entry.mark_default();
@@ -200,6 +262,80 @@ fn main() -> Result<()> {
}
}
// Tell the bootloader interface what entries are available.
BootloaderInterface::set_entries(entries.iter().map(|entry| entry.name()))
.context("unable to set entries in bootloader interface")?;
// Execute the late phase.
phase(context.clone(), &config.phases.late).context("unable to execute late phase")?;
// Acquire the timeout setting from the bootloader interface.
let bootloader_interface_timeout =
BootloaderInterface::get_timeout().context("unable to get bootloader interface timeout")?;
// Acquire the default entry from the bootloader interface.
let bootloader_interface_default_entry = BootloaderInterface::get_default_entry()
.context("unable to get bootloader interface default entry")?;
// Acquire the oneshot entry from the bootloader interface.
let bootloader_interface_oneshot_entry = BootloaderInterface::get_oneshot_entry()
.context("unable to get bootloader interface oneshot entry")?;
// If --boot is specified, boot that entry immediately.
let mut force_boot_entry = context.root().options().boot.clone();
// If --force-menu is specified, show the boot menu regardless of the value of --boot.
let mut force_boot_menu = context.root().options().force_menu;
// Determine the menu timeout in seconds based on the options or configuration.
// We prefer the options over the configuration to allow for overriding.
let mut menu_timeout = context
.root()
.options()
.menu_timeout
.unwrap_or(config.options.menu_timeout);
// Apply bootloader interface timeout settings.
match bootloader_interface_timeout {
BootloaderInterfaceTimeout::MenuForce => {
// Force the boot menu.
force_boot_menu = true;
}
BootloaderInterfaceTimeout::MenuHidden | BootloaderInterfaceTimeout::MenuDisabled => {
// Hide the boot menu by setting the timeout to zero.
menu_timeout = 0;
}
BootloaderInterfaceTimeout::Timeout(timeout) => {
// Configure the timeout to the specified value.
menu_timeout = timeout;
}
BootloaderInterfaceTimeout::Unspecified => {
// Do nothing.
}
}
// Apply bootloader interface default entry settings.
if let Some(ref bootloader_interface_default_entry) = bootloader_interface_default_entry {
// Iterate over all the entries and mark the default entry as the one specified.
for entry in &mut entries {
// Mark the entry as the default entry if it matches the specified entry.
// If the entry does not match the specified entry, unmark it as the default entry.
if entry.is_match(bootloader_interface_default_entry) {
entry.mark_default();
} else {
entry.unmark_default();
}
}
}
// Apply bootloader interface oneshot entry settings.
// If set, we will force booting the oneshot entry.
if let Some(ref bootloader_interface_oneshot_entry) = bootloader_interface_oneshot_entry {
force_boot_entry = Some(bootloader_interface_oneshot_entry.clone());
}
// If no entries were the default, pick the first entry as the default entry.
if entries.iter().all(|entry| !entry.is_default())
&& let Some(entry) = entries.first_mut()
@@ -207,21 +343,7 @@ fn main() -> Result<()> {
entry.mark_default();
}
// Execute the late phase.
phase(context.clone(), &config.phases.late).context("unable to execute late phase")?;
// If --boot is specified, boot that entry immediately.
let force_boot_entry = context.root().options().boot.as_ref();
// If --force-menu is specified, show the boot menu regardless of the value of --boot.
let force_boot_menu = context.root().options().force_menu;
// Determine the menu timeout in seconds based on the options or configuration.
// We prefer the options over the configuration to allow for overriding.
let menu_timeout = context
.root()
.options()
.menu_timeout
.unwrap_or(config.defaults.menu_timeout);
// Convert the menu timeout to a duration.
let menu_timeout = Duration::from_secs(menu_timeout);
// Use the forced boot entry if possible, otherwise pick the first entry using a boot menu.
@@ -230,9 +352,14 @@ fn main() -> Result<()> {
.context(format!("unable to find entry: {force_boot_entry}"))?
} else {
// Delegate to the menu to select an entry to boot.
menu::select(menu_timeout, &entries).context("unable to select entry via boot menu")?
menu::select(&timer, menu_timeout, &entries)
.context("unable to select entry via boot menu")?
};
// Tell the bootloader interface what the selected entry is.
BootloaderInterface::set_selected_entry(entry.name().to_string())
.context("unable to set selected entry in bootloader interface")?;
// Execute all the actions for the selected entry.
for action in &entry.declaration().actions {
let action = entry.context().stamp(action);
@@ -240,6 +367,28 @@ fn main() -> Result<()> {
.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.
// If we reach here, we will exit back to whoever called us.
Ok(())

View File

@@ -1,6 +1,8 @@
use crate::entries::BootableEntry;
use crate::integrations::bootloader_interface::BootloaderInterface;
use crate::platform::timer::PlatformTimer;
use anyhow::{Context, Result, bail};
use log::info;
use log::{info, warn};
use std::time::Duration;
use uefi::ResultExt;
use uefi::boot::TimerTrigger;
@@ -40,14 +42,48 @@ fn read(input: &mut Input, timeout: &Duration) -> Result<MenuOperation> {
uefi::boot::create_event_ex(EventType::TIMER, Tpl::CALLBACK, None, None, None)
.context("unable to create timer event")?
};
// The timeout is in increments of 100 nanoseconds.
let trigger = TimerTrigger::Relative(timeout.as_nanos() as u64 / 100);
let timeout_hundred_nanos = timeout.as_nanos() / 100;
// Check if the timeout is too large to fit into an u64.
if timeout_hundred_nanos > u64::MAX as u128 {
bail!("timeout duration overflow");
}
// Set a timer to trigger after the specified duration.
let trigger = TimerTrigger::Relative(timeout_hundred_nanos as u64);
uefi::boot::set_timer(&timer_event, trigger).context("unable to set timeout timer")?;
let mut events = [timer_event, key_event];
let event = uefi::boot::wait_for_event(&mut events)
let mut events = vec![timer_event, key_event];
// Wait for either the timer event or the key event to trigger.
// Store the result so that we can free the timer event.
let event_result = uefi::boot::wait_for_event(&mut events)
.discard_errdata()
.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() {
// Store the result of the close event so we can determine if we can safely assert it.
let close_event_result =
uefi::boot::close_event(timer_event).context("unable to close timer event");
if event_result.is_err()
&& let Err(ref close_event_error) = close_event_result
{
// Log a warning if we failed to close the timer event.
// This is done to ensure we don't mask the wait_for_event error.
warn!("unable to close timer event: {}", close_event_error);
} else {
// If we reach here, we can safely assert that the close event succeeded without
// masking the wait_for_event error.
close_event_result?;
}
}
// Acquire the event that triggered.
let event = event_result?;
// The first event is the timer event.
// If it has triggered, the user did not select a numbered entry.
@@ -97,7 +133,7 @@ fn select_with_input<'a>(
info!("Boot Menu:");
for (index, entry) in entries.iter().enumerate() {
let title = entry.context().stamp(&entry.declaration().title);
info!(" [{}] {} ({})", index, title, entry.name());
info!(" [{}] {}", index, title);
}
}
@@ -121,7 +157,7 @@ fn select_with_input<'a>(
// Entry was selected by number. If the number is invalid, we continue.
MenuOperation::Number(index) => {
let Some(entry) = entries.get(index) else {
println!("invalid entry number");
info!("invalid entry number");
continue;
};
return Ok(entry);
@@ -147,7 +183,15 @@ fn select_with_input<'a>(
/// Shows a boot menu to select a bootable entry to boot.
/// The actual work is done internally in [select_with_input] which is called
/// within the context of the standard input device.
pub fn select(timeout: Duration, entries: &[BootableEntry]) -> Result<&BootableEntry> {
pub fn select<'live>(
timer: &'live PlatformTimer,
timeout: Duration,
entries: &'live [BootableEntry],
) -> Result<&'live BootableEntry> {
// Notify the bootloader interface that we are about to display the menu.
BootloaderInterface::mark_menu(timer)
.context("unable to mark menu display in bootloader interface")?;
// Acquire the standard input device and run the boot menu.
uefi::system::with_stdin(move |input| select_with_input(input, timeout, entries))
}

View File

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

View File

@@ -1,4 +1,5 @@
use anyhow::{Context, Result, bail};
use log::info;
use std::collections::BTreeMap;
/// The type of option. This disambiguates different behavior
@@ -45,8 +46,24 @@ pub trait OptionsRepresentable {
let configured: BTreeMap<_, _> = BTreeMap::from_iter(Self::options().to_vec());
// Collect all the arguments to Sprout.
// Skip the first argument which is the path to our executable.
let args = std::env::args().skip(1).collect::<Vec<_>>();
// Skip the first argument, which is the path to our executable.
let mut args = std::env::args().skip(1).collect::<Vec<_>>();
// Correct firmware that may add invalid arguments at the start.
// Witnessed this on a Dell Precision 5690 when direct booting.
loop {
// Grab the first argument or break.
let Some(arg) = args.first() else {
break;
};
// If the argument starts with a tilde, remove it.
if arg.starts_with("`") {
args.remove(0);
continue;
}
break;
}
// Represent options as key-value pairs.
let mut options = BTreeMap::new();
@@ -95,7 +112,7 @@ pub trait OptionsRepresentable {
let maybe_next = iterator.peek();
// If the next value isn't another option, set the value to the next value.
// Otherwise, it is an empty string.
// Otherwise, it is None.
value = if let Some(next) = maybe_next
&& !next.starts_with("--")
{
@@ -113,9 +130,9 @@ pub trait OptionsRepresentable {
// Handle the --help flag case.
if description.form == OptionForm::Help {
// Generic configured options output.
println!("Configured Options:");
info!("Configured Options:");
for (name, description) in &configured {
println!(
info!(
" --{}{}: {}",
name,
if description.form == OptionForm::Value {

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;

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

@@ -0,0 +1,94 @@
// 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")]
let frequency = aarch64::frequency();
#[cfg(target_arch = "x86_64")]
let frequency = x86_64::frequency();
// If the frequency is 0, then something went very wrong and we should panic.
if frequency.ticks() == 0 {
panic!("timer frequency is zero");
}
frequency
}
/// Platform timer that allows measurement of the elapsed time.
#[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() -> u64 {
let start = start();
uefi::boot::stall(MEASURE_FREQUENCY_DURATION);
let stop = stop();
let elapsed = stop.wrapping_sub(start) as f64;
(elapsed / MEASURE_FREQUENCY_DURATION.as_secs_f64()) as u64
}
/// Acquire the platform timer frequency.
/// On x86_64, this is slightly expensive, so it should be done once.
pub fn frequency() -> TickFrequency {
let frequency = measure_frequency();
TickFrequency::Measured(frequency, MEASURE_FREQUENCY_DURATION)
}

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, bail};
use std::ops::Deref;
use uefi::boot::SearchType;
use uefi::fs::{FileSystem, Path};
use uefi::proto::device_path::text::{AllowShortcuts, DevicePathFromText, DisplayOnly};
use uefi::proto::device_path::{DevicePath, PoolDevicePath};
use uefi::proto::media::fs::SimpleFileSystem;
use uefi::{CString16, Handle};
use uefi::proto::media::partition::PartitionInfo;
use uefi::{CString16, Guid, Handle};
use uefi_raw::Status;
/// Support code for the EFI framebuffer.
pub mod framebuffer;
@@ -12,6 +15,12 @@ pub mod framebuffer;
/// Support code for the media loader protocol.
pub mod media_loader;
/// Support code for EFI variables.
pub mod variables;
/// Implements a version comparison algorithm according to the BLS specification.
pub mod vercmp;
/// Parses the input `path` as a [DevicePath].
/// Uses the [DevicePathFromText] protocol exclusively, and will fail if it cannot acquire the protocol.
pub fn text_to_device_path(path: &str) -> Result<PoolDevicePath> {
@@ -98,10 +107,24 @@ pub struct ResolvedPath {
pub filesystem_handle: Handle,
}
impl ResolvedPath {
/// Read the file specified by this path into a buffer and return it.
pub fn read_file(&self) -> Result<Vec<u8>> {
let fs = uefi::boot::open_protocol_exclusive::<SimpleFileSystem>(self.filesystem_handle)
.context("unable to open filesystem protocol")?;
let mut fs = FileSystem::new(fs);
let path = self
.sub_path
.to_string(DisplayOnly(false), AllowShortcuts(false))?;
let content = fs.read(Path::new(&path));
content.context("unable to read file contents")
}
}
/// Resolve a path specified by `input` to its various components.
/// Uses `default_root_path` as the base root if one is not specified in the path.
/// Returns [ResolvedPath] which contains the resolved components.
pub fn resolve_path(default_root_path: &DevicePath, input: &str) -> Result<ResolvedPath> {
pub fn resolve_path(default_root_path: Option<&DevicePath>, input: &str) -> Result<ResolvedPath> {
let mut path = text_to_device_path(input).context("unable to convert text to path")?;
let path_has_device = path
.node_iter()
@@ -117,6 +140,9 @@ pub fn resolve_path(default_root_path: &DevicePath, input: &str) -> Result<Resol
if !input.starts_with('\\') {
input.insert(0, '\\');
}
let default_root_path = default_root_path.context("unable to get default root path")?;
input.insert_str(
0,
device_path_root(default_root_path)
@@ -131,8 +157,11 @@ pub fn resolve_path(default_root_path: &DevicePath, input: &str) -> Result<Resol
let root_path = text_to_device_path(root.as_str())
.context("unable to convert root to path")?
.to_boxed();
let mut root_path = root_path.as_ref();
let handle = uefi::boot::locate_device_path::<SimpleFileSystem>(&mut root_path)
let root_path = root_path.as_ref();
// locate_device_path modifies the path, so we need to clone it.
let root_path_modifiable = root_path.to_owned();
let handle = uefi::boot::locate_device_path::<SimpleFileSystem>(&mut &*root_path_modifiable)
.context("unable to locate filesystem device path")?;
let subpath = device_path_subpath(path.deref()).context("unable to get device subpath")?;
Ok(ResolvedPath {
@@ -150,14 +179,118 @@ pub fn resolve_path(default_root_path: &DevicePath, input: &str) -> Result<Resol
/// This acquires exclusive protocol access to the [SimpleFileSystem] protocol of the resolved
/// filesystem handle, so care must be taken to call this function outside a scope with
/// the filesystem handle protocol acquired.
pub fn read_file_contents(default_root_path: &DevicePath, input: &str) -> Result<Vec<u8>> {
pub fn read_file_contents(default_root_path: Option<&DevicePath>, input: &str) -> Result<Vec<u8>> {
let resolved = resolve_path(default_root_path, input)?;
let fs = uefi::boot::open_protocol_exclusive::<SimpleFileSystem>(resolved.filesystem_handle)
.context("unable to open filesystem protocol")?;
let mut fs = FileSystem::new(fs);
let path = resolved
.sub_path
.to_string(DisplayOnly(false), AllowShortcuts(false))?;
let content = fs.read(Path::new(&path));
content.context("unable to read file contents")
resolved.read_file()
}
/// Filter a string-like Option `input` such that an empty string is [None].
pub fn empty_is_none<T: AsRef<str>>(input: Option<T>) -> Option<T> {
input.filter(|input| !input.as_ref().is_empty())
}
/// Combine a sequence of strings into a single string, separated by spaces, ignoring empty strings.
pub fn combine_options<T: AsRef<str>>(options: impl Iterator<Item = T>) -> String {
options
.flat_map(|item| empty_is_none(Some(item)))
.map(|item| item.as_ref().to_string())
.collect::<Vec<_>>()
.join(" ")
}
/// Produce a unique hash for the input.
/// This uses SHA-256, which is unique enough but relatively short.
pub fn unique_hash(input: &str) -> String {
sha256::digest(input.as_bytes())
}
/// Represents the type of partition GUID that can be retrieved.
#[derive(PartialEq, Eq)]
pub enum PartitionGuidForm {
/// The partition GUID is the unique partition GUID.
Partition,
/// The partition GUID is the partition type GUID.
PartitionType,
}
/// Retrieve the partition / partition type GUID of the device root `path`.
/// This only works on GPT partitions. If the root is not a GPT partition, None is returned.
/// If the GUID is all zeros, this will return None.
pub fn partition_guid(path: &DevicePath, form: PartitionGuidForm) -> Result<Option<Guid>> {
// Clone the path so we can pass it to the UEFI stack.
let path = path.to_boxed();
let result = uefi::boot::locate_device_path::<PartitionInfo>(&mut &*path);
let handle = match result {
Ok(handle) => Ok(Some(handle)),
Err(error) => {
// If the error is NOT_FOUND or UNSUPPORTED, we can return None.
// These are non-fatal errors.
if error.status() == Status::NOT_FOUND || error.status() == Status::UNSUPPORTED {
Ok(None)
} else {
Err(error)
}
}
}
.context("unable to locate device path")?;
// If we have the handle, we can try to open the partition info protocol.
if let Some(handle) = handle {
// Open the partition info protocol.
let partition_info = uefi::boot::open_protocol_exclusive::<PartitionInfo>(handle)
.context("unable to open partition info protocol")?;
// Find the unique partition GUID.
// If this is not a GPT partition, this will produce None.
Ok(partition_info
.gpt_partition_entry()
.map(|entry| match form {
// Match the form of the partition GUID.
PartitionGuidForm::Partition => entry.unique_partition_guid,
PartitionGuidForm::PartitionType => entry.partition_type_guid.0,
})
.filter(|guid| !guid.is_zero()))
} else {
Ok(None)
}
}
/// Find a handle that provides the specified `protocol`.
pub fn find_handle(protocol: &Guid) -> Result<Option<Handle>> {
// Locate the requested protocol handle.
match uefi::boot::locate_handle_buffer(SearchType::ByProtocol(protocol)) {
// If a handle is found, the protocol is available.
Ok(handles) => Ok(if handles.is_empty() {
None
} else {
Some(handles[0])
}),
// If an error occurs, check if it is because the protocol is not available.
// If so, return false. Otherwise, return the error.
Err(error) => {
if error.status() == Status::NOT_FOUND {
Ok(None)
} else {
Err(error).context("unable to determine if the protocol is available")
}
}
}
}
/// Convert a byte slice into a CString16.
pub fn utf16_bytes_to_cstring16(bytes: &[u8]) -> Result<CString16> {
// Validate the input bytes are the right length.
if !bytes.len().is_multiple_of(2) {
bail!("utf16 bytes must be a multiple of 2");
}
// Convert the bytes to UTF-16 data.
let data = bytes
// Chunk everything into two bytes.
.chunks_exact(2)
// Reinterpret the bytes as u16 little-endian.
.map(|chunk| u16::from_le_bytes([chunk[0], chunk[1]]))
// Collect the result into a vector.
.collect::<Vec<_>>();
CString16::try_from(data).context("unable to convert utf16 bytes to CString16")
}

View File

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

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

@@ -0,0 +1,152 @@
use crate::utils;
use anyhow::{Context, Result};
use log::warn;
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 the cstr16 value specified by the `key`.
/// Returns None if the value isn't set.
/// If the value is not decodable, we will return None and log a warning.
pub fn get_cstr16(&self, key: &str) -> Result<Option<String>> {
let name = Self::name(key)?;
// Retrieve the variable data, handling variable not existing as None.
match uefi::runtime::get_variable_boxed(&name, &self.vendor) {
Ok((data, _)) => {
// Try to decode UTF-16 bytes to a CString16.
match utils::utf16_bytes_to_cstring16(&data) {
Ok(value) => {
// We have a value, so return the UTF-8 value.
Ok(Some(value.to_string()))
}
Err(error) => {
// We encountered an error, so warn and return None.
warn!("efi variable '{}' is not valid UTF-16: {}", key, error);
Ok(None)
}
}
}
Err(error) => {
// If the variable does not exist, we will return None.
if error.status() == Status::NOT_FOUND {
Ok(None)
} else {
Err(error).with_context(|| format!("unable to get efi variable {}", key))
}
}
}
}
/// Retrieve a boolean value specified by the `key`.
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)
}
/// Set the u64 little-endian variable specified by `key` to `value`.
/// The variable `class` controls the attributes for the variable.
pub fn set_u64le(&self, key: &str, value: u64, class: VariableClass) -> Result<()> {
self.set(key, &value.to_le_bytes(), class)
}
pub fn remove(&self, key: &str) -> Result<()> {
let name = Self::name(key)?;
// Delete the variable from UEFI.
uefi::runtime::delete_variable(&name, &self.vendor)
.with_context(|| format!("unable to remove efi variable {}", key))
}
}

184
src/utils/vercmp.rs Normal file
View File

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