51 Commits

Author SHA1 Message Date
dependabot[bot]
bf28558a83 chore(deps): bump actions/checkout in the actions-updates group (#49)
Bumps the actions-updates group with 1 update: [actions/checkout](https://github.com/actions/checkout).


Updates `actions/checkout` from 5.0.0 to 6.0.1
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](08c6903cd8...8e8c483db8)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 6.0.1
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-18 11:39:56 -08:00
Ariadne Conill
0b75e547f7 Merge pull request #43 from edera-dev/chore/dev-alpine-clean
chore(dev): make alpine boot fully clean with kernel module infra
2025-11-27 20:33:10 -08:00
5605056c82 chore(dev): make alpine boot fully clean with kernel module infra 2025-11-26 23:05:01 -08:00
d4fcba18c0 Merge pull request #39 from edera-dev/dependabot/github_actions/actions-updates-c5043b94ad
chore(deps): bump step-security/harden-runner from 2.13.1 to 2.13.2 in the actions-updates group
2025-11-25 21:35:22 -08:00
a dinosaur
5dcd763db9 chore(options):use jaarg alloc-less api, removing map middleman (#41) 2025-11-25 21:34:18 -08:00
4f8abadb3a Merge pull request #42 from edera-dev/fix/pr-workflow-attest
fix(workflows): disable artifact publish attestation for pull requests
2025-11-25 20:43:24 -08:00
57e90a4623 fix(workflows): disable artifact publish attestation for pull requests 2025-11-25 20:38:49 -08:00
dependabot[bot]
136b899844 chore(deps): bump step-security/harden-runner
Bumps the actions-updates group with 1 update: [step-security/harden-runner](https://github.com/step-security/harden-runner).


Updates `step-security/harden-runner` from 2.13.1 to 2.13.2
- [Release notes](https://github.com/step-security/harden-runner/releases)
- [Commits](f4a75cfd61...95d9a5deda)

---
updated-dependencies:
- dependency-name: step-security/harden-runner
  dependency-version: 2.13.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-24 11:01:47 +00:00
0dbd011648 Merge pull request #40 from edera-dev/chore/release-v0.0.26
chore(release): sprout: version 0.0.26
2025-11-17 10:18:11 -08:00
65c392944b chore(release): sprout: version 0.0.26 2025-11-17 10:14:58 -08:00
Ariadne Conill
2683229bd3 Merge pull request #35 from edera-dev/chore/dev-improve-alpine
chore(dev): autologin to alpine and writable rootfs
2025-11-17 10:01:17 -08:00
a7a9554875 Merge pull request #38 from edera-dev/feat/partial-match-default
feat(entries): support '*' suffix as a partial match to an entry
2025-11-15 21:25:59 -08:00
5ad617c54f feat(entries): support '*' suffix as a partial match to an entry 2025-11-15 20:36:31 -08:00
0aa7a46808 Merge pull request #27 from edera-dev/feat/jaarg
feat(options): replace options parser with jaarg
2025-11-15 18:02:46 -08:00
8711c54074 feat(boot): utilize jaarg for options parsing 2025-11-15 17:31:46 -08:00
Ariadne Conill
c21c140039 Merge pull request #37 from edera-dev/fix/linux-auto-detect-aarch64-images
fix(autoconfigure/linux): detect Image as a kernel, which openSUSE uses
2025-11-15 14:50:18 -08:00
c053f62b88 fix(autoconfigure/linux): detect Image as a kernel, which openSUSE uses 2025-11-15 13:41:07 -08:00
e0bd703511 chore(dev): autologin to alpine and writable rootfs 2025-11-15 13:37:50 -08:00
2a9c9f6907 Merge pull request #34 from edera-dev/chore/release-0.0.25
chore(release): sprout: version 0.0.25
2025-11-14 10:49:09 -05:00
16755acdfe chore(release): sprout: version 0.0.25 2025-11-14 10:45:16 -05:00
ebb1f0bb44 Merge pull request #33 from edera-dev/chore/upgrade-deps
chore(deps): upgrade rust to 1.91.1 and bump cargo and docker deps
2025-11-14 00:30:48 -05:00
ecba8a5e02 chore(deps): upgrade rust to 1.91.1 and bump cargo and docker deps 2025-11-14 00:24:09 -05:00
0028e3eefc Merge pull request #32 from rkratky/typo 2025-11-12 10:15:49 -05:00
Robert Krátký
8603794c44 Fix a typo: grubaa64.efi -> grubx64.efi 2025-11-12 16:11:13 +01:00
4fae4080a2 Merge pull request #29 from edera-dev/dependabot/github_actions/actions-updates-1be5372544
chore(deps): bump the actions-updates group with 2 updates
2025-11-10 10:09:58 -05:00
dependabot[bot]
da5e0daa51 chore(deps): bump the actions-updates group with 2 updates
Bumps the actions-updates group with 2 updates: [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv) and [github/codeql-action](https://github.com/github/codeql-action).


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

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

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

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

View File

@@ -1,10 +1,12 @@
name: zizmor name: zizmor
on: on:
push:
branches: ["main"]
pull_request: pull_request:
branches: ["**"] branches:
- main
push:
branches:
- main
permissions: permissions:
contents: read # Needed to checkout the repository. contents: read # Needed to checkout the repository.
@@ -23,17 +25,17 @@ jobs:
actions: read # Needed to analyze action metadata. actions: read # Needed to analyze action metadata.
steps: steps:
- name: harden runner - name: harden runner
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2
with: with:
egress-policy: audit egress-policy: audit
- name: checkout - name: checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with: with:
persist-credentials: false persist-credentials: false
- name: setup uv - name: setup uv
uses: astral-sh/setup-uv@2ddd2b9cb38ad8efd50337e8ab201519a34c9f24 # v7.1.1 uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2
- name: zizmor - name: zizmor
run: uvx zizmor --pedantic --format sarif . > results.sarif run: uvx zizmor --pedantic --format sarif . > results.sarif
@@ -41,7 +43,7 @@ jobs:
GH_TOKEN: "${{ secrets.GITHUB_TOKEN }}" GH_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
- name: upload - name: upload
uses: github/codeql-action/upload-sarif@4e94bd11f71e507f7f87df81788dff88d1dacbfb # v4.31.0 uses: github/codeql-action/upload-sarif@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2
with: with:
sarif_file: results.sarif sarif_file: results.sarif
category: zizmor category: zizmor

View File

@@ -21,12 +21,12 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: harden runner - name: harden runner
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2
with: with:
egress-policy: audit egress-policy: audit
- name: checkout - name: checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with: with:
persist-credentials: false persist-credentials: false
@@ -51,12 +51,12 @@ jobs:
name: 'build ${{ matrix.arch }}' name: 'build ${{ matrix.arch }}'
steps: steps:
- name: harden runner - name: harden runner
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2
with: with:
egress-policy: audit egress-policy: audit
- name: checkout - name: checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with: with:
persist-credentials: false persist-credentials: false
@@ -80,12 +80,12 @@ jobs:
name: 'clippy ${{ matrix.arch }}' name: 'clippy ${{ matrix.arch }}'
steps: steps:
- name: harden runner - name: harden runner
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2
with: with:
egress-policy: audit egress-policy: audit
- name: checkout - name: checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with: with:
persist-credentials: false persist-credentials: false

View File

@@ -1,10 +1,12 @@
name: codeql name: codeql
on: on:
push:
branches: [ "main" ]
pull_request: pull_request:
branches: [ "main" ] branches:
- main
push:
branches:
- main
schedule: schedule:
- cron: '33 16 * * 0' - cron: '33 16 * * 0'
@@ -35,23 +37,23 @@ jobs:
build-mode: none build-mode: none
steps: steps:
- name: harden runner - name: harden runner
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2
with: with:
egress-policy: audit egress-policy: audit
- name: checkout - name: checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with: with:
persist-credentials: false persist-credentials: false
- name: initialize codeql - name: initialize codeql
uses: github/codeql-action/init@4e94bd11f71e507f7f87df81788dff88d1dacbfb # v4.31.0 uses: github/codeql-action/init@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }} build-mode: ${{ matrix.build-mode }}
config-file: ./.github/codeql/codeql-config.yaml config-file: ./.github/codeql/codeql-config.yaml
- name: perform codeql analysis - name: perform codeql analysis
uses: github/codeql-action/analyze@4e94bd11f71e507f7f87df81788dff88d1dacbfb # v4.31.0 uses: github/codeql-action/analyze@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2
with: with:
category: "/language:${{matrix.language}}" category: "/language:${{matrix.language}}"

View File

@@ -1,19 +1,12 @@
name: publish name: publish
on: on:
push:
branches:
- main
pull_request: pull_request:
branches: branches:
- main - main
paths: push:
- bin/** branches:
- src/** - main
- Cargo.*
- rust-toolchain.toml
- .github/workflows/publish.yaml
permissions: permissions:
contents: read # Needed to checkout the repository. contents: read # Needed to checkout the repository.
@@ -32,12 +25,12 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: harden runner - name: harden runner
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2
with: with:
egress-policy: audit egress-policy: audit
- name: checkout - name: checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with: with:
persist-credentials: false persist-credentials: false
@@ -60,3 +53,4 @@ jobs:
with: with:
subject-name: artifacts.zip subject-name: artifacts.zip
subject-digest: "sha256:${{ steps.upload.outputs.artifact-digest }}" subject-digest: "sha256:${{ steps.upload.outputs.artifact-digest }}"
if: github.event_name != 'pull_request'

View File

@@ -25,12 +25,12 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: harden runner - name: harden runner
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2
with: with:
egress-policy: audit egress-policy: audit
- name: checkout - name: checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with: with:
persist-credentials: false persist-credentials: false

59
Cargo.lock generated
View File

@@ -46,9 +46,9 @@ dependencies = [
[[package]] [[package]]
name = "crypto-common" name = "crypto-common"
version = "0.1.6" version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
dependencies = [ dependencies = [
"generic-array", "generic-array",
"typenum", "typenum",
@@ -65,34 +65,51 @@ dependencies = [
] ]
[[package]] [[package]]
name = "edera-sprout" name = "edera-sprout-boot"
version = "0.0.21" version = "0.0.26"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bitflags", "edera-sprout-build",
"edera-sprout-config", "edera-sprout-config",
"edera-sprout-eficore",
"hex", "hex",
"jaarg",
"log", "log",
"sha2", "sha2",
"shlex",
"spin",
"toml", "toml",
"uefi", "uefi",
"uefi-raw", "uefi-raw",
] ]
[[package]]
name = "edera-sprout-build"
version = "0.0.26"
[[package]] [[package]]
name = "edera-sprout-config" name = "edera-sprout-config"
version = "0.0.21" version = "0.0.26"
dependencies = [ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "edera-sprout-eficore"
version = "0.0.26"
dependencies = [
"anyhow",
"bitflags",
"log",
"shlex",
"spin",
"uefi",
"uefi-raw",
]
[[package]] [[package]]
name = "generic-array" name = "generic-array"
version = "0.14.9" version = "0.14.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
dependencies = [ dependencies = [
"typenum", "typenum",
"version_check", "version_check",
@@ -104,6 +121,12 @@ version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "jaarg"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "534d589df1ef528a238f4bc4b1db081a1280f3aedf2695fd8971e9853a7fa4f6"
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.177" version = "0.2.177"
@@ -156,9 +179,9 @@ dependencies = [
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.41" version = "1.0.42"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
] ]
@@ -236,9 +259,9 @@ dependencies = [
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.108" version = "2.0.110"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da58917d35242480a05c2897064da0a80589a2a0476c9a3f2fdc83b53502e917" checksum = "a99801b5bd34ede4cf3fc688c5919368fea4e4814a4664359503e6015b280aea"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -293,9 +316,9 @@ dependencies = [
[[package]] [[package]]
name = "uefi" name = "uefi"
version = "0.36.0" version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f123e69767fc287c44d70ee19af3b39d1bfb735dbaff5090e95b5b13cd656d16" checksum = "71fe9058b73ee2b6559524af9e33199c13b2485ddbf3ad1181b68051cdc50c17"
dependencies = [ dependencies = [
"bitflags", "bitflags",
"cfg-if", "cfg-if",
@@ -320,9 +343,9 @@ dependencies = [
[[package]] [[package]]
name = "uefi-raw" name = "uefi-raw"
version = "0.12.0" version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8aff2f4f2b556a36a201d335a1e0a57754967a96857b1f47a52d5a23825cac84" checksum = "7f64fe59e11af447d12fd60a403c74106eb104309f34b4c6dbce6e927d97da9d"
dependencies = [ dependencies = [
"bitflags", "bitflags",
"uguid", "uguid",

View File

@@ -1,13 +1,15 @@
[workspace] [workspace]
members = [ members = [
"crates/boot",
"crates/build",
"crates/config", "crates/config",
"crates/sprout", "crates/eficore",
] ]
resolver = "3" resolver = "3"
[workspace.package] [workspace.package]
license = "Apache-2.0" license = "Apache-2.0"
version = "0.0.21" version = "0.0.26"
homepage = "https://sprout.edera.dev" homepage = "https://sprout.edera.dev"
repository = "https://github.com/edera-dev/sprout" repository = "https://github.com/edera-dev/sprout"
edition = "2024" edition = "2024"
@@ -16,8 +18,7 @@ edition = "2024"
bitflags = "2.10.0" bitflags = "2.10.0"
log = "0.4.28" log = "0.4.28"
spin = "0.10.0" spin = "0.10.0"
uefi = "0.36.0" uefi-raw = "0.13.0"
uefi-raw = "0.12.0"
[workspace.dependencies.anyhow] [workspace.dependencies.anyhow]
version = "1.0.100" version = "1.0.100"
@@ -28,6 +29,11 @@ version = "0.4.3"
default-features = false default-features = false
features = ["alloc"] features = ["alloc"]
[workspace.dependencies.jaarg]
version = "0.2.2"
default-features = false
features = ["alloc"]
[workspace.dependencies.serde] [workspace.dependencies.serde]
version = "1.0.228" version = "1.0.228"
default-features = false default-features = false
@@ -46,6 +52,11 @@ version = "0.9.8"
default-features = false default-features = false
features = ["serde", "parse"] features = ["serde", "parse"]
[workspace.dependencies.uefi]
version = "0.36.1"
default-features = false
features = ["alloc", "global_allocator", "panic_handler"]
# Common build profiles # Common build profiles
# NOTE: We have to compile everything for opt-level = 2 due to optimization passes # NOTE: We have to compile everything for opt-level = 2 due to optimization passes
# which don't handle the UEFI target properly. # which don't handle the UEFI target properly.

View File

@@ -11,6 +11,17 @@ We currently only support `x86_64-unknown-uefi` and `aarch64-unknown-uefi` targe
To test your changes in QEMU, please run `./hack/dev/boot.sh`, you can specify `x86_64` or `aarch64` To test your changes in QEMU, please run `./hack/dev/boot.sh`, you can specify `x86_64` or `aarch64`
as an argument to boot.sh to boot the specified architecture. as an argument to boot.sh to boot the specified architecture.
## Crate Structure
Sprout is split into multiple crates:
- `edera-sprout-boot` as `crates/boot`: Bootloader entrypoint for Sprout.
- `edera-sprout-build` at `crates/build`: Build logic for Sprout.
- `edera-sprout-config` at `crates/config`: Serialization structures for the Sprout configuration file.
- `edera-sprout-eficore` at `crates/eficore`: Core library for Sprout EFI code.
It is intended that overtime Sprout will be split into even more crates.
## Hack Scripts ## Hack Scripts
You can use the `./hack` scripts to run common development tasks: You can use the `./hack` scripts to run common development tasks:

View File

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

View File

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

View File

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

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

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

View File

@@ -1,13 +1,14 @@
use crate::context::SproutContext; use crate::context::SproutContext;
use crate::integrations::bootloader_interface::BootloaderInterface;
use crate::integrations::shim::{ShimInput, ShimSupport};
use crate::utils; use crate::utils;
use crate::utils::media_loader::MediaLoaderHandle;
use crate::utils::media_loader::constants::linux::LINUX_EFI_INITRD_MEDIA_GUID;
use alloc::boxed::Box; use alloc::boxed::Box;
use alloc::rc::Rc; use alloc::rc::Rc;
use anyhow::{Context, Result, bail}; use anyhow::{Context, Result, bail};
use edera_sprout_config::actions::chainload::ChainloadConfiguration; use edera_sprout_config::actions::chainload::ChainloadConfiguration;
use eficore::bootloader_interface::BootloaderInterface;
use eficore::loader::source::ImageSource;
use eficore::loader::{ImageLoadRequest, ImageLoader};
use eficore::media_loader::MediaLoaderHandle;
use eficore::media_loader::constants::linux::LINUX_EFI_INITRD_MEDIA_GUID;
use log::error; use log::error;
use uefi::CString16; use uefi::CString16;
use uefi::proto::loaded_image::LoadedImage; use uefi::proto::loaded_image::LoadedImage;
@@ -18,19 +19,23 @@ pub fn chainload(context: Rc<SproutContext>, configuration: &ChainloadConfigurat
let sprout_image = uefi::boot::image_handle(); let sprout_image = uefi::boot::image_handle();
// Resolve the path to the image to chainload. // Resolve the path to the image to chainload.
let resolved = utils::resolve_path( let resolved = eficore::path::resolve_path(
Some(context.root().loaded_image_path()?), Some(context.root().loaded_image_path()?),
&context.stamp(&configuration.path), &context.stamp(&configuration.path),
) )
.context("unable to resolve chainload path")?; .context("unable to resolve chainload path")?;
// Load the image to chainload using the shim support integration. // Create a new image load request with the current image and the resolved path.
let request = ImageLoadRequest::new(sprout_image, ImageSource::ResolvedPath(&resolved));
// Load the image to chainload using the image loader support module.
// It will determine if the image needs to be loaded via the shim or can be loaded directly. // It will determine if the image needs to be loaded via the shim or can be loaded directly.
let image = ShimSupport::load(sprout_image, ShimInput::ResolvedPath(&resolved))?; let image = ImageLoader::load(request)?;
// Open the LoadedImage protocol of the image to chainload. // Open the LoadedImage protocol of the image to chainload.
let mut loaded_image_protocol = uefi::boot::open_protocol_exclusive::<LoadedImage>(image) let mut loaded_image_protocol =
.context("unable to open loaded image protocol")?; uefi::boot::open_protocol_exclusive::<LoadedImage>(*image.handle())
.context("unable to open loaded image protocol")?;
// Stamp and combine the options to pass to the image. // Stamp and combine the options to pass to the image.
let options = let options =
@@ -68,9 +73,11 @@ pub fn chainload(context: Rc<SproutContext>, configuration: &ChainloadConfigurat
// If an initrd is provided, register it with the EFI stack. // If an initrd is provided, register it with the EFI stack.
let mut initrd_handle = None; let mut initrd_handle = None;
if let Some(linux_initrd) = initrd { if let Some(linux_initrd) = initrd {
let content = let content = eficore::path::read_file_contents(
utils::read_file_contents(Some(context.root().loaded_image_path()?), &linux_initrd) Some(context.root().loaded_image_path()?),
.context("unable to read linux initrd")?; &linux_initrd,
)
.context("unable to read linux initrd")?;
let handle = let handle =
MediaLoaderHandle::register(LINUX_EFI_INITRD_MEDIA_GUID, content.into_boxed_slice()) MediaLoaderHandle::register(LINUX_EFI_INITRD_MEDIA_GUID, content.into_boxed_slice())
.context("unable to register linux initrd")?; .context("unable to register linux initrd")?;
@@ -85,7 +92,7 @@ pub fn chainload(context: Rc<SproutContext>, configuration: &ChainloadConfigurat
// This call might return, or it may pass full control to another image that will never return. // This call might return, or it may pass full control to another image that will never return.
// Capture the result to ensure we can return an error if the image fails to start, but only // Capture the result to ensure we can return an error if the image fails to start, but only
// after the optional initrd has been unregistered. // after the optional initrd has been unregistered.
let result = uefi::boot::start_image(image); let result = uefi::boot::start_image(*image.handle());
// Unregister the initrd if it was registered. // Unregister the initrd if it was registered.
if let Some(initrd_handle) = initrd_handle if let Some(initrd_handle) = initrd_handle

View File

@@ -1,15 +1,7 @@
use crate::{ use crate::{
actions, actions,
context::SproutContext, context::SproutContext,
utils::{ utils::{self},
self,
media_loader::{
MediaLoaderHandle,
constants::xen::{
XEN_EFI_CONFIG_MEDIA_GUID, XEN_EFI_KERNEL_MEDIA_GUID, XEN_EFI_RAMDISK_MEDIA_GUID,
},
},
},
}; };
use alloc::rc::Rc; use alloc::rc::Rc;
use alloc::string::{String, ToString}; use alloc::string::{String, ToString};
@@ -17,6 +9,12 @@ use alloc::{format, vec};
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use edera_sprout_config::actions::chainload::ChainloadConfiguration; use edera_sprout_config::actions::chainload::ChainloadConfiguration;
use edera_sprout_config::actions::edera::EderaConfiguration; use edera_sprout_config::actions::edera::EderaConfiguration;
use eficore::media_loader::{
MediaLoaderHandle,
constants::xen::{
XEN_EFI_CONFIG_MEDIA_GUID, XEN_EFI_KERNEL_MEDIA_GUID, XEN_EFI_RAMDISK_MEDIA_GUID,
},
};
use log::error; use log::error;
use uefi::Guid; use uefi::Guid;
@@ -79,8 +77,9 @@ fn register_media_loader_file(
// Stamp the path to the file. // Stamp the path to the file.
let path = context.stamp(path); let path = context.stamp(path);
// Read the file contents. // Read the file contents.
let content = utils::read_file_contents(Some(context.root().loaded_image_path()?), &path) let content =
.context(format!("unable to read {} file", what))?; eficore::path::read_file_contents(Some(context.root().loaded_image_path()?), &path)
.context(format!("unable to read {} file", what))?;
// Register the media loader. // Register the media loader.
let handle = MediaLoaderHandle::register(guid, content.into_boxed_slice()) let handle = MediaLoaderHandle::register(guid, content.into_boxed_slice())
.context(format!("unable to register {} media loader", what))?; .context(format!("unable to register {} media loader", what))?;

View File

@@ -25,7 +25,7 @@ const LINUX_CHAINLOAD_ACTION_PREFIX: &str = "linux-chainload-";
const SCAN_LOCATIONS: &[&str] = &["\\boot", "\\"]; const SCAN_LOCATIONS: &[&str] = &["\\boot", "\\"];
/// Prefixes of kernel files to scan for. /// Prefixes of kernel files to scan for.
const KERNEL_PREFIXES: &[&str] = &["vmlinuz"]; const KERNEL_PREFIXES: &[&str] = &["vmlinuz", "Image"];
/// Prefixes of initramfs files to match to. /// Prefixes of initramfs files to match to.
const INITRAMFS_PREFIXES: &[&str] = &["initramfs", "initrd", "initrd.img"]; const INITRAMFS_PREFIXES: &[&str] = &["initramfs", "initrd", "initrd.img"];

View File

@@ -1,10 +1,9 @@
use crate::options::SproutOptions; use crate::options::SproutOptions;
use crate::platform::tpm::PlatformTpm;
use crate::utils;
use alloc::vec::Vec; use alloc::vec::Vec;
use anyhow::{Context, Result, bail}; use anyhow::{Context, Result, bail};
use core::ops::Deref; use core::ops::Deref;
use edera_sprout_config::{RootConfiguration, latest_version}; use edera_sprout_config::{RootConfiguration, latest_version};
use eficore::platform::tpm::PlatformTpm;
use log::info; use log::info;
use toml::Value; use toml::Value;
use uefi::proto::device_path::LoadedImageDevicePath; use uefi::proto::device_path::LoadedImageDevicePath;
@@ -21,7 +20,7 @@ fn load_raw_config(options: &SproutOptions) -> Result<Vec<u8>> {
info!("configuration file: {}", options.config); info!("configuration file: {}", options.config);
// Read the contents of the sprout config file. // Read the contents of the sprout config file.
let content = utils::read_file_contents(Some(&path), &options.config) let content = eficore::path::read_file_contents(Some(&path), &options.config)
.context("unable to read sprout config file")?; .context("unable to read sprout config file")?;
// Measure the sprout.toml into the TPM, if needed and possible. // Measure the sprout.toml into the TPM, if needed and possible.

View File

@@ -1,5 +1,4 @@
use crate::options::SproutOptions; use crate::options::SproutOptions;
use crate::platform::timer::PlatformTimer;
use alloc::boxed::Box; use alloc::boxed::Box;
use alloc::collections::{BTreeMap, BTreeSet}; use alloc::collections::{BTreeMap, BTreeSet};
use alloc::format; use alloc::format;
@@ -10,6 +9,7 @@ use anyhow::anyhow;
use anyhow::{Result, bail}; use anyhow::{Result, bail};
use core::cmp::Reverse; use core::cmp::Reverse;
use edera_sprout_config::actions::ActionDeclaration; use edera_sprout_config::actions::ActionDeclaration;
use eficore::platform::timer::PlatformTimer;
use uefi::proto::device_path::DevicePath; use uefi::proto::device_path::DevicePath;
/// The maximum number of iterations that can be performed in [SproutContext::finalize]. /// The maximum number of iterations that can be performed in [SproutContext::finalize].

View File

@@ -1,12 +1,12 @@
use crate::context::SproutContext; use crate::context::SproutContext;
use crate::integrations::shim::{ShimInput, ShimSupport};
use crate::utils;
use alloc::collections::BTreeMap; use alloc::collections::BTreeMap;
use alloc::format; use alloc::format;
use alloc::rc::Rc; use alloc::rc::Rc;
use alloc::string::String; use alloc::string::String;
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use edera_sprout_config::drivers::DriverDeclaration; use edera_sprout_config::drivers::DriverDeclaration;
use eficore::loader::source::ImageSource;
use eficore::loader::{ImageLoadRequest, ImageLoader};
use log::info; use log::info;
use uefi::boot::SearchType; use uefi::boot::SearchType;
@@ -16,20 +16,23 @@ fn load_driver(context: Rc<SproutContext>, driver: &DriverDeclaration) -> Result
let sprout_image = uefi::boot::image_handle(); let sprout_image = uefi::boot::image_handle();
// Resolve the path to the driver image. // Resolve the path to the driver image.
let resolved = utils::resolve_path( let resolved = eficore::path::resolve_path(
Some(context.root().loaded_image_path()?), Some(context.root().loaded_image_path()?),
&context.stamp(&driver.path), &context.stamp(&driver.path),
) )
.context("unable to resolve path to driver")?; .context("unable to resolve path to driver")?;
// Load the driver image using the shim support integration. // Create an image load request with the current image and the resolved path.
let request = ImageLoadRequest::new(sprout_image, ImageSource::ResolvedPath(&resolved));
// Load the driver image using the image loader support module.
// It will determine if the image needs to be loaded via the shim or can be loaded directly. // It will determine if the image needs to be loaded via the shim or can be loaded directly.
let image = ShimSupport::load(sprout_image, ShimInput::ResolvedPath(&resolved))?; let image = ImageLoader::load(request)?;
// Start the driver image, this is expected to return control to sprout. // Start the driver image, this is expected to return control to sprout.
// There is no guarantee that the driver will actually return control as it is // There is no guarantee that the driver will actually return control as it is
// just a standard EFI image. // just a standard EFI image.
uefi::boot::start_image(image).context("unable to start driver image")?; uefi::boot::start_image(*image.handle()).context("unable to start driver image")?;
Ok(()) Ok(())
} }

View File

@@ -93,7 +93,17 @@ impl BootableEntry {
} }
/// Determine if this entry matches `needle` by comparing to the name or title of the entry. /// Determine if this entry matches `needle` by comparing to the name or title of the entry.
/// If `needle` ends with *, we will match a partial match.
pub fn is_match(&self, needle: &str) -> bool { pub fn is_match(&self, needle: &str) -> bool {
// If the needle ends with '*', we will accept a partial match.
if needle.ends_with("*") {
// Strip off any '*' at the end.
let partial = needle.trim_end_matches("*");
// Check if the name or title start with the partial match.
return self.name.starts_with(partial) || self.title.starts_with(partial);
}
// Standard quality matching rules.
self.name == needle || self.title == needle self.name == needle || self.title == needle
} }

View File

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

View File

@@ -1,7 +1,6 @@
use crate::context::SproutContext; use crate::context::SproutContext;
use crate::entries::BootableEntry; use crate::entries::BootableEntry;
use crate::generators::bls::entry::BlsEntry; use crate::generators::bls::entry::BlsEntry;
use crate::utils;
use crate::utils::vercmp; use crate::utils::vercmp;
use alloc::format; use alloc::format;
use alloc::rc::Rc; use alloc::rc::Rc;
@@ -89,8 +88,9 @@ pub fn generate(context: Rc<SproutContext>, bls: &BlsConfiguration) -> Result<Ve
let path = context.stamp(&bls.path); let path = context.stamp(&bls.path);
// Resolve the path to the BLS directory. // Resolve the path to the BLS directory.
let bls_resolved = utils::resolve_path(Some(context.root().loaded_image_path()?), &path) let bls_resolved =
.context("unable to resolve bls path")?; eficore::path::resolve_path(Some(context.root().loaded_image_path()?), &path)
.context("unable to resolve bls path")?;
// Construct a filesystem path to the BLS entries directory. // Construct a filesystem path to the BLS entries directory.
let mut entries_path = PathBuf::from( let mut entries_path = PathBuf::from(

View File

@@ -1,19 +1,12 @@
#![doc = include_str!("../README.md")] #![doc = include_str!("../README.md")]
#![no_std] #![no_std]
#![no_main] #![no_main]
extern crate alloc; extern crate alloc;
use crate::context::{RootContext, SproutContext}; use crate::context::{RootContext, SproutContext};
use crate::entries::BootableEntry; use crate::entries::BootableEntry;
use crate::integrations::bootloader_interface::{BootloaderInterface, BootloaderInterfaceTimeout};
use crate::options::SproutOptions; use crate::options::SproutOptions;
use crate::options::parser::OptionsRepresentable;
use crate::phases::phase; use crate::phases::phase;
use crate::platform::timer::PlatformTimer;
use crate::platform::tpm::PlatformTpm;
use crate::secure::SecureBoot;
use crate::utils::PartitionGuidForm;
use alloc::collections::BTreeMap; use alloc::collections::BTreeMap;
use alloc::format; use alloc::format;
use alloc::string::ToString; use alloc::string::ToString;
@@ -22,6 +15,12 @@ use anyhow::{Context, Result, bail};
use core::ops::Deref; use core::ops::Deref;
use core::time::Duration; use core::time::Duration;
use edera_sprout_config::RootConfiguration; use edera_sprout_config::RootConfiguration;
use eficore::bootloader_interface::{BootloaderInterface, BootloaderInterfaceTimeout};
use eficore::partition::PartitionGuidForm;
use eficore::platform::timer::PlatformTimer;
use eficore::platform::tpm::PlatformTpm;
use eficore::secure::SecureBoot;
use eficore::setup;
use log::{error, info, warn}; use log::{error, info, warn};
use uefi::entry; use uefi::entry;
use uefi::proto::device_path::LoadedImageDevicePath; use uefi::proto::device_path::LoadedImageDevicePath;
@@ -51,14 +50,11 @@ pub mod extractors;
/// generators: Runtime code that can generate entries with specific values. /// generators: Runtime code that can generate entries with specific values.
pub mod generators; pub mod generators;
/// platform: Integration or support code for specific hardware platforms.
pub mod platform;
/// menu: Display a boot menu to select an entry to boot. /// menu: Display a boot menu to select an entry to boot.
pub mod menu; pub mod menu;
/// integrations: Code that interacts with other systems. /// options: Parse the options of the Sprout executable.
pub mod integrations; pub mod options;
/// phases: Hooks into specific parts of the boot process. /// phases: Hooks into specific parts of the boot process.
pub mod phases; pub mod phases;
@@ -66,15 +62,6 @@ pub mod phases;
/// sbat: Secure Boot Attestation section. /// sbat: Secure Boot Attestation section.
pub mod sbat; pub mod sbat;
/// secure: Secure Boot support.
pub mod secure;
/// setup: Code that initializes the UEFI environment for Sprout.
pub mod setup;
/// options: Parse the options of the Sprout executable.
pub mod options;
/// utils: Utility functions that are used by other parts of Sprout. /// utils: Utility functions that are used by other parts of Sprout.
pub mod utils; pub mod utils;
@@ -85,7 +72,7 @@ const DELAY_ON_ERROR: Duration = Duration::from_secs(10);
fn run() -> Result<()> { fn run() -> Result<()> {
// For safety reasons, we will note that Secure Boot is in beta on Sprout. // For safety reasons, we will note that Secure Boot is in beta on Sprout.
if SecureBoot::enabled().context("unable to determine Secure Boot status")? { if SecureBoot::enabled().context("unable to determine Secure Boot status")? {
warn!("Secure Boot is enabled. Sprout Secure Boot is in beta."); warn!("Sprout Secure Boot is in beta. Some functionality may not work as expected.");
} }
// Start the platform timer. // Start the platform timer.
@@ -136,7 +123,7 @@ fn run() -> Result<()> {
// Grab the partition GUID of the ESP that sprout was loaded from. // Grab the partition GUID of the ESP that sprout was loaded from.
let loaded_image_partition_guid = let loaded_image_partition_guid =
utils::partition_guid(&loaded_image_path, PartitionGuidForm::Partition) eficore::partition::partition_guid(&loaded_image_path, PartitionGuidForm::Partition)
.context("unable to retrieve loaded image partition guid")?; .context("unable to retrieve loaded image partition guid")?;
// Set the partition GUID of the ESP that sprout was loaded from in the bootloader interface. // Set the partition GUID of the ESP that sprout was loaded from in the bootloader interface.
@@ -385,6 +372,9 @@ fn run() -> Result<()> {
fn efi_main() -> Status { fn efi_main() -> Status {
// Initialize the basic UEFI environment. // Initialize the basic UEFI environment.
// If initialization fails, we will return ABORTED. // If initialization fails, we will return ABORTED.
// NOTE: This function will also initialize the logger.
// The logger will panic if it is unable to initialize.
// It is guaranteed that if this returns, the logger is initialized.
if let Err(error) = setup::init() { if let Err(error) = setup::init() {
error!("unable to initialize environment: {}", error); error!("unable to initialize environment: {}", error);
return Status::ABORTED; return Status::ABORTED;
@@ -394,7 +384,7 @@ fn efi_main() -> Status {
let result = run(); let result = run();
if let Err(ref error) = result { if let Err(ref error) = result {
// Print an error trace. // Print an error trace.
error!("sprout encountered an error"); error!("sprout encountered an error: {}", error);
for (index, stack) in error.chain().enumerate() { for (index, stack) in error.chain().enumerate() {
error!("[{}]: {}", index, stack); error!("[{}]: {}", index, stack);
} }

View File

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

132
crates/boot/src/options.rs Normal file
View File

@@ -0,0 +1,132 @@
use alloc::string::{String, ToString};
use anyhow::Result;
use core::ptr::null_mut;
use jaarg::{
ErrorUsageWriter, ErrorUsageWriterContext, HelpWriter, HelpWriterContext, Opt, Opts,
ParseControl, ParseResult, StandardErrorUsageWriter, StandardFullHelpWriter,
};
use log::{error, info};
use uefi_raw::Status;
/// Default configuration file path.
const DEFAULT_CONFIG_PATH: &str = "\\sprout.toml";
/// The parsed options of sprout.
#[derive(Debug)]
pub struct SproutOptions {
/// Configures Sprout automatically based on the environment.
pub autoconfigure: bool,
/// Path to a configuration file to load.
pub config: String,
/// Entry to boot without showing the boot menu.
pub boot: Option<String>,
/// Force display of the boot menu.
pub force_menu: bool,
/// The timeout for the boot menu in seconds.
pub menu_timeout: Option<u64>,
}
/// The default Sprout options.
impl Default for SproutOptions {
fn default() -> Self {
Self {
autoconfigure: false,
config: DEFAULT_CONFIG_PATH.to_string(),
boot: None,
force_menu: false,
menu_timeout: None,
}
}
}
/// The options parser mechanism for Sprout.
impl SproutOptions {
/// Produces [SproutOptions] from the arguments provided by the UEFI core.
/// Internally we utilize the `jaarg` argument parser which has excellent no_std support.
pub fn parse() -> Result<Self> {
enum ArgID {
Help,
AutoConfigure,
Config,
Boot,
ForceMenu,
MenuTimeout,
}
// All the options for the Sprout executable.
const OPTIONS: Opts<ArgID> = Opts::new(&[
Opt::help_flag(ArgID::Help, &["--help"]).help_text("Display Sprout Help"),
Opt::flag(ArgID::AutoConfigure, &["--autoconfigure"])
.help_text("Enable Sprout autoconfiguration"),
Opt::value(ArgID::Config, &["--config"], "PATH")
.help_text("Path to Sprout configuration file"),
Opt::value(ArgID::Boot, &["--boot"], "ENTRY")
.help_text("Entry to boot, bypassing the menu"),
Opt::flag(ArgID::ForceMenu, &["--force-menu"]).help_text("Force showing the boot menu"),
Opt::value(ArgID::MenuTimeout, &["--menu-timeout"], "TIMEOUT")
.help_text("Boot menu timeout, in seconds"),
]);
// Acquire the arguments as determined by the UEFI core.
let args = eficore::env::args()?;
// Use the default value of sprout options and have the raw options be parsed into it.
let mut result = Self::default();
// Parse the OPTIONS into a map using jaarg.
match OPTIONS.parse(
"sprout",
args.iter(),
|program_name, id, _opt, _name, value| {
match id {
ArgID::AutoConfigure => {
// Enable autoconfiguration.
result.autoconfigure = true;
}
ArgID::Config => {
// The configuration file to load.
result.config = value.into();
}
ArgID::Boot => {
// The entry to boot.
result.boot = Some(value.into());
}
ArgID::ForceMenu => {
// Force showing of the boot menu.
result.force_menu = true;
}
ArgID::MenuTimeout => {
// The timeout for the boot menu in seconds.
result.menu_timeout = Some(value.parse::<u64>()?);
}
ArgID::Help => {
let ctx = HelpWriterContext {
options: &OPTIONS,
program_name,
};
info!("{}", StandardFullHelpWriter::new(ctx));
return Ok(ParseControl::Quit);
}
}
Ok(ParseControl::Continue)
},
|program_name, error| {
let ctx = ErrorUsageWriterContext {
options: &OPTIONS,
program_name,
error,
};
error!("{}", StandardErrorUsageWriter::new(ctx));
},
) {
ParseResult::ContinueSuccess => Ok(result),
ParseResult::ExitSuccess => unsafe {
uefi::boot::exit(uefi::boot::image_handle(), Status::SUCCESS, 0, null_mut());
},
ParseResult::ExitError => unsafe {
uefi::boot::exit(uefi::boot::image_handle(), Status::ABORTED, 0, null_mut());
},
}
}
}

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

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

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

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

View File

@@ -23,9 +23,9 @@ pub fn compare_versions_optional(a: Option<&str>, b: Option<&str>) -> Ordering {
match (a, b) { match (a, b) {
// If both have values, compare them. // If both have values, compare them.
(Some(a), Some(b)) => compare_versions(a, b), (Some(a), Some(b)) => compare_versions(a, b),
// If the second value is None, return that it is less than the first. // If the second value is None, then `a` is less than `b`.
(Some(_a), None) => Ordering::Less, (Some(_a), None) => Ordering::Less,
// If the first value is None, return that it is greater than the second. // If the first value is None, the `a` is greater than `b`.
(None, Some(_b)) => Ordering::Greater, (None, Some(_b)) => Ordering::Greater,
// If both values are None, return that they are equal. // If both values are None, return that they are equal.
(None, None) => Ordering::Equal, (None, None) => Ordering::Equal,

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

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

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

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

View File

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

View File

@@ -13,3 +13,4 @@ default-features = false
[lib] [lib]
name = "edera_sprout_config" name = "edera_sprout_config"
path = "src/lib.rs"

View File

@@ -78,8 +78,7 @@ pub struct RootConfiguration {
/// Options 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)] #[derive(Serialize, Deserialize, Debug, Default, Clone)]
pub struct OptionsConfiguration { pub struct OptionsConfiguration {
/// The entry to boot without showing the boot menu. /// The entry to mark as the default entry, instead of the first entry.
/// If not specified, a boot menu is shown.
#[serde(rename = "default-entry", default)] #[serde(rename = "default-entry", default)]
pub default_entry: Option<String>, pub default_entry: Option<String>,
/// The timeout of the boot menu. /// The timeout of the boot menu.

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

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

View File

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

View File

@@ -3,14 +3,14 @@ use alloc::vec::Vec;
use anyhow::{Context, Result, bail}; use anyhow::{Context, Result, bail};
use uefi::proto::loaded_image::{LoadOptionsError, LoadedImage}; use uefi::proto::loaded_image::{LoadOptionsError, LoadedImage};
/// Loads the command-line arguments passed to Sprout. /// Loads the command-line arguments passed to the current image.
pub fn args() -> Result<Vec<String>> { pub fn args() -> Result<Vec<String>> {
// Acquire the image handle of Sprout. // Acquire the current image handle.
let handle = uefi::boot::image_handle(); let handle = uefi::boot::image_handle();
// Open the LoadedImage protocol for Sprout. // Open the LoadedImage protocol for the current image.
let loaded_image = uefi::boot::open_protocol_exclusive::<LoadedImage>(handle) let loaded_image = uefi::boot::open_protocol_exclusive::<LoadedImage>(handle)
.context("unable to open loaded image protocol for sprout")?; .context("unable to open loaded image protocol for current image")?;
// Load the command-line argument string. // Load the command-line argument string.
let options = match loaded_image.load_options_as_cstr16() { let options = match loaded_image.load_options_as_cstr16() {
@@ -35,7 +35,7 @@ pub fn args() -> Result<Vec<String>> {
let options = options.to_string(); let options = options.to_string();
// Use shlex to parse the options. // Use shlex to parse the options.
// If shlex fails, we will fall back to a simple whitespace split. // If shlex fails, we will perform a simple whitespace split.
let mut args = shlex::split(&options).unwrap_or_else(|| { let mut args = shlex::split(&options).unwrap_or_else(|| {
options options
.split_ascii_whitespace() .split_ascii_whitespace()
@@ -43,6 +43,20 @@ pub fn args() -> Result<Vec<String>> {
.collect::<Vec<_>>() .collect::<Vec<_>>()
}); });
// Correct firmware that may add invalid arguments at the start.
// Witnessed this on a Dell Precision 5690 when direct booting.
args = args
.into_iter()
.skip_while(|arg| {
arg.chars()
.next()
// Filter out unprintable characters and backticks.
// Both of which have been observed in the wild.
.map(|c| c < 0x1f as char || c == '`')
.unwrap_or(false)
})
.collect();
// If there is a first argument, check if it is not an option. // If there is a first argument, check if it is not an option.
// If it is not, we will assume it is the path to the executable and remove it. // If it is not, we will assume it is the path to the executable and remove it.
if let Some(arg) = args.first() if let Some(arg) = args.first()
@@ -51,27 +65,5 @@ pub fn args() -> Result<Vec<String>> {
args.remove(0); args.remove(0);
} }
// 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;
};
// Check if the argument is a valid character.
// If it is not, remove it and continue.
let Some(first_character) = arg.chars().next() else {
break;
};
// If the character is not a printable character or a backtick, remove it and continue.
if first_character < 0x1f as char || first_character == '`' {
args.remove(0);
continue;
}
break;
}
Ok(args) Ok(args)
} }

View File

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

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

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

View File

@@ -0,0 +1,141 @@
use crate::loader::source::ImageSource;
use crate::secure::SecureBoot;
use crate::shim::hook::SecurityHook;
use crate::shim::{ShimInput, ShimSupport};
use anyhow::{Context, Result, bail};
use log::warn;
use uefi::Handle;
use uefi::boot::LoadImageSource;
/// Represents EFI image sources generically.
pub mod source;
/// Handle to a loaded EFI image.
pub struct ImageHandle {
/// Handle to the loaded image.
handle: Handle,
}
impl ImageHandle {
/// Create a new image handle based on a handle from the UEFI stack.
pub fn new(handle: Handle) -> Self {
Self { handle }
}
/// Retrieve the underlying handle.
pub fn handle(&self) -> &Handle {
&self.handle
}
}
/// Request to load an image from a source, with support for additional validation features.
pub struct ImageLoadRequest<'source> {
/// Handle to the current image.
current_image: Handle,
/// Source of the image to load.
source: ImageSource<'source>,
}
impl<'source> ImageLoadRequest<'source> {
/// Create a new image load request with a current image and a source.
pub fn new(current_image: Handle, source: ImageSource<'source>) -> Self {
Self {
current_image,
source,
}
}
/// Retrieve the current image.
pub fn current_image(&self) -> &Handle {
&self.current_image
}
/// Retrieve the source of the image to load.
pub fn source(&'source self) -> &'source ImageSource<'source> {
&self.source
}
/// Convert the request into a source.
pub fn into_source(self) -> ImageSource<'source> {
self.source
}
}
/// EFI image loader.
pub struct ImageLoader;
impl ImageLoader {
/// Load an image using the image `request` which allows
pub fn load(request: ImageLoadRequest) -> Result<ImageHandle> {
// Determine whether Secure Boot is enabled.
let secure_boot =
SecureBoot::enabled().context("unable to determine if secure boot is enabled")?;
// Determine whether the shim is loaded.
let shim_loaded = ShimSupport::loaded().context("unable to determine if shim is loaded")?;
// Determine whether the shim loader is available.
let shim_loader_available = ShimSupport::loader_available()
.context("unable to determine if shim loader is available")?;
// Determines whether LoadImage in Boot Services must be patched.
// Version 16 of the shim doesn't require extra effort to load Secure Boot binaries.
// If the image loader is installed, we can skip over the security hook.
let requires_security_hook = secure_boot && shim_loaded && !shim_loader_available;
// If the security hook is required, we will bail for now.
if requires_security_hook {
// Install the security hook, if possible. If it's not, this is necessary to continue,
// so we should bail.
let installed = SecurityHook::install().context("unable to install security hook")?;
if !installed {
bail!("unable to install security hook required for this platform");
}
}
// If the shim is loaded, we will need to retain the shim protocol to allow
// loading multiple images.
if shim_loaded {
// Retain the shim protocol after loading the image.
ShimSupport::retain()?;
}
// Clone the current image handle to use for loading the image.
let current_image = *request.current_image();
// Converts the source to a shim input with an owned data buffer.
let input = ShimInput::from(request.into_source())
.into_owned_data_buffer()
.context("unable to convert input to loaded data buffer")?;
// Constructs a LoadImageSource from the input.
let source = LoadImageSource::FromBuffer {
buffer: input.buffer().context("unable to get buffer from input")?,
file_path: input.file_path(),
};
// Loads the image using Boot Services LoadImage function.
let result = uefi::boot::load_image(current_image, source).context("unable to load image");
// If the security override is required, we will uninstall the security hook.
if requires_security_hook {
let uninstall_result = crate::shim::hook::SecurityHook::uninstall();
// Ensure we don't mask load image errors if uninstalling fails.
if result.is_err()
&& let Err(uninstall_error) = &uninstall_result
{
// Warn on the error since the load image error is more important.
warn!("unable to uninstall security hook: {}", uninstall_error);
} else {
// Otherwise, ensure we handle the original uninstallation result.
uninstall_result?;
}
}
// Assert the result and grab the handle.
let handle = result?;
// Retrieve the handle from the result and make a new image handle.
Ok(ImageHandle::new(handle))
}
}

View File

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

View File

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

View File

@@ -161,14 +161,31 @@ impl MediaLoaderHandle {
// Install a protocol interface for the device path. // Install a protocol interface for the device path.
// This ensures it can be located by other EFI programs. // This ensures it can be located by other EFI programs.
let primary_handle = unsafe { let primary_handle = match unsafe {
uefi::boot::install_protocol_interface( uefi::boot::install_protocol_interface(
None, None,
&DevicePathProtocol::GUID, &DevicePathProtocol::GUID,
path.as_ffi_ptr() as *mut c_void, path.as_ffi_ptr() as *mut c_void,
) )
} }
.context("unable to install media loader device path handle")?; .context("unable to install media loader device path handle")
{
// Acquiring the primary handle succeeded, so we can return the handle.
Ok(handle) => handle,
// If acquiring the primary handle failed, we free the device path and return the error.
Err(error) => {
// SAFETY: We know that the device path is leaked,
// so we can safely take a reference to it again.
// The UEFI stack failed to install the protocol interface
// if we reach here, so the path is no longer in use.
let path = unsafe { Box::from_raw(path) };
// Explicitly drop the path to clarify the lifetime.
drop(path);
// Return the original error.
return Err(error);
}
};
// Leak the data we need to pass to the UEFI stack. // Leak the data we need to pass to the UEFI stack.
let data = Box::leak(data); let data = Box::leak(data);

View File

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

View File

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

View File

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

View File

@@ -17,7 +17,7 @@ pub enum TickFrequency {
/// The platform provides the tick frequency. /// The platform provides the tick frequency.
Hardware(u64), Hardware(u64),
/// The tick frequency is measured internally. /// The tick frequency is measured internally.
Measured(u64, Duration), Measured(u64),
} }
impl TickFrequency { impl TickFrequency {
@@ -25,7 +25,7 @@ impl TickFrequency {
fn ticks(&self) -> u64 { fn ticks(&self) -> u64 {
match self { match self {
TickFrequency::Hardware(frequency) => *frequency, TickFrequency::Hardware(frequency) => *frequency,
TickFrequency::Measured(frequency, _) => *frequency, TickFrequency::Measured(frequency) => *frequency,
} }
} }

View File

@@ -10,16 +10,6 @@ pub fn ticks() -> u64 {
counter counter
} }
/// We can use the actual ticks value as our start value.
pub fn start() -> u64 {
ticks()
}
/// We can use the actual ticks value as our stop value.
pub fn stop() -> u64 {
ticks()
}
/// Our frequency is provided by cntfrq_el0 on the platform. /// Our frequency is provided by cntfrq_el0 on the platform.
pub fn frequency() -> TickFrequency { pub fn frequency() -> TickFrequency {
let frequency: u64; let frequency: u64;

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,17 +1,11 @@
use crate::integrations::shim::hook::SecurityHook; use crate::path::ResolvedPath;
use crate::secure::SecureBoot; use crate::variables::{VariableClass, VariableController};
use crate::utils;
use crate::utils::ResolvedPath;
use crate::utils::variables::{VariableClass, VariableController};
use alloc::boxed::Box; use alloc::boxed::Box;
use alloc::string::ToString; use alloc::string::ToString;
use alloc::vec::Vec; use alloc::vec::Vec;
use anyhow::{Context, Result, anyhow, bail}; use anyhow::{Context, Result, anyhow, bail};
use core::ffi::c_void; use core::ffi::c_void;
use core::pin::Pin; use core::pin::Pin;
use log::warn;
use uefi::Handle;
use uefi::boot::LoadImageSource;
use uefi::proto::device_path::text::{AllowShortcuts, DisplayOnly}; use uefi::proto::device_path::text::{AllowShortcuts, DisplayOnly};
use uefi::proto::device_path::{DevicePath, FfiDevicePath}; use uefi::proto::device_path::{DevicePath, FfiDevicePath};
use uefi::proto::unsafe_protocol; use uefi::proto::unsafe_protocol;
@@ -19,7 +13,7 @@ use uefi_raw::table::runtime::VariableVendor;
use uefi_raw::{Guid, Status, guid}; use uefi_raw::{Guid, Status, guid};
/// Security hook support. /// Security hook support.
mod hook; pub mod hook;
/// Support for the shim loader application for Secure Boot. /// Support for the shim loader application for Secure Boot.
pub struct ShimSupport; pub struct ShimSupport;
@@ -90,7 +84,7 @@ impl<'a> ShimInput<'a> {
let path = path let path = path
.to_string(DisplayOnly(false), AllowShortcuts(false)) .to_string(DisplayOnly(false), AllowShortcuts(false))
.context("unable to convert device path to string")?; .context("unable to convert device path to string")?;
let path = utils::resolve_path(None, &path.to_string()) let path = crate::path::resolve_path(None, &path.to_string())
.context("unable to resolve path")?; .context("unable to resolve path")?;
// Read the file path. // Read the file path.
let data = path.read_file()?; let data = path.read_file()?;
@@ -163,14 +157,14 @@ impl ShimSupport {
/// Determines whether the shim is loaded. /// Determines whether the shim is loaded.
pub fn loaded() -> Result<bool> { pub fn loaded() -> Result<bool> {
Ok(utils::find_handle(&Self::SHIM_LOCK_GUID) Ok(crate::handle::find_handle(&Self::SHIM_LOCK_GUID)
.context("unable to find shim lock protocol")? .context("unable to find shim lock protocol")?
.is_some()) .is_some())
} }
/// Determines whether the shim loader is available. /// Determines whether the shim loader is available.
pub fn loader_available() -> Result<bool> { pub fn loader_available() -> Result<bool> {
Ok(utils::find_handle(&Self::SHIM_IMAGE_LOADER_GUID) Ok(crate::handle::find_handle(&Self::SHIM_IMAGE_LOADER_GUID)
.context("unable to find shim image loader protocol")? .context("unable to find shim image loader protocol")?
.is_some()) .is_some())
} }
@@ -178,7 +172,7 @@ impl ShimSupport {
/// Use the shim to validate the `input`, returning [ShimVerificationOutput] when complete. /// Use the shim to validate the `input`, returning [ShimVerificationOutput] when complete.
pub fn verify(input: ShimInput) -> Result<ShimVerificationOutput> { pub fn verify(input: ShimInput) -> Result<ShimVerificationOutput> {
// Acquire the handle to the shim lock protocol. // Acquire the handle to the shim lock protocol.
let handle = utils::find_handle(&Self::SHIM_LOCK_GUID) let handle = crate::handle::find_handle(&Self::SHIM_LOCK_GUID)
.context("unable to find shim lock protocol")? .context("unable to find shim lock protocol")?
.ok_or_else(|| anyhow!("unable to find shim lock protocol"))?; .ok_or_else(|| anyhow!("unable to find shim lock protocol"))?;
// Acquire the protocol exclusively to the shim lock. // Acquire the protocol exclusively to the shim lock.
@@ -238,72 +232,6 @@ impl ShimSupport {
.unwrap_or(ShimVerificationOutput::VerifiedDataNotLoaded)) .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 /// Set the ShimRetainProtocol variable to indicate that shim should retain the protocols
/// for the full lifetime of boot services. /// for the full lifetime of boot services.
pub fn retain() -> Result<()> { pub fn retain() -> Result<()> {

View File

@@ -1,5 +1,4 @@
use crate::integrations::shim::{ShimInput, ShimSupport, ShimVerificationOutput}; use crate::shim::{ShimInput, ShimSupport, ShimVerificationOutput};
use crate::utils;
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use core::slice; use core::slice;
use log::warn; use log::warn;
@@ -181,14 +180,14 @@ impl SecurityHook {
/// Install the security hook if needed. /// Install the security hook if needed.
pub fn install() -> Result<bool> { pub fn install() -> Result<bool> {
// Find the security arch protocol. If we can't find it, we will return false. // Find the security arch protocol. If we can't find it, we will return false.
let Some(hook_arch) = utils::find_handle(&SECURITY_ARCH_GUID) let Some(hook_arch) = crate::handle::find_handle(&SECURITY_ARCH_GUID)
.context("unable to check security arch existence")? .context("unable to check security arch existence")?
else { else {
return Ok(false); return Ok(false);
}; };
// Find the security arch2 protocol. If we can't find it, we will return false. // Find the security arch2 protocol. If we can't find it, we will return false.
let Some(hook_arch2) = utils::find_handle(&SECURITY_ARCH2_GUID) let Some(hook_arch2) = crate::handle::find_handle(&SECURITY_ARCH2_GUID)
.context("unable to check security arch2 existence")? .context("unable to check security arch2 existence")?
else { else {
return Ok(false); return Ok(false);
@@ -228,14 +227,14 @@ impl SecurityHook {
/// Uninstalls the global security hook, if installed. /// Uninstalls the global security hook, if installed.
pub fn uninstall() -> Result<()> { pub fn uninstall() -> Result<()> {
// Find the security arch protocol. If we can't find it, we will do nothing. // Find the security arch protocol. If we can't find it, we will do nothing.
let Some(hook_arch) = utils::find_handle(&SECURITY_ARCH_GUID) let Some(hook_arch) = crate::handle::find_handle(&SECURITY_ARCH_GUID)
.context("unable to check security arch existence")? .context("unable to check security arch existence")?
else { else {
return Ok(()); return Ok(());
}; };
// Find the security arch2 protocol. If we can't find it, we will do nothing. // Find the security arch2 protocol. If we can't find it, we will do nothing.
let Some(hook_arch2) = utils::find_handle(&SECURITY_ARCH2_GUID) let Some(hook_arch2) = crate::handle::find_handle(&SECURITY_ARCH2_GUID)
.context("unable to check security arch2 existence")? .context("unable to check security arch2 existence")?
else { else {
return Ok(()); return Ok(());

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,137 +0,0 @@
use crate::options::parser::{OptionDescription, OptionForm, OptionsRepresentable};
use alloc::collections::BTreeMap;
use alloc::string::{String, ToString};
use anyhow::{Context, Result, bail};
/// Acquire arguments from UEFI environment.
pub mod env;
/// The Sprout options parser.
pub mod parser;
/// Default configuration file path.
const DEFAULT_CONFIG_PATH: &str = "\\sprout.toml";
/// The parsed options of sprout.
#[derive(Debug)]
pub struct SproutOptions {
/// Configures Sprout automatically based on the environment.
pub autoconfigure: bool,
/// Path to a configuration file to load.
pub config: String,
/// Entry to boot without showing the boot menu.
pub boot: Option<String>,
/// Force display of the boot menu.
pub force_menu: bool,
/// The timeout for the boot menu in seconds.
pub menu_timeout: Option<u64>,
}
/// The default Sprout options.
impl Default for SproutOptions {
fn default() -> Self {
Self {
autoconfigure: false,
config: DEFAULT_CONFIG_PATH.to_string(),
boot: None,
force_menu: false,
menu_timeout: None,
}
}
}
/// The options parser mechanism for Sprout.
impl OptionsRepresentable for SproutOptions {
/// Produce the [SproutOptions] structure.
type Output = Self;
/// 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 {
description: "Path to Sprout configuration file",
form: OptionForm::Value,
},
),
(
"boot",
OptionDescription {
description: "Entry to boot, bypassing the menu",
form: OptionForm::Value,
},
),
(
"force-menu",
OptionDescription {
description: "Force showing of the boot menu",
form: OptionForm::Flag,
},
),
(
"menu-timeout",
OptionDescription {
description: "Boot menu timeout, in seconds",
form: OptionForm::Value,
},
),
(
"help",
OptionDescription {
description: "Display Sprout Help",
form: OptionForm::Help,
},
),
]
}
/// Produces [SproutOptions] from the parsed raw `options` map.
fn produce(options: BTreeMap<String, Option<String>>) -> Result<Self> {
// Use the default value of sprout options and have the raw options be parsed into it.
let mut result = Self::default();
for (key, value) in options {
match key.as_str() {
"autoconfigure" => {
// Enable autoconfiguration.
result.autoconfigure = true;
}
"config" => {
// The configuration file to load.
result.config = value.context("--config option requires a value")?;
}
"boot" => {
// The entry to boot.
result.boot = Some(value.context("--boot option requires a value")?);
}
"force-menu" => {
// Force showing of the boot menu.
result.force_menu = true;
}
"menu-timeout" => {
// The timeout for the boot menu in seconds.
let value = value.context("--menu-timeout option requires a value")?;
let value = value
.parse::<u64>()
.context("menu-timeout must be a number")?;
result.menu_timeout = Some(value);
}
_ => bail!("unknown option: --{key}"),
}
}
Ok(result)
}
}

View File

@@ -1,153 +0,0 @@
use crate::options::env;
use alloc::collections::BTreeMap;
use alloc::string::{String, ToString};
use anyhow::{Context, Result, bail};
use core::ptr::null_mut;
use log::info;
use uefi_raw::Status;
/// The type of option. This disambiguates different behavior
/// of how options are handled.
#[derive(Debug, Clone, Ord, PartialOrd, Eq, PartialEq)]
pub enum OptionForm {
/// A flag, like --verbose.
Flag,
/// A value, in the form --abc 123 or --abc=123.
Value,
/// Help flag, like --help.
Help,
}
/// The description of an option, used in the options parser
/// to make decisions about how to progress.
#[derive(Debug, Clone)]
pub struct OptionDescription<'a> {
/// The description of the option.
pub description: &'a str,
/// The type of option to parse as.
pub form: OptionForm,
}
/// Represents a type that can be parsed from command line arguments.
/// This is a super minimal options parser mechanism just for Sprout.
pub trait OptionsRepresentable {
/// The output type that parsing will produce.
type Output;
/// The configured options for this type. This should describe all the options
/// that are valid to produce the type. The left hand side is the name of the option,
/// and the right hand side is the description.
fn options() -> &'static [(&'static str, OptionDescription<'static>)];
/// Produces the type by taking the `options` and processing it into the output.
fn produce(options: BTreeMap<String, Option<String>>) -> Result<Self::Output>;
/// For minimalism, we don't want a full argument parser. Instead, we use
/// a simple --xyz = xyz: None and --abc 123 = abc: Some("123") format.
/// We also support --abc=123 = abc: Some("123") format.
fn parse_raw() -> Result<BTreeMap<String, Option<String>>> {
// Access the configured options for this type.
let configured: BTreeMap<_, _> = BTreeMap::from_iter(Self::options().to_vec());
// Collect all the arguments to Sprout.
// Skip the first argument, which is the path to our executable.
let args = env::args()?;
// Represent options as key-value pairs.
let mut options = BTreeMap::new();
// Iterators makes this way easier.
let mut iterator = args.into_iter().peekable();
loop {
// Consume the next option, if any.
let Some(option) = iterator.next() else {
break;
};
// If the option doesn't start with --, that is invalid.
if !option.starts_with("--") {
bail!("invalid option: {option}");
}
// Strip the -- prefix off.
let mut option = option["--".len()..].trim().to_string();
// An optional value.
let mut value = None;
// Check if the option is of the form --abc=123
if let Some((part_key, part_value)) = option.split_once('=') {
let part_key = part_key.to_string();
let part_value = part_value.to_string();
option = part_key;
value = Some(part_value);
}
// Error on empty option names.
if option.is_empty() {
bail!("invalid empty option");
}
// Find the description of the configured option, if any.
let Some(description) = configured.get(option.as_str()) else {
bail!("invalid option: --{option}");
};
// Check if the option requires a value and error if none was provided.
if description.form == OptionForm::Value && value.is_none() {
// Check for the next value.
let maybe_next = iterator.peek();
// If the next value isn't another option, set the value to the next value.
// Otherwise, it is None.
value = if let Some(next) = maybe_next
&& !next.starts_with("--")
{
iterator.next()
} else {
None
};
}
// If the option form does not support a value and there is a value, error.
if description.form != OptionForm::Value && value.is_some() {
bail!("option --{} does not take a value", option);
}
// Handle the --help flag case.
if description.form == OptionForm::Help {
// Generic configured options output.
info!("Configured Options:");
for (name, description) in &configured {
info!(
" --{}{}: {}",
name,
if description.form == OptionForm::Value {
" <value>"
} else {
""
},
description.description
);
}
// Exit because the help has been displayed.
unsafe {
uefi::boot::exit(uefi::boot::image_handle(), Status::SUCCESS, 0, null_mut());
};
}
// Insert the option and the value into the map.
options.insert(option, value);
}
Ok(options)
}
/// Parses the program arguments as a [Self::Output], calling [Self::parse_raw] and [Self::produce].
fn parse() -> Result<Self::Output> {
// Parse the program arguments into a raw map.
let options = Self::parse_raw().context("unable to parse options")?;
// Produce the options from the map.
Self::produce(options)
}
}

View File

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

View File

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

View File

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

View File

@@ -1,8 +0,0 @@
use anyhow::{Context, Result};
/// Initializes the UEFI environment.
pub fn init() -> Result<()> {
// Initialize the uefi internals.
uefi::helpers::init().context("unable to initialize uefi")?;
Ok(())
}

View File

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

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

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

View File

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

View File

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

View File

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

View File

@@ -30,9 +30,9 @@ if [ "${QEMU_GDB_WAIT}" = "1" ]; then
set -- "${@}" "-S" set -- "${@}" "-S"
fi fi
set -- "${@}" -smp 2 -m 4096 set -- "${@}" -nodefaults -smp 2 -m 4096
if [ "${NO_GRAPHICAL_BOOT}" = "1" ]; then if [ "${NO_GRAPHICAL}" = "1" ]; then
set -- "${@}" -nographic set -- "${@}" -nographic
else else
if [ "${GRAPHICAL_ONLY}" != "1" ]; then if [ "${GRAPHICAL_ONLY}" != "1" ]; then
@@ -40,9 +40,9 @@ else
set -- "${@}" -serial stdio set -- "${@}" -serial stdio
else else
set -- "${@}" \ set -- "${@}" \
-device virtio-serial-pci,id=vs0 \ -device 'virtio-serial-pci,id=vs0' \
-chardev stdio,id=stdio0 \ -chardev 'stdio,id=stdio0,signal=off' \
-device virtconsole,chardev=stdio0,id=console0 -device 'virtconsole,chardev=stdio0,id=console0,name=alpine'
fi fi
fi fi
@@ -55,6 +55,19 @@ else
fi fi
fi fi
if [ "${NO_INPUT}" != "1" ]; then
set -- "${@}" \
-device qemu-xhci \
-device usb-kbd \
-device usb-mouse
fi
if [ "${NO_NETWORK}" != "1" ]; then
set -- "${@}" \
-netdev 'user,id=network0' \
-device 'virtio-net-pci,netdev=network0'
fi
rm -f "${FINAL_DIR}/ovmf-boot.fd" rm -f "${FINAL_DIR}/ovmf-boot.fd"
cp "${FINAL_DIR}/ovmf.fd" "${FINAL_DIR}/ovmf-boot.fd" cp "${FINAL_DIR}/ovmf.fd" "${FINAL_DIR}/ovmf-boot.fd"
if [ "${TARGET_ARCH}" = "aarch64" ]; then if [ "${TARGET_ARCH}" = "aarch64" ]; then
@@ -63,7 +76,7 @@ fi
# shellcheck disable=SC2086 # shellcheck disable=SC2086
set -- "${@}" \ set -- "${@}" \
-drive "if=pflash,file=${FINAL_DIR}/ovmf-boot.fd,format=raw,readonly=on" \ -drive "if=pflash,file=${FINAL_DIR}/ovmf-boot.fd,format=raw,readonly=on" \
-device nvme,drive=disk1,serial=cafebabe -device 'nvme,drive=disk1,serial=cafebabe'
set -- "${@}" \ set -- "${@}" \
-drive "if=none,file=${FINAL_DIR}/sprout.img,format=raw,id=disk1,readonly=on" -drive "if=none,file=${FINAL_DIR}/sprout.img,format=raw,id=disk1,readonly=on"

View File

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

View File

@@ -48,7 +48,7 @@ copy_from_image_polyfill() {
SOURCE="${2}" SOURCE="${2}"
TARGET="${3}" TARGET="${3}"
docker build -t "${IMAGE}-copy-polyfill:${DOCKER_TAG}" --build-arg "TARGET_IMAGE=${IMAGE}:${DOCKER_TAG}" \ docker build --platform="${DOCKER_TARGET}" -t "${IMAGE}-copy-polyfill:${DOCKER_TAG}" --build-arg "TARGET_IMAGE=${IMAGE}:${DOCKER_TAG}" \
-f hack/dev/utils/Dockerfile.copy-polyfill hack -f hack/dev/utils/Dockerfile.copy-polyfill hack
# note: the -w '//' is a workaround for Git Bash where / is magically rewritten. # note: the -w '//' is a workaround for Git Bash where / is magically rewritten.
docker run --rm -i -w '//' "${IMAGE}-copy-polyfill:${DOCKER_TAG}" cat "image/${SOURCE}" >"${TARGET}" docker run --rm -i -w '//' "${IMAGE}-copy-polyfill:${DOCKER_TAG}" cat "image/${SOURCE}" >"${TARGET}"
@@ -72,6 +72,7 @@ if [ "${SKIP_KERNEL_BUILD}" != "1" ]; then
fi fi
copy_from_image "${DOCKER_PREFIX}/sprout-kernel-${TARGET_ARCH}" "kernel.efi" "${FINAL_DIR}/kernel.efi" copy_from_image "${DOCKER_PREFIX}/sprout-kernel-${TARGET_ARCH}" "kernel.efi" "${FINAL_DIR}/kernel.efi"
copy_from_image "${DOCKER_PREFIX}/sprout-kernel-${TARGET_ARCH}" "kernel.modules.tgz" "${FINAL_DIR}/kernel.modules.tgz"
fi fi
if [ "${SKIP_VM_BUILD}" != "1" ]; then if [ "${SKIP_VM_BUILD}" != "1" ]; then
@@ -80,8 +81,12 @@ if [ "${SKIP_VM_BUILD}" != "1" ]; then
-f hack/dev/vm/Dockerfile.ovmf "${FINAL_DIR}" -f hack/dev/vm/Dockerfile.ovmf "${FINAL_DIR}"
copy_from_image "${DOCKER_PREFIX}/sprout-ovmf-${TARGET_ARCH}" "ovmf.fd" "${FINAL_DIR}/ovmf.fd" copy_from_image "${DOCKER_PREFIX}/sprout-ovmf-${TARGET_ARCH}" "ovmf.fd" "${FINAL_DIR}/ovmf.fd"
copy_from_image "${DOCKER_PREFIX}/sprout-ovmf-${TARGET_ARCH}" "shell.efi" "${FINAL_DIR}/shell.efi" copy_from_image "${DOCKER_PREFIX}/sprout-ovmf-${TARGET_ARCH}" "shell.efi" "${FINAL_DIR}/shell.efi"
rm -rf "${FINAL_DIR}/initramfs.build"
mkdir -p "${FINAL_DIR}/initramfs.build"
cp -r "hack/dev/vm/files" "${FINAL_DIR}/initramfs.build/files"
cp "${FINAL_DIR}/kernel.modules.tgz" "${FINAL_DIR}/initramfs.build/kernel.modules.tgz"
docker build --platform="${DOCKER_TARGET}" -t "${DOCKER_PREFIX}/sprout-initramfs-${TARGET_ARCH}:${DOCKER_TAG}" \ docker build --platform="${DOCKER_TARGET}" -t "${DOCKER_PREFIX}/sprout-initramfs-${TARGET_ARCH}:${DOCKER_TAG}" \
-f hack/dev/vm/Dockerfile.initramfs "${FINAL_DIR}" -f hack/dev/vm/Dockerfile.initramfs "${FINAL_DIR}/initramfs.build"
copy_from_image "${DOCKER_PREFIX}/sprout-initramfs-${TARGET_ARCH}" "initramfs" "${FINAL_DIR}/initramfs" copy_from_image "${DOCKER_PREFIX}/sprout-initramfs-${TARGET_ARCH}" "initramfs" "${FINAL_DIR}/initramfs"
if [ -n "${SPROUT_XEN_EFI_OVERRIDE}" ]; then if [ -n "${SPROUT_XEN_EFI_OVERRIDE}" ]; then

View File

@@ -8,7 +8,7 @@ has-item = "\\vmlinuz"
[actions.chainload-kernel] [actions.chainload-kernel]
chainload.path = "$boot\\vmlinuz" chainload.path = "$boot\\vmlinuz"
chainload.options = ["console=hvc0"] chainload.options = ["console=hvc0", "overlaytmpfs=yes"]
chainload.linux-initrd = "$boot\\initramfs" chainload.linux-initrd = "$boot\\initramfs"
[entries.kernel] [entries.kernel]

View File

@@ -9,7 +9,7 @@ has-item = "\\vmlinuz"
[actions.chainload-kernel] [actions.chainload-kernel]
chainload.path = "$boot\\vmlinuz" chainload.path = "$boot\\vmlinuz"
chainload.options = ["console=hvc0"] chainload.options = ["console=hvc0", "overlaytmpfs=yes"]
chainload.linux-initrd = "$boot\\initramfs" chainload.linux-initrd = "$boot\\initramfs"
[entries.kernel] [entries.kernel]

View File

@@ -1,7 +1,7 @@
ARG KERNEL_SOURCE_URL=https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.17.2.tar.xz ARG KERNEL_SOURCE_URL=https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.17.8.tar.xz
ARG KERNEL_CHECKSUM=sha256:fdebcb065065f5c1b8dc68a6fb59cda50cdddbf9103d207c2196d55ea764f57f ARG KERNEL_CHECKSUM=sha256:5a8de64a75fca706c01c6c0a77cf75a74618439db195e25f1f0268af6b2fb1da
FROM --platform=$BUILDPLATFORM debian:trixie@sha256:fd8f5a1df07b5195613e4b9a0b6a947d3772a151b81975db27d47f093f60c6e6 AS buildenv FROM --platform=$BUILDPLATFORM debian:trixie@sha256:01a723bf5bfb21b9dda0c9a33e0538106e4d02cce8f557e118dd61259553d598 AS buildenv
RUN export DEBIAN_FRONTEND=noninteractive && apt-get update && apt-get install -y \ RUN export DEBIAN_FRONTEND=noninteractive && apt-get update && apt-get install -y \
build-essential squashfs-tools python3-yaml \ build-essential squashfs-tools python3-yaml \
patch diffutils sed mawk findutils zstd \ patch diffutils sed mawk findutils zstd \
@@ -32,6 +32,9 @@ ENV BUILDPLATFORM=${BUILDPLATFORM}
ENV TARGETPLATFORM=${TARGETPLATFORM} ENV TARGETPLATFORM=${TARGETPLATFORM}
WORKDIR /build/src WORKDIR /build/src
RUN /build/docker-build.sh RUN /build/docker-build.sh
COPY --chown=build:build docker-install.sh /build/docker-install.sh
RUN /build/docker-install.sh
FROM scratch AS final FROM scratch AS final
COPY --from=build /build/src/kernel.image /kernel.efi COPY --from=build /build/src/kernel.image /kernel.efi
COPY --from=build /build/src/kernel.modules.tgz /kernel.modules.tgz

View File

@@ -28,21 +28,33 @@ else
exit 1 exit 1
fi fi
echo "CROSS_COMPILE=${MAYBE_CROSS_COMPILE}" > kernel.buildenv
echo "TARGET_KARCH=${TARGET_KARCH}" >> kernel.buildenv
make CROSS_COMPILE="${MAYBE_CROSS_COMPILE}" ARCH="${TARGET_KARCH}" defconfig make CROSS_COMPILE="${MAYBE_CROSS_COMPILE}" ARCH="${TARGET_KARCH}" defconfig
if [ "${TARGET_KARCH}" = "x86_64" ]; then if [ "${TARGET_KARCH}" = "x86_64" ]; then
make CROSS_COMPILE="${MAYBE_CROSS_COMPILE}" ARCH="${TARGET_KARCH}" xen.config make CROSS_COMPILE="${MAYBE_CROSS_COMPILE}" ARCH="${TARGET_KARCH}" xen.config
./scripts/config -e XEN_PV ./scripts/config -e XEN_PV
./scripts/config -e XEN_PV_DOM0 ./scripts/config -e XEN_PV_DOM0
fi fi
make CROSS_COMPILE="${MAYBE_CROSS_COMPILE}" ARCH="${TARGET_KARCH}" mod2yesconfig
./scripts/config -e BPF_SYSCALL
./scripts/config -e UEVENT_HELPER
./scripts/config --set-str UEVENT_HELPER_PATH "/sbin/hotplug"
./scripts/config -e DRM_VIRTIO_GPU ./scripts/config -e DRM_VIRTIO_GPU
./scripts/config -e FRAMEBUFFER_CONSOLE ./scripts/config -e FRAMEBUFFER_CONSOLE
./scripts/config -e FRAMEBUFFER_CONSOLE_DETECT_PRIMARY ./scripts/config -e FRAMEBUFFER_CONSOLE_DETECT_PRIMARY
./scripts/config -e LOGO
./scripts/config -e XEN_DOM0 ./scripts/config -e XEN_DOM0
make CROSS_COMPILE="${MAYBE_CROSS_COMPILE}" ARCH="${TARGET_KARCH}" mod2noconfig
make "-j$(nproc)" CROSS_COMPILE="${MAYBE_CROSS_COMPILE}" ARCH="${TARGET_KARCH}" make "-j$(nproc)" CROSS_COMPILE="${MAYBE_CROSS_COMPILE}" ARCH="${TARGET_KARCH}"
[ -f "arch/x86/boot/bzImage" ] && cp "arch/x86/boot/bzImage" kernel.image [ -f "arch/x86/boot/bzImage" ] && cp "arch/x86/boot/bzImage" kernel.image
[ -f "arch/arm64/boot/Image.gz" ] && gzip -d <"arch/arm64/boot/Image.gz" >kernel.image [ -f "arch/arm64/boot/Image.gz" ] && gzip -d <"arch/arm64/boot/Image.gz" >kernel.image
exit 0 exit 0

View File

@@ -0,0 +1,11 @@
#!/bin/sh
set -e
. /build/src/kernel.buildenv
[ -f "arch/x86/boot/bzImage" ] && cp "arch/x86/boot/bzImage" kernel.image
[ -f "arch/arm64/boot/Image.gz" ] && gzip -d <"arch/arm64/boot/Image.gz" >kernel.image
make CROSS_COMPILE="${MAYBE_CROSS_COMPILE}" ARCH="${TARGET_KARCH}" INSTALL_MOD_PATH="/build/install" modules_install
cd /build/install
tar czpf /build/src/kernel.modules.tgz .

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
FROM alpine:3.22@sha256:4bcff63911fcb4448bd4fdacec207030997caf25e9bea4045fa6c8c44de311d1 AS rootfs FROM alpine:3.22@sha256:4b7ce07002c69e8f3d704a9c5d6fd3053be500b7f1c69fc0d80990c2ad8dd412 AS rootfs
RUN apk --no-cache add alpine-base tzdata RUN apk --no-cache add alpine-base tzdata wireless-regdb ifupdown-ng agetty
RUN rc-update add devfs sysinit && \ RUN rc-update add devfs sysinit && \
rc-update add dmesg sysinit && \ rc-update add dmesg sysinit && \
rc-update add mdev sysinit && \ rc-update add mdev sysinit && \
@@ -7,6 +7,7 @@ RUN rc-update add devfs sysinit && \
rc-update add sysctl boot && \ rc-update add sysctl boot && \
rc-update add hostname boot && \ rc-update add hostname boot && \
rc-update add bootmisc boot && \ rc-update add bootmisc boot && \
rc-update add networking boot && \
rc-update add syslog boot && \ rc-update add syslog boot && \
rc-update add mount-ro shutdown && \ rc-update add mount-ro shutdown && \
rc-update add killprocs shutdown && \ rc-update add killprocs shutdown && \
@@ -14,11 +15,13 @@ RUN rc-update add devfs sysinit && \
ln -s /sbin/init /init && \ ln -s /sbin/init /init && \
echo 'root:root' | chpasswd && \ echo 'root:root' | chpasswd && \
echo 'sprout' > /etc/hostname && \ echo 'sprout' > /etc/hostname && \
echo '' > /etc/motd && \ rm /etc/motd && \
ln -s /usr/share/zoneinfo/UTC /etc/localtime && \ ln -s /usr/share/zoneinfo/UTC /etc/localtime && \
echo 'hvc0::respawn:/sbin/getty -L hvc0 115200 vt100' >> /etc/inittab echo 'hvc0::respawn:/sbin/agetty --autologin root -L hvc0 115200 vt100' >> /etc/inittab
ADD kernel.modules.tgz /
COPY files/interfaces /etc/network/interfaces
FROM alpine:3.22@sha256:4bcff63911fcb4448bd4fdacec207030997caf25e9bea4045fa6c8c44de311d1 AS build FROM alpine:3.22@sha256:4b7ce07002c69e8f3d704a9c5d6fd3053be500b7f1c69fc0d80990c2ad8dd412 AS build
COPY --from=rootfs / /rootfs COPY --from=rootfs / /rootfs
WORKDIR /rootfs WORKDIR /rootfs
RUN find . | cpio -R 0:0 --ignore-devno --renumber-inodes -o -H newc --quiet > /initramfs RUN find . | cpio -R 0:0 --ignore-devno --renumber-inodes -o -H newc --quiet > /initramfs

View File

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

View File

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

View File

@@ -0,0 +1,3 @@
auto eth0
iface eth0
use dhcp

View File

@@ -1,4 +1,4 @@
[toolchain] [toolchain]
channel = "1.91.0" channel = "1.91.1"
components = ["rustfmt", "clippy"] components = ["rustfmt", "clippy"]
targets = ["x86_64-unknown-uefi", "aarch64-unknown-uefi"] targets = ["x86_64-unknown-uefi", "aarch64-unknown-uefi"]