Compare commits

...
158 Commits
Author SHA1 Message Date
alex 5afbd15fd0 sprout: version 0.0.29 (#99) 2026-09-26 19:07:20 -07:00
alex 2da1cea25c fix(autoconfigure): skip filesystems that can't be opened
If the device path or filesystem protocol of any detected filesystem
could not be opened, autoconfiguration failed entirely and boot was
aborted. Log a warning and skip that filesystem instead, so the other
filesystems are still autoconfigured.
2026-09-26 18:58:54 -07:00
alex 84bd84b7d2 fix(phases): run the late phase after the entry is chosen
The late phase is documented to run after the entry is chosen but
before its actions are executed, but it ran before the boot menu was
shown. Run it after the entry is selected and reported to the
bootloader interface, right before executing the entry's actions.
2026-09-26 18:58:54 -07:00
alex db0edb1b6e fix(bls): sort entries with a version before entries without
compare_versions_optional treated a missing version as newer than any
version, so among entries with equal sort-key and machine-id, an entry
without a version sorted first. Treat a missing version as the oldest,
matching systemd's strverscmp_improved.
2026-09-26 18:58:54 -07:00
alex 282dc5369a fix(bls): sort entries with a sort-key before entries without
Sort keys were compared as Options, so an entry without a sort-key
sorted before one with a sort-key, and machine-id and version were
compared even when neither entry had a sort-key. Per the Boot Loader
Specification and systemd-boot, an entry with a sort-key sorts first,
and machine-id and version are only used when both entries have one.
2026-09-26 18:58:54 -07:00
alex 44c8ab28af fix(bls): compare version numbers of any length
Numeric segments were parsed into a saturating u64, so any two numbers
above u64::MAX compared equal. Compare the digits instead, by length
and then digit by digit, as systemd does.
2026-09-26 18:58:54 -07:00
alex 29e405f11f fix(bls): sort numeric version segments newer than letters
When only one version had a digit at the current position, both sides
were parsed as numbers and the letter side read as 0. "0" then compared
equal to "a" and the result depended on what followed. Per the
specification, the side with digits is newer.
2026-09-26 18:58:54 -07:00
alex 9d6d88c5e0 fix(bls): sort pre-release versions before releases
The '~' check ran after the end-of-string check, so "1~rc1" compared as
newer than "1". Per the UAPI version format specification, '~' must be
handled first so a pre-release sorts older than the release it
precedes. An empty string is kept as the oldest version, matching
systemd's strverscmp_improved.
2026-09-26 18:58:54 -07:00
alex dd35c2153b fix(bls): skip unreadable or invalid BLS entries
A single BLS entry that could not be read, was not valid UTF-8, or
failed to parse aborted entry generation and therefore the whole boot,
contrary to the generator's documented best-effort behavior. Log a
warning and skip the entry instead.
2026-09-26 18:58:54 -07:00
alex dbc292e369 fix(bls): allow BLS directories without an entries directory
Autoconfigure detects BLS when either \loader\loader.conf or
\loader\entries exists, but the BLS generator required the entries
directory and failed the boot without it. This broke ESPs that only
have a loader.conf, such as UKI-only systemd-boot setups. Generate no
entries when the directory is missing.
2026-09-26 18:58:54 -07:00
alex 6f84a1014a fix(chainload): skip initrd paths without a file component
Autoconfigured BLS actions use "$root\\$initrd" as the initrd path.
For BLS entries without an initrd line this stamps to the root of the
filesystem, and Sprout tried to read that directory as the initrd,
failing the boot. Treat a path that resolves to no file as no initrd.
2026-09-26 18:58:54 -07:00
alex 9967a7ab80 fix(chainload): release LoadedImage before starting the image
The chainload action held the child image's LoadedImage protocol open
exclusively while calling start_image. Any image that opens its own
LoadedImage protocol exclusively, such as another uefi-rs application
or Sprout itself, then failed with ACCESS_DENIED. Scope the protocol to
setting the load options.
2026-09-26 18:58:54 -07:00
alex bcfb7dc680 fix(shim): take security arch2 boot_policy as a UEFI Boolean
The firmware passes BootPolicy as a UEFI BOOLEAN (a u8). Declaring it
as a Rust bool in the extern signature is undefined behavior for any
value other than 0 or 1. Use uefi_raw::Boolean instead.
2026-09-26 18:58:54 -07:00
alex bcc78fd4a3 fix(shim): don't overwrite original handlers when already hooked
If the security hook was installed while already installed, the
protocols contained Sprout's own handlers, which were then saved as the
"original" handlers. A verification failure would then call back into
the hook forever. Keep the existing saved state in that case.
2026-09-26 18:58:54 -07:00
alex ad37a2bcf3 fix(variables): accept UTF-16 values without a NUL terminator
EFI string variables written without a trailing NUL, or with extra NUL
padding, failed to decode and were silently treated as unset. Decode up
to the first NUL and terminate the string ourselves, as systemd-boot
does.

Because empty values now decode to an empty string rather than failing,
treat empty LoaderEntryDefault and LoaderEntryOneShot values as unset so
an empty one-shot entry doesn't fail to match any entry.
2026-09-26 18:58:54 -07:00
alex df5d8f5c39 fix(bootloader-interface): ignore invalid timeout values
A LoaderConfigTimeout or LoaderConfigTimeoutOneShot value that is not a
number or a known keyword made Sprout abort boot. Log a warning and
treat the variable as unset instead, matching systemd-boot.
2026-09-26 18:58:54 -07:00
alex 48e3bc48a9 fix(loader): install the security hook right before LoadImage
The hook was installed before retaining the shim protocol and reading
the image, so an error in either step returned without uninstalling
it. Install it immediately before LoadImage instead.
2026-09-26 18:58:54 -07:00
alex 5c2724fd22 fix(env): filter all ASCII control characters from leading args
The check for unprintable leading arguments added by firmware used
c < 0x1f, which missed 0x1f itself. Use is_ascii_control() instead.
2026-09-26 18:58:54 -07:00
alex f7e7a9122b fix(menu): ignore key events without an available key
Some firmware signals the key event without a key being available.
This previously aborted boot with "no key was pressed"; treat it as a
no-op and keep waiting instead.
2026-09-26 18:58:54 -07:00
alex 1f5bc07c22 fix(autoconfigure): match arm64 Image kernels
File names are lowercased before matching kernel prefixes, but the
arm64 prefix was "Image", so it could never match and arm64 kernels
were not autoconfigured. Use a lowercase "image" prefix.
2026-09-26 18:58:54 -07:00
dependabot[bot] 724180618b chore(deps): bump the actions-updates group across 1 directory with 9 updates (#93)
Bumps the actions-updates group with 9 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [step-security/harden-runner](https://github.com/step-security/harden-runner) | `2.17.0` | `2.21.1` |
| [actions/checkout](https://github.com/actions/checkout) | `6.0.2` | `7.0.1` |
| [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv) | `8.0.0` | `10.1.0` |
| [github/codeql-action/upload-sarif](https://github.com/github/codeql-action) | `4.35.1` | `4.38.1` |
| [github/codeql-action/init](https://github.com/github/codeql-action) | `4.35.1` | `4.38.1` |
| [github/codeql-action/analyze](https://github.com/github/codeql-action) | `4.35.1` | `4.38.1` |
| [actions/upload-artifact](https://github.com/actions/upload-artifact) | `7.0.0` | `7.0.1` |
| [actions/attest-build-provenance](https://github.com/actions/attest-build-provenance) | `4.1.0` | `4.2.2` |
| [actions/create-github-app-token](https://github.com/actions/create-github-app-token) | `3.0.0` | `3.2.0` |



Updates `step-security/harden-runner` from 2.17.0 to 2.21.1
- [Release notes](https://github.com/step-security/harden-runner/releases)
- [Commits](https://github.com/step-security/harden-runner/compare/f808768d1510423e83855289c910610ca9b43176...e14015d583714f6e62063499dc959a02595150a1)

Updates `actions/checkout` from 6.0.2 to 7.0.1
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/de0fac2e4500dabe0009e67214ff5f5447ce83dd...3d3c42e5aac5ba805825da76410c181273ba90b1)

Updates `astral-sh/setup-uv` from 8.0.0 to 10.1.0
- [Release notes](https://github.com/astral-sh/setup-uv/releases)
- [Commits](https://github.com/astral-sh/setup-uv/compare/cec208311dfd045dd5311c1add060b2062131d57...bec219d24cd3e171d82865faccec33120bb574f4)

Updates `github/codeql-action/upload-sarif` from 4.35.1 to 4.38.1
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/c10b8064de6f491fea524254123dbe5e09572f13...1c5b675653bb5c22dbe9b12b556ec555138e09fd)

Updates `github/codeql-action/init` from 4.35.1 to 4.38.1
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/c10b8064de6f491fea524254123dbe5e09572f13...1c5b675653bb5c22dbe9b12b556ec555138e09fd)

Updates `github/codeql-action/analyze` from 4.35.1 to 4.38.1
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/c10b8064de6f491fea524254123dbe5e09572f13...1c5b675653bb5c22dbe9b12b556ec555138e09fd)

Updates `actions/upload-artifact` from 7.0.0 to 7.0.1
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/bbbca2ddaa5d8feaa63e36b76fdaad77386f024f...043fb46d1a93c77aae656e7c1c64a875d1fc6a0a)

Updates `actions/attest-build-provenance` from 4.1.0 to 4.2.2
- [Release notes](https://github.com/actions/attest-build-provenance/releases)
- [Changelog](https://github.com/actions/attest-build-provenance/blob/main/RELEASE.md)
- [Commits](https://github.com/actions/attest-build-provenance/compare/a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32...4d101475d8b20a2381f78447822ac1eab6504dd8)

Updates `actions/create-github-app-token` from 3.0.0 to 3.2.0
- [Release notes](https://github.com/actions/create-github-app-token/releases)
- [Changelog](https://github.com/actions/create-github-app-token/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/create-github-app-token/compare/f8d387b68d61c58ab83c6c016672934102569859...bcd2ba49218906704ab6c1aa796996da409d3eb1)

---
updated-dependencies:
- dependency-name: actions/attest-build-provenance
  dependency-version: 4.1.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions-updates
- dependency-name: actions/checkout
  dependency-version: 7.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions-updates
- dependency-name: actions/create-github-app-token
  dependency-version: 3.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions-updates
- dependency-name: actions/upload-artifact
  dependency-version: 7.0.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions-updates
- dependency-name: astral-sh/setup-uv
  dependency-version: 8.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions-updates
- dependency-name: github/codeql-action/analyze
  dependency-version: 4.36.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions-updates
- dependency-name: github/codeql-action/init
  dependency-version: 4.36.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions-updates
- dependency-name: github/codeql-action/upload-sarif
  dependency-version: 4.36.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions-updates
- dependency-name: step-security/harden-runner
  dependency-version: 2.19.4
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions-updates
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-26 18:37:06 -07:00
Ben Leggett 8b39f703c6 fix(ci): scope token (#94) 2026-09-26 18:24:44 -07:00
alex 29cdc75776 chore(deps): bump rust to 1.98.1, bump all images, and dependencies (#97) 2026-09-25 11:28:32 -07:00
alex 0e4452b625 chore(upgrade): upgrade to rust v1.97.1, bump deps and migrate, bump images (#95) 2026-08-02 19:02:09 -07:00
alex aafda6e3da chore(deps): upgrade spin and shlex (#86) 2026-05-19 11:51:37 -05:00
Luca Di Maio 863aa25971 Merge pull request #85 from edera-dev/89luca89/tpm_work_ws2
sprout: measure xen and dom0 payloads and cmdlines into PCRs 9/11/12
2026-05-18 13:24:50 +02:00
Luca Di Maio 2c1c380ab5 sprout: measure xen and dom0 payloads and cmdlines into PCRs 9/11/12
Extend PCR 9 with the initrd, PCR 11 with the dom0 kernel,
and PCR 12 with the Xen options and dom0 cmdline.

Signed-off-by: Luca Di Maio <[email protected]>
2026-05-18 12:43:41 +02:00
dependabot[bot] 076be95306 chore(deps): bump the actions-updates group across 1 directory with 3 updates (#81)
Bumps the actions-updates group with 3 updates in the / directory: [step-security/harden-runner](https://github.com/step-security/harden-runner), [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv) and [github/codeql-action](https://github.com/github/codeql-action).


Updates `step-security/harden-runner` from 2.16.0 to 2.16.1
- [Release notes](https://github.com/step-security/harden-runner/releases)
- [Commits](https://github.com/step-security/harden-runner/compare/fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594...f808768d1510423e83855289c910610ca9b43176)

Updates `astral-sh/setup-uv` from 7.2.0 to 8.0.0
- [Release notes](https://github.com/astral-sh/setup-uv/releases)
- [Commits](https://github.com/astral-sh/setup-uv/compare/61cb8a9741eeb8a550a1b8544337180c0fc8476b...cec208311dfd045dd5311c1add060b2062131d57)

Updates `github/codeql-action` from 4.31.2 to 4.35.1
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/0499de31b99561a6d14a36a5f662c2a54f91beee...c10b8064de6f491fea524254123dbe5e09572f13)

---
updated-dependencies:
- dependency-name: step-security/harden-runner
  dependency-version: 2.16.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions-updates
- dependency-name: astral-sh/setup-uv
  dependency-version: 8.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions-updates
- dependency-name: github/codeql-action
  dependency-version: 4.35.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions-updates
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-16 14:45:25 -07:00
alex 6f9c4a2852 chore(deps): upgrade rust and dependencies (#82) 2026-04-16 21:43:36 +00:00
Ben Leggett d75fce041c Merge pull request #75 from edera-dev/bleggett/bls-unit-tests
Split out unit testable code
2026-03-30 18:08:53 -04:00
dependabot[bot] f9391261d5 chore(deps): bump the actions-updates group across 1 directory with 4 updates (#74)
Bumps the actions-updates group with 4 updates in the / directory: [step-security/harden-runner](https://github.com/step-security/harden-runner), [actions/upload-artifact](https://github.com/actions/upload-artifact), [actions/attest-build-provenance](https://github.com/actions/attest-build-provenance) and [actions/create-github-app-token](https://github.com/actions/create-github-app-token).


Updates `step-security/harden-runner` from 2.14.1 to 2.16.0
- [Release notes](https://github.com/step-security/harden-runner/releases)
- [Commits](https://github.com/step-security/harden-runner/compare/e3f713f2d8f53843e71c69a996d56f51aa9adfb9...fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594)

Updates `actions/upload-artifact` from 6.0.0 to 7.0.0
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/b7c566a772e6b6bfb58ed0dc250532a479d7789f...bbbca2ddaa5d8feaa63e36b76fdaad77386f024f)

Updates `actions/attest-build-provenance` from 3.2.0 to 4.1.0
- [Release notes](https://github.com/actions/attest-build-provenance/releases)
- [Changelog](https://github.com/actions/attest-build-provenance/blob/main/RELEASE.md)
- [Commits](https://github.com/actions/attest-build-provenance/compare/96278af6caaf10aea03fd8d33a09a777ca52d62f...a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32)

Updates `actions/create-github-app-token` from 3.0.0.pre.beta.2 to 3
- [Release notes](https://github.com/actions/create-github-app-token/releases)
- [Commits](https://github.com/actions/create-github-app-token/compare/bf559f85448f9380bcfa2899dbdc01eb5b37be3a...f8d387b68d61c58ab83c6c016672934102569859)

---
updated-dependencies:
- dependency-name: step-security/harden-runner
  dependency-version: 2.16.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions-updates
- dependency-name: actions/upload-artifact
  dependency-version: 7.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions-updates
- dependency-name: actions/attest-build-provenance
  dependency-version: 4.1.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions-updates
- dependency-name: actions/create-github-app-token
  dependency-version: '3'
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions-updates
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-30 20:23:45 +01:00
Benjamin Leggett c44b429553 fmt 2026-03-25 16:13:46 -04:00
Benjamin Leggett c1115af841 Skip the UEFI crates during cargo test 2026-03-25 16:07:21 -04:00
Benjamin Leggett b53d21cea5 Split out parsing stuff as well, and test it 2026-03-25 16:01:28 -04:00
Benjamin Leggett 133476a0df Split out some BLS stuff into a separate testable crate without UEFI types 2026-03-25 15:31:47 -04:00
alex c70d1e4d80 sprout: version 0.0.28 (#64) 2026-02-09 12:34:08 -08:00
Ariadne Conill 1f6fc237c6 Merge pull request #66 from edera-dev/chore/upgrade
chore(upgrade): bump all dependencies, rust, and images
2026-02-09 09:52:12 -08:00
alex c86215f196 chore(upgrade): bump all dependencies, rust, and images 2026-02-09 09:38:57 -08:00
dependabot[bot] 425ce8ede4 chore(deps): bump actions/attest-build-provenance (#65)
Bumps the actions-updates group with 1 update: [actions/attest-build-provenance](https://github.com/actions/attest-build-provenance).


Updates `actions/attest-build-provenance` from 3.1.0 to 3.2.0
- [Release notes](https://github.com/actions/attest-build-provenance/releases)
- [Changelog](https://github.com/actions/attest-build-provenance/blob/main/RELEASE.md)
- [Commits](https://github.com/actions/attest-build-provenance/compare/00014ed6ed5efc5b1ab7f7f34a39eb55d41aa4f8...96278af6caaf10aea03fd8d33a09a777ca52d62f)

---
updated-dependencies:
- dependency-name: actions/attest-build-provenance
  dependency-version: 3.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions-updates
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-09 08:55:39 -08:00
dependabot[bot] 656fde2044 chore(deps): bump step-security/harden-runner (#63)
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.14.0 to 2.14.1
- [Release notes](https://github.com/step-security/harden-runner/releases)
- [Commits](https://github.com/step-security/harden-runner/compare/20cf305ff2072d973412fa9b1e3a4f227bda3c76...e3f713f2d8f53843e71c69a996d56f51aa9adfb9)

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

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-03 15:20:17 -05:00
Ariadne Conill 5740c14e31 Merge pull request #62 from edera-dev/chore/code-cleanup-stamp-iter
chore(sprout): implement iterator stamping to cleanup code
2026-02-02 19:12:14 +01:00
alex c295de4951 chore(sprout): implement iterator stamping to cleanup code 2026-01-26 20:13:52 -08:00
dependabot[bot] 95e0360721 chore(deps): bump toml in the cargo-updates group (#59)
Bumps the cargo-updates group with 1 update: [toml](https://github.com/toml-rs/toml).


Updates `toml` from 0.9.10+spec-1.1.0 to 0.9.11+spec-1.1.0
- [Commits](https://github.com/toml-rs/toml/compare/toml-v0.9.10...toml-v0.9.11)

---
updated-dependencies:
- dependency-name: toml
  dependency-version: 0.9.11+spec-1.1.0
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo-updates
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-26 08:54:07 -08:00
dependabot[bot] 1370e1b39b chore(deps): bump the actions-updates group across 1 directory with 3 updates (#61)
Bumps the actions-updates group with 3 updates in the / directory: [actions/checkout](https://github.com/actions/checkout), [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv) and [actions/attest-build-provenance](https://github.com/actions/attest-build-provenance).


Updates `actions/checkout` from 6.0.1 to 6.0.2
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/8e8c483db84b4bee98b60c0593521ed34d9990e8...de0fac2e4500dabe0009e67214ff5f5447ce83dd)

Updates `astral-sh/setup-uv` from 7.1.6 to 7.2.0
- [Release notes](https://github.com/astral-sh/setup-uv/releases)
- [Commits](https://github.com/astral-sh/setup-uv/compare/681c641aba71e4a1c380be3ab5e12ad51f415867...61cb8a9741eeb8a550a1b8544337180c0fc8476b)

Updates `actions/attest-build-provenance` from 3.0.0 to 3.1.0
- [Release notes](https://github.com/actions/attest-build-provenance/releases)
- [Changelog](https://github.com/actions/attest-build-provenance/blob/main/RELEASE.md)
- [Commits](https://github.com/actions/attest-build-provenance/compare/977bb373ede98d70efdf65b84cb5f73e068dcc2a...00014ed6ed5efc5b1ab7f7f34a39eb55d41aa4f8)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 6.0.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions-updates
- dependency-name: astral-sh/setup-uv
  dependency-version: 7.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions-updates
- dependency-name: actions/attest-build-provenance
  dependency-version: 3.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions-updates
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-26 08:53:40 -08:00
alex b60c345958 sprout: version 0.0.27 (#57) 2025-12-23 22:51:26 -08:00
dependabot[bot] 45270d3bf3 chore(deps): bump the actions-updates group with 3 updates (#56)
Bumps the actions-updates group with 3 updates: [step-security/harden-runner](https://github.com/step-security/harden-runner), [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv) and [actions/upload-artifact](https://github.com/actions/upload-artifact).


Updates `step-security/harden-runner` from 2.13.2 to 2.14.0
- [Release notes](https://github.com/step-security/harden-runner/releases)
- [Commits](https://github.com/step-security/harden-runner/compare/95d9a5deda9de15063e7595e9719c11c38c90ae2...20cf305ff2072d973412fa9b1e3a4f227bda3c76)

Updates `astral-sh/setup-uv` from 7.1.2 to 7.1.6
- [Release notes](https://github.com/astral-sh/setup-uv/releases)
- [Commits](https://github.com/astral-sh/setup-uv/compare/85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41...681c641aba71e4a1c380be3ab5e12ad51f415867)

Updates `actions/upload-artifact` from 5.0.0 to 6.0.0
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/330a01c490aca151604b8cf639adc76d48f6c5d4...b7c566a772e6b6bfb58ed0dc250532a479d7789f)

---
updated-dependencies:
- dependency-name: step-security/harden-runner
  dependency-version: 2.14.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions-updates
- dependency-name: astral-sh/setup-uv
  dependency-version: 7.1.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions-updates
- dependency-name: actions/upload-artifact
  dependency-version: 6.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions-updates
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-23 18:54:25 -08:00
alex 6a57c72869 feat(boot): introduce sort keys, which are sorted in reverse order, to make boot order more configurable (#55) 2025-12-21 14:11:26 -08:00
alex d50f22a386 chore(docs): fix all rustdoc warnings (#54) 2025-12-21 14:11:10 -08:00
alex a8a3774c35 chore(upgrade): upgrade dependencies, rust => 1.92.0, dev kernel => 6.18.2, alpine => v3.23 (#53) 2025-12-20 23:00:51 -08:00
alex 81c8217ee0 fix(boot): on boot handoff, clear the screen using clear rather than reset (#52)
On some Dell firmware, the reset operation doesn't actually clear the screen.
This change uses clear instead of reset to ensure the screen is properly cleared.
2025-12-20 22:29:44 -08:00
alex 17e729d068 fix(eficore): unregister media loader on drop to prevent more opportunities for leaks (#50) 2025-12-20 22:01:04 -08:00
alex 2da457ee7c feat(boot): clear screen before boot handoff, if --retain-boot-console is not specified on the command-line (#51) 2025-12-20 21:45:35 -08:00
dependabot[bot] fafabe234e chore(deps): bump log from 0.4.28 to 0.4.29 in the cargo-updates group (#48)
Bumps the cargo-updates group with 1 update: [log](https://github.com/rust-lang/log).


Updates `log` from 0.4.28 to 0.4.29
- [Release notes](https://github.com/rust-lang/log/releases)
- [Changelog](https://github.com/rust-lang/log/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/log/compare/0.4.28...0.4.29)

---
updated-dependencies:
- dependency-name: log
  dependency-version: 0.4.29
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo-updates
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-20 19:55:23 -08:00
dependabot[bot] c2cafe6c20 chore(deps): bump rust in the docker-updates group (#46)
Bumps the docker-updates group with 1 update: rust.


Updates `rust` from `fbcca3e` to `8efbfb7`

---
updated-dependencies:
- dependency-name: rust
  dependency-version: 1.91.1-alpine
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: docker-updates
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-19 10:53:01 -08:00
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](https://github.com/actions/checkout/compare/08c6903cd8c0fde910a37f88322edcfb5dd907a8...8e8c483db84b4bee98b60c0593521ed34d9990e8)

---
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] <[email protected]>
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
alex 5605056c82 chore(dev): make alpine boot fully clean with kernel module infra 2025-11-26 23:05:01 -08:00
alex 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
alex 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
alex 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](https://github.com/step-security/harden-runner/compare/f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a...95d9a5deda9de15063e7595e9719c11c38c90ae2)

---
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] <[email protected]>
2025-11-24 11:01:47 +00:00
alex 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
alex 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
alex 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
alex 5ad617c54f feat(entries): support '*' suffix as a partial match to an entry 2025-11-15 20:36:31 -08:00
alex 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
alex 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
alex c053f62b88 fix(autoconfigure/linux): detect Image as a kernel, which openSUSE uses 2025-11-15 13:41:07 -08:00
alex e0bd703511 chore(dev): autologin to alpine and writable rootfs 2025-11-15 13:37:50 -08:00
alex 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
alex 16755acdfe chore(release): sprout: version 0.0.25 2025-11-14 10:45:16 -05:00
alex 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
alex ecba8a5e02 chore(deps): upgrade rust to 1.91.1 and bump cargo and docker deps 2025-11-14 00:24:09 -05:00
alex 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
alex 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](https://github.com/astral-sh/setup-uv/compare/2ddd2b9cb38ad8efd50337e8ab201519a34c9f24...85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41)

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](https://github.com/github/codeql-action/compare/4e94bd11f71e507f7f87df81788dff88d1dacbfb...0499de31b99561a6d14a36a5f662c2a54f91beee)

---
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] <[email protected]>
2025-11-10 12:27:22 +00:00
alex 0fb54a948b chore(doc): add fedora setup guide and tweak existing guides 2025-11-06 23:51:57 -05:00
alex c4475ad42d fix(boot): ensure top-level error is printed 2025-11-06 13:04:05 -05:00
alex 5e4e86857c sprout: version 0.0.24 2025-11-06 12:03:46 -05:00
alex d3cad25749 chore(deps): upgrade uefi dependencies 2025-11-06 12:03:19 -05:00
alex 22780e6102 chore(eficore): decouple the shim support from the image load callsites 2025-11-06 11:52:00 -05:00
alex c52d61b07f fix(eficore): handle possible leak when the first install protocol interface fails 2025-11-04 14:32:32 -05:00
alex 0ce6ffa3da sprout: version 0.0.23 2025-11-04 12:16:53 -05:00
alex a1028c629d fix(eficore/env): improve quirk handling for dell systems 2025-11-03 23:58:48 -05:00
alex 503a9cba0a chore(code): move load options parsing to crates/eficore 2025-11-03 23:45:35 -05:00
alex 532fb38d5a chore(code): move crates/sprout to crates/boot and name it edera-sprout-boot 2025-11-03 22:52:54 -05:00
alex 9a803ad355 chore(code): sbat section generator build tool 2025-11-03 22:37:06 -05:00
alex 632781abbf chore(code): split much of the efi support code to crates/eficore 2025-11-03 20:47:21 -05:00
alex 48497700d8 fix(sprout): make secure boot warning more specific 2025-11-03 15:31:44 -05:00
alex 34ac57d291 sprout: version 0.0.22 2025-11-03 14:47:59 -05:00
alex 37abe49347 feat(sprout): implement custom logger which shortens output 2025-11-03 14:45:48 -05:00
alex 79615f7436 chore(docs): add openSUSE Secure Boot guide 2025-11-03 04:49:55 -05:00
alex 7a7d92ef70 chore(hack): add keyboard and mouse to dev qemu 2025-11-03 03:41:03 -05:00
alex b34c171ccb fix(hack): remove xen images during clean 2025-11-03 03:18:23 -05:00
alex 384c1e7eaf chore(docker): swap rust for docker builds to 1.91.0 2025-11-03 03:08:09 -05:00
alex 0b7b5066e4 chore(workflows): align on push/pull_request events across workflows 2025-11-03 03:01:57 -05:00
alex ba634ed68a fix(platform/timer): on x86_64, elide usage of asm!() and use _rdtsc() intrinsic 2025-11-03 02:57:22 -05:00
alex be63c5171b chore(doc): add clarifying comments in vercmp 2025-11-03 02:46:41 -05:00
alex f740c35568 fix(tpm): add clarifying parentheses to version check 2025-11-03 02:37:52 -05:00
alex 8a0b70a99b chore(doc): add documentation to VariableController::remove 2025-11-03 02:35:57 -05:00
alex 223a00563e chore(menu): add note as to why we match on the timer event 2025-11-03 02:35:01 -05:00
alex 029e59b209 sprout: version 0.0.21 2025-11-03 02:15:23 -05:00
alex bde1cd01c8 Merge pull request #28 from edera-dev/experiment/no-std
feat(sprout): introduce no_std sprout which uses stable rust
2025-11-02 23:11:52 -08:00
alex 0017d7874d feat(sprout): introduce no_std sprout which uses stable rust 2025-11-03 02:04:21 -05:00
alex 1c2acdc568 chore(build): pin rust-toolchain to nightly-2025-11-03 2025-11-03 00:37:04 -05:00
alex 1f322ff4bf chore(workflows): publish should upload and attest all artifacts in a single zip 2025-11-03 00:32:54 -05:00
alex 0bb7d7ccb1 sprout: version 0.0.20 2025-11-03 00:10:20 -05:00
alex 74b6a8deb3 chore(workflows): release workflow should attest all artifacts together 2025-11-03 00:06:53 -05:00
alex 3e5d54913c sprout: version 0.0.19 2025-11-02 23:59:36 -05:00
alex b616e75e96 chore(workflows): release workflow should attest the efi artifacts 2025-11-02 23:57:58 -05:00
alex 069f858e95 chore(workflows): publish workload should provide build provenance 2025-11-02 23:52:15 -05:00
alex ada13b7dd5 sprout: version 0.0.18 2025-11-02 23:40:57 -05:00
alex 8179fdb565 fix(hack): remove splash copy options 2025-11-02 23:37:46 -05:00
alex ed3bfb77c4 chore(crates): introduce new config crate for sprout configuration 2025-11-02 23:28:31 -05:00
alex ccc75a2e14 chore(workspace): move most things into the workspace 2025-11-02 22:35:07 -05:00
alex 9c12e5f12f chore(code): move sprout code to crates/sprout and remove splash support for minimalism 2025-11-02 22:23:00 -05:00
alex b103fdacf2 chore(docs): add debian setup guide 2025-11-02 19:52:25 -05:00
alex 7be42ba074 chore(docs): reorganize setup guides 2025-11-02 19:26:02 -05:00
alex 8a6f4dc19d chore(docs): add ubuntu secure boot setup guide 2025-11-02 18:08:57 -05:00
alex 830eaca19a fix(autoconfigure/linux): workaround canonical stubble bug relating to empty load options 2025-11-02 17:58:06 -05:00
alex 3febca5797 fix(chainload): ensure that load options are always set, even if it is to an empty string 2025-11-02 17:47:36 -05:00
alex 524d0871f3 sprout: version 0.0.17 2025-11-02 16:28:07 -05:00
alex f0628f77e2 fix(shim): repair x86_64 shim verification by using the SYSV calling convention 2025-11-02 05:57:24 -05:00
alex cc37c2b26a fix(shim): use pinned boxed slices to represent data that needs to be passed to uefi stack 2025-11-02 05:08:03 -05:00
alex 8d403d74c9 fix(shim/hook): clarify const-ness of some parameters 2025-11-02 04:16:05 -05:00
alex cc4bc6efcc fix(shim/hook): when using older hook protocol, read the data into an owned buffer 2025-11-02 02:48:41 -05:00
alex d4bcfcd9b1 fix(shim): reflect the const pointer-ness of the verifiable data 2025-11-02 02:45:25 -05:00
alex c34462b812 fix(options): accidental infinite loop due when not running on Dell firmware 2025-11-02 02:21:29 -05:00
alex 79471f6862 fix(quirk): skip initial options that start with a tilde to work around dell firmware 2025-11-02 02:13:39 -05:00
alex 9c31dba6fa fix(shim): only call into shim if it is available AND secure boot is enabled 2025-11-02 01:52:21 -05:00
alex 84d60e09be fix(bootloader-interface): when there are no entries, don't attempt to set LoaderEntries 2025-11-02 01:38:07 -05:00
alex eabb612330 fix(shim/hook): call original hook function if the shim verify fails 2025-11-02 01:07:16 -05:00
alex 1a6ed0af99 fix(shim): avoid masking the underlying error when shim verify fails 2025-11-02 00:27:45 -04:00
alex 3b4a66879f fix(autoconfigure/linux): sort kernels by version, newer kernels first 2025-11-01 22:06:48 -04:00
alex 01b3706914 fix(bls): quirk for when the version field is present already in the title 2025-11-01 21:40:39 -04:00
alex 5033bc7bf4 fix(menu): only show the title, not the name of the entry 2025-11-01 21:36:17 -04:00
alex f6441b5694 feat(bls): add version comparison to ensure entries are always sorted properly 2025-11-01 21:34:55 -04:00
alex 03d0e40141 fix(bls): add support for appending version to the title 2025-11-01 21:03:02 -04:00
alex 3a54970386 feat(bls): initial support for sorting of entries, not using version comparison 2025-11-01 21:00:54 -04:00
alex 8a5dc33b5a fix(options): add missing --autoconfigure flag 2025-11-01 20:07:32 -04:00
alex 757d10ec65 fix(timer): add check for zero frequency 2025-11-01 19:18:04 -04:00
alex 3b32f6c3ce fix(platform/timer/x86_64): if frequency is zero, panic 2025-11-01 19:08:47 -04:00
alex 6b1d220490 fix(bls): skip over any files named ".conf" to avoid empty names 2025-11-01 18:59:41 -04:00
alex f7558fd024 fix(menu): prevent masking of wait for event errors if closing the timer event fails 2025-11-01 18:58:24 -04:00
alex d1fd13163f fix(filesystem-device-match): make behavior of ignoring filesystem errors more explicit 2025-11-01 18:51:07 -04:00
alex a998832f6b fix(utils): improve safety of media loader and utf-16 handling 2025-11-01 18:49:10 -04:00
alex 992520c201 chore(main): collapse duplicate code for menu hidden or disabled 2025-11-01 18:38:08 -04:00
alex e9cba9da33 fix(menu): handle event freeing if wait for event fails 2025-11-01 18:37:07 -04:00
alex 0f8f12c70f fix(bootloader-interface): fix menu time marking 2025-11-01 18:35:07 -04:00
alex 1c732a1c43 feat(bootloader-interface): add support for LoaderEntryDefault and LoaderEntryOneShot 2025-11-01 18:04:06 -04:00
alex 08b9e2570e fix(bootloader-interface): disable setting of LoaderEntryDefault since this is intended to be user set 2025-11-01 17:50:54 -04:00
alex f361570b0e feat(bootloader-interface): add support for LoaderConfigTimeout and LoaderConfigTimeoutOneShot 2025-11-01 17:47:41 -04:00
alex 679b0c0290 feat(bootloader-interface): signal support for XBOOTLDR 2025-11-01 17:09:49 -04:00
alex f9dd56c8e7 feat(bootloader-interface): add support for LoaderFeatures 2025-11-01 03:24:14 -04:00
126 changed files with 4937 additions and 2550 deletions

No files matched your search

+9 -7
View File
@@ -1,10 +1,12 @@
name: zizmor
on:
push:
branches: ["main"]
pull_request:
branches: ["**"]
branches:
- main
push:
branches:
- main
permissions:
contents: read # Needed to checkout the repository.
@@ -23,17 +25,17 @@ jobs:
actions: read # Needed to analyze action metadata.
steps:
- name: harden runner
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
uses: step-security/harden-runner@e14015d583714f6e62063499dc959a02595150a1 # v2.21.1
with:
egress-policy: audit
- name: checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: setup uv
uses: astral-sh/setup-uv@2ddd2b9cb38ad8efd50337e8ab201519a34c9f24 # v7.1.1
uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
- name: zizmor
run: uvx zizmor --pedantic --format sarif . > results.sarif
@@ -41,7 +43,7 @@ jobs:
GH_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
- name: upload
uses: github/codeql-action/upload-sarif@4e94bd11f71e507f7f87df81788dff88d1dacbfb # v4.31.0
uses: github/codeql-action/upload-sarif@1c5b675653bb5c22dbe9b12b556ec555138e09fd # v4.38.1
with:
sarif_file: results.sarif
category: zizmor
+6 -6
View File
@@ -21,12 +21,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: harden runner
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
uses: step-security/harden-runner@e14015d583714f6e62063499dc959a02595150a1 # v2.21.1
with:
egress-policy: audit
- name: checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
@@ -51,12 +51,12 @@ jobs:
name: 'build ${{ matrix.arch }}'
steps:
- name: harden runner
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
uses: step-security/harden-runner@e14015d583714f6e62063499dc959a02595150a1 # v2.21.1
with:
egress-policy: audit
- name: checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
@@ -80,12 +80,12 @@ jobs:
name: 'clippy ${{ matrix.arch }}'
steps:
- name: harden runner
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
uses: step-security/harden-runner@e14015d583714f6e62063499dc959a02595150a1 # v2.21.1
with:
egress-policy: audit
- name: checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
+9 -7
View File
@@ -1,10 +1,12 @@
name: codeql
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
branches:
- main
push:
branches:
- main
schedule:
- cron: '33 16 * * 0'
@@ -35,23 +37,23 @@ jobs:
build-mode: none
steps:
- name: harden runner
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
uses: step-security/harden-runner@e14015d583714f6e62063499dc959a02595150a1 # v2.21.1
with:
egress-policy: audit
- name: checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: initialize codeql
uses: github/codeql-action/init@4e94bd11f71e507f7f87df81788dff88d1dacbfb # v4.31.0
uses: github/codeql-action/init@1c5b675653bb5c22dbe9b12b556ec555138e09fd # v4.38.1
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
config-file: ./.github/codeql/codeql-config.yaml
- name: perform codeql analysis
uses: github/codeql-action/analyze@4e94bd11f71e507f7f87df81788dff88d1dacbfb # v4.31.0
uses: github/codeql-action/analyze@1c5b675653bb5c22dbe9b12b556ec555138e09fd # v4.38.1
with:
category: "/language:${{matrix.language}}"
+17 -20
View File
@@ -1,19 +1,12 @@
name: publish
on:
push:
branches:
- main
pull_request:
branches:
- main
paths:
- bin/**
- src/**
- Cargo.*
- rust-toolchain.toml
- .github/workflows/publish.yaml
push:
branches:
- main
permissions:
contents: read # Needed to checkout the repository.
@@ -27,15 +20,17 @@ jobs:
name: artifacts
permissions:
contents: write # Needed to upload artifacts.
id-token: write # Needed for attestation.
attestations: write # Needed for attestations.
runs-on: ubuntu-latest
steps:
- name: harden runner
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
uses: step-security/harden-runner@e14015d583714f6e62063499dc959a02595150a1 # v2.21.1
with:
egress-policy: audit
- name: checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
@@ -46,14 +41,16 @@ jobs:
- name: 'assemble artifacts'
run: ./hack/assemble.sh
- name: 'upload sprout-x86_64.efi artifact'
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
- name: 'upload artifacts'
id: upload
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: sprout-x86_64.efi
path: target/assemble/sprout-x86_64.efi
name: artifacts
path: target/assemble/*
- name: 'upload sprout-aarch64.efi artifact'
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
- name: 'attest artifacts'
uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2
with:
name: sprout-aarch64.efi
path: target/assemble/sprout-aarch64.efi
subject-name: artifacts.zip
subject-digest: "sha256:${{ steps.upload.outputs.artifact-digest }}"
if: github.event_name != 'pull_request'
+14 -4
View File
@@ -20,15 +20,17 @@ jobs:
name: release
permissions:
contents: write # Needed to upload release assets.
id-token: write # Needed for attestation.
attestations: write # Needed for attestations.
runs-on: ubuntu-latest
steps:
- name: harden runner
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
uses: step-security/harden-runner@e14015d583714f6e62063499dc959a02595150a1 # v2.21.1
with:
egress-policy: audit
- name: checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
@@ -36,15 +38,23 @@ jobs:
run: |
cargo version
- name: 'assemble artifacts'
- name: 'assemble release artifacts'
run: ./hack/assemble.sh
- name: 'attest release artifacts'
uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2
with:
subject-path: target/assemble/*
- name: 'generate cultivator token'
uses: actions/create-github-app-token@bf559f85448f9380bcfa2899dbdc01eb5b37be3a # v3.0.0-beta.2
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.0.0-beta.2
id: generate-token
with:
app-id: "${{ secrets.EDERA_CULTIVATION_APP_ID }}"
private-key: "${{ secrets.EDERA_CULTIVATION_APP_PRIVATE_KEY }}"
# Scope to what uploading assets and `gh release edit` need: write to
# release contents.
permission-contents: write
if: ${{ github.event.inputs.release-tag != '' }}
- name: 'upload release artifacts'
Generated
+126 -233
View File
@@ -2,34 +2,11 @@
# It is not intended for manual editing.
version = 4
[[package]]
name = "adler2"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
[[package]]
name = "anyhow"
version = "1.0.100"
version = "1.0.104"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
[[package]]
name = "async-trait"
version = "0.1.89"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "autocfg"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
[[package]]
name = "bit_field"
@@ -39,136 +16,111 @@ checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6"
[[package]]
name = "bitflags"
version = "2.10.0"
version = "2.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06"
[[package]]
name = "block-buffer"
version = "0.10.4"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa"
dependencies = [
"generic-array",
"hybrid-array",
]
[[package]]
name = "bytemuck"
version = "1.24.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4"
[[package]]
name = "byteorder-lite"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
[[package]]
name = "bytes"
version = "1.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
[[package]]
name = "cfg-if"
version = "1.0.4"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600"
[[package]]
name = "cpufeatures"
version = "0.2.17"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566"
dependencies = [
"libc",
]
[[package]]
name = "crc32fast"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
dependencies = [
"cfg-if",
]
[[package]]
name = "crypto-common"
version = "0.1.6"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453"
dependencies = [
"generic-array",
"typenum",
"hybrid-array",
]
[[package]]
name = "digest"
version = "0.10.7"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2"
dependencies = [
"block-buffer",
"crypto-common",
]
[[package]]
name = "edera-sprout"
version = "0.0.16"
name = "edera-sprout-bls"
version = "0.0.29"
dependencies = [
"anyhow",
"image",
]
[[package]]
name = "edera-sprout-boot"
version = "0.0.29"
dependencies = [
"anyhow",
"edera-sprout-bls",
"edera-sprout-build",
"edera-sprout-config",
"edera-sprout-eficore",
"edera-sprout-parsing",
"hex",
"jaarg",
"log",
"serde",
"sha256",
"sha2",
"toml",
"uefi",
"uefi-raw",
]
[[package]]
name = "equivalent"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
name = "edera-sprout-build"
version = "0.0.29"
[[package]]
name = "fdeflate"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c"
name = "edera-sprout-config"
version = "0.0.29"
dependencies = [
"simd-adler32",
"serde",
]
[[package]]
name = "flate2"
version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb"
name = "edera-sprout-eficore"
version = "0.0.29"
dependencies = [
"crc32fast",
"miniz_oxide",
"anyhow",
"bitflags",
"log",
"shlex",
"spin",
"uefi",
"uefi-raw",
]
[[package]]
name = "generic-array"
version = "0.14.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2"
name = "edera-sprout-parsing"
version = "0.0.29"
dependencies = [
"typenum",
"version_check",
"hex",
"sha2",
]
[[package]]
name = "hashbrown"
version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d"
[[package]]
name = "hex"
version = "0.4.3"
@@ -176,133 +128,90 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "image"
version = "0.25.8"
name = "hybrid-array"
version = "0.4.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "529feb3e6769d234375c4cf1ee2ce713682b8e76538cb13f9fc23e1400a591e7"
checksum = "27f864f10dfb56725ce5ce5472bc52252c8f93a4ab86327122cebf62c5f59a17"
dependencies = [
"bytemuck",
"byteorder-lite",
"moxcms",
"num-traits",
"png",
"typenum",
]
[[package]]
name = "indexmap"
version = "2.12.0"
name = "jaarg"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f"
dependencies = [
"equivalent",
"hashbrown",
]
checksum = "534d589df1ef528a238f4bc4b1db081a1280f3aedf2695fd8971e9853a7fa4f6"
[[package]]
name = "libc"
version = "0.2.177"
version = "0.2.189"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976"
checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
[[package]]
name = "lock_api"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
dependencies = [
"scopeguard",
]
[[package]]
name = "log"
version = "0.4.28"
version = "0.4.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432"
[[package]]
name = "miniz_oxide"
version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
dependencies = [
"adler2",
"simd-adler32",
]
[[package]]
name = "moxcms"
version = "0.7.6"
source = "git+https://github.com/edera-dev/sprout-patched-deps.git?rev=2c4fcc84b50d40c28f540d4271109ea0ca7e1268#2c4fcc84b50d40c28f540d4271109ea0ca7e1268"
dependencies = [
"num-traits",
"pxfm",
]
[[package]]
name = "num-traits"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
]
[[package]]
name = "png"
version = "0.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97baced388464909d42d89643fe4361939af9b7ce7a31ee32a168f832a70f2a0"
dependencies = [
"bitflags",
"crc32fast",
"fdeflate",
"flate2",
"miniz_oxide",
]
checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
[[package]]
name = "proc-macro2"
version = "1.0.103"
version = "1.0.107"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8"
checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
dependencies = [
"unicode-ident",
]
[[package]]
name = "ptr_meta"
version = "0.3.1"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b9a0cf95a1196af61d4f1cbdab967179516d9a4a4312af1f31948f8f6224a79"
checksum = "743da816b98c921cdbe8628ef7381b76f25ecf4da599fc80aca90eae7ef70cc0"
dependencies = [
"ptr_meta_derive",
]
[[package]]
name = "ptr_meta_derive"
version = "0.3.1"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7347867d0a7e1208d93b46767be83e2b8f978c3dad35f775ac8d8847551d6fe1"
checksum = "1c8d9ca532f185d5d4db7a7c9d51420b452168ea1c2b913953281bd6fe1fcbd0"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "pxfm"
version = "0.1.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3cbdf373972bf78df4d3b518d07003938e2c7d1fb5891e55f9cb6df57009d84"
dependencies = [
"num-traits",
]
[[package]]
name = "quote"
version = "1.0.41"
version = "1.0.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1"
checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
dependencies = [
"proc-macro2",
]
[[package]]
name = "serde"
version = "1.0.228"
name = "scopeguard"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "serde"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
dependencies = [
"serde_core",
"serde_derive",
@@ -310,18 +219,18 @@ dependencies = [
[[package]]
name = "serde_core"
version = "1.0.228"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.228"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
dependencies = [
"proc-macro2",
"quote",
@@ -330,18 +239,18 @@ dependencies = [
[[package]]
name = "serde_spanned"
version = "1.0.3"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e24345aa0fe688594e73770a5f6d1b216508b4f93484c0026d521acd30134392"
checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26"
dependencies = [
"serde_core",
]
[[package]]
name = "sha2"
version = "0.10.9"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4"
dependencies = [
"cfg-if",
"cpufeatures",
@@ -349,27 +258,25 @@ dependencies = [
]
[[package]]
name = "sha256"
version = "1.6.0"
name = "shlex"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f880fc8562bdeb709793f00eb42a2ad0e672c4f883bbe59122b926eca935c8f6"
checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
[[package]]
name = "spin"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0134f9043ed38b087ac4f7d4af44c79e2c9e5094421fe3164f435ce585953b10"
dependencies = [
"async-trait",
"bytes",
"hex",
"sha2",
"lock_api",
]
[[package]]
name = "simd-adler32"
version = "0.3.7"
source = "git+https://github.com/edera-dev/sprout-patched-deps.git?rev=2c4fcc84b50d40c28f540d4271109ea0ca7e1268#2c4fcc84b50d40c28f540d4271109ea0ca7e1268"
[[package]]
name = "syn"
version = "2.0.108"
version = "3.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da58917d35242480a05c2897064da0a80589a2a0476c9a3f2fdc83b53502e917"
checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee"
dependencies = [
"proc-macro2",
"quote",
@@ -378,48 +285,40 @@ dependencies = [
[[package]]
name = "toml"
version = "0.9.8"
version = "1.1.6+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0dc8b1fb61449e27716ec0e1bdf0f6b8f3e8f6b05391e8497b8b6d7804ea6d8"
checksum = "920602543f0911ab71da12c50d59701da54c196d1a2bf5cb4b75667f137a406a"
dependencies = [
"indexmap",
"serde_core",
"serde_spanned",
"toml_datetime",
"toml_parser",
"toml_writer",
"winnow",
]
[[package]]
name = "toml_datetime"
version = "0.7.3"
version = "1.1.1+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533"
checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7"
dependencies = [
"serde_core",
]
[[package]]
name = "toml_parser"
version = "1.0.4"
version = "1.1.3+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e"
checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56"
dependencies = [
"winnow",
]
[[package]]
name = "toml_writer"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df8b2b54733674ad286d16267dcfc7a71ed5c776e4ac7aa3c3e2561f7c637bf2"
[[package]]
name = "typenum"
version = "1.19.0"
version = "1.20.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
[[package]]
name = "ucs2"
@@ -432,9 +331,9 @@ dependencies = [
[[package]]
name = "uefi"
version = "0.36.0"
version = "0.41.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f123e69767fc287c44d70ee19af3b39d1bfb735dbaff5090e95b5b13cd656d16"
checksum = "368cef7d94514897e6dad4350945c7910a9f5bf9021191d1c58ee3ec09962697"
dependencies = [
"bitflags",
"cfg-if",
@@ -448,9 +347,9 @@ dependencies = [
[[package]]
name = "uefi-macros"
version = "0.19.0"
version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4687412b5ac74d245d5bfb1733ede50c31be19bf8a4b6a967a29b451bab49e67"
checksum = "643efa8b64be89780bc498b290fab04337c7c4d171cc82cce2e133a74616dcc4"
dependencies = [
"proc-macro2",
"quote",
@@ -459,9 +358,9 @@ dependencies = [
[[package]]
name = "uefi-raw"
version = "0.12.0"
version = "0.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8aff2f4f2b556a36a201d335a1e0a57754967a96857b1f47a52d5a23825cac84"
checksum = "3636327a6d88a38c0f2567fb695ec7ba27b3483d1877730486605aa0866da201"
dependencies = [
"bitflags",
"uguid",
@@ -475,18 +374,12 @@ checksum = "0c8352f8c05e47892e7eaf13b34abd76a7f4aeaf817b716e88789381927f199c"
[[package]]
name = "unicode-ident"
version = "1.0.20"
version = "1.0.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "462eeb75aeb73aea900253ce739c8e18a67423fadf006037cd3ff27e82748a06"
[[package]]
name = "version_check"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954"
[[package]]
name = "winnow"
version = "0.7.13"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf"
checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81"
+53 -42
View File
@@ -1,45 +1,68 @@
[package]
name = "edera-sprout"
description = "Modern UEFI bootloader"
[workspace]
members = [
"crates/boot",
"crates/build",
"crates/config",
"crates/eficore",
"crates/bls",
"crates/parsing",
]
resolver = "3"
[workspace.package]
license = "Apache-2.0"
version = "0.0.16"
version = "0.0.29"
homepage = "https://sprout.edera.dev"
repository = "https://github.com/edera-dev/sprout"
edition = "2024"
[dependencies]
anyhow = "1.0.100"
toml = "0.9.8"
log = "0.4.28"
[workspace.dependencies]
bitflags = "2.13.2"
log = "0.4.34"
spin = "0.12.3"
uefi-raw = "0.17.0"
[dependencies.image]
version = "0.25.8"
default-features = false
features = ["png"]
optional = true
[dependencies.serde]
version = "1.0.228"
features = ["derive"]
[dependencies.sha256]
version = "1.6.0"
[workspace.dependencies.anyhow]
version = "1.0.104"
default-features = false
[dependencies.uefi]
version = "0.36.0"
features = ["alloc", "logger"]
[workspace.dependencies.hex]
version = "0.4.3"
default-features = false
features = ["alloc"]
[dependencies.uefi-raw]
version = "0.12.0"
[workspace.dependencies.jaarg]
version = "0.2.2"
default-features = false
features = ["alloc"]
[features]
default = ["splash"]
splash = ["dep:image"]
[workspace.dependencies.serde]
version = "1.0.229"
default-features = false
features = ["alloc", "derive"]
[profile.dev]
# We have to compile for opt-level = 2 due to optimization passes
[workspace.dependencies.sha2]
version = "0.11.0"
default-features = false
[workspace.dependencies.shlex]
version = "2.0.1"
default-features = false
[workspace.dependencies.toml]
version = "1.1.6"
default-features = false
features = ["serde", "parse"]
[workspace.dependencies.uefi]
version = "0.41.0"
default-features = false
features = ["alloc", "global_allocator", "panic_handler"]
# Common build profiles
# NOTE: We have to compile everything for opt-level = 2 due to optimization passes
# which don't handle the UEFI target properly.
[profile.dev]
opt-level = 2
[profile.release]
@@ -56,15 +79,3 @@ inherits = "dev"
strip = "debuginfo"
debug = 0
opt-level = 2
[patch.crates-io.simd-adler32]
git = "https://github.com/edera-dev/sprout-patched-deps.git"
rev = "2c4fcc84b50d40c28f540d4271109ea0ca7e1268"
[patch.crates-io.moxcms]
git = "https://github.com/edera-dev/sprout-patched-deps.git"
rev = "2c4fcc84b50d40c28f540d4271109ea0ca7e1268"
[[bin]]
name = "sprout"
path = "src/main.rs"
+11 -4
View File
@@ -5,16 +5,23 @@ This guide is a work in progress.
## Development Setup
You can use any Rust development environment to develop Sprout.
Rustup is recommended as the Rust toolchain manager to manage Rust versions and targets.
Sprout currently requires Rust nightly to support uefi_std. See [uefi_std](https://doc.rust-lang.org/beta/rustc/platform-support/unknown-uefi.html) for more details.
We currently only support `x86_64-unknown-uefi` and `aarch64-unknown-uefi` targets.
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.
## 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
You can use the `./hack` scripts to run common development tasks:
+1 -2
View File
@@ -1,8 +1,7 @@
# syntax=docker/dockerfile:1.7-labs
ARG RUST_PROFILE=release
ARG RUST_TARGET_SUBDIR=release
FROM --platform=$BUILDPLATFORM rustlang/rust:nightly-alpine@sha256:34532121803db17008af0cdc4e2e1210466cb257cc9d3840dac42d706640fee5 AS build
FROM --platform=$BUILDPLATFORM rust:1.98.1-alpine@sha256:7cc1c22d77d9432f7fe012a70e6d3e555af54c2a6832700ed7d553f1769ae89f AS build
RUN apk --no-cache add musl-dev busybox-static
ARG RUST_PROFILE
RUN adduser -S -s /bin/sh build
+24 -13
View File
@@ -18,6 +18,9 @@ existing UEFI bootloader or booted by the hardware directly.
Sprout is licensed under Apache 2.0 and is open to modifications and contributions.
**NOTE**: Sprout is still in beta. Some features may not work as expected.
Please [report any bugs you find](https://github.com/edera-dev/sprout/issues/new/choose).
## Background
At [Edera] we make compute isolation technology for a wide variety of environments, often ones we do not fully control.
@@ -37,10 +40,24 @@ simplify installation and usage.
## Documentation
- [Fedora Setup Guide]
- [Generic Linux Setup Guide]
- [Alpine Edge Setup Guide]
- [Windows Setup Guide]
### Setup Guides
Some guides support Secure Boot and some do not.
We recommend running Sprout without Secure Boot for development, and with Secure Boot for production.
| Operating System | Secure Boot Enabled | Link |
|------------------|---------------------|-------------------------------------------------------|
| Fedora | ✅ | [Setup Guide](./docs/setup/signed/fedora.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) |
| Alpine Edge | ❌ | [Setup Guide](./docs/setup/unsigned/alpine-edge.md) |
| Generic Linux | ❌ | [Setup Guide](./docs/setup/unsigned/generic-linux.md) |
| Windows | ❌ | [Setup Guide](./docs/setup/unsigned/windows.md) |
### Project Documentation
- [Development Guide]
- [Contributing Guide]
- [Sprout License]
@@ -49,8 +66,6 @@ simplify installation and usage.
## Features
**NOTE**: Sprout is still in beta.
### Current
- [x] Loadable driver support
@@ -61,12 +76,12 @@ simplify installation and usage.
- [x] Load Linux initrd from disk
- [x] Basic boot menu
- [x] BLS autoconfiguration support
- [x] [Secure Boot support](https://github.com/edera-dev/sprout/issues/20): partial
- [x] [Secure Boot support](https://github.com/edera-dev/sprout/issues/20): beta
- [x] [Bootloader interface support](https://github.com/edera-dev/sprout/issues/21): beta
- [x] [BLS specification conformance](https://github.com/edera-dev/sprout/issues/2): beta
### Roadmap
- [ ] [Bootloader interface support](https://github.com/edera-dev/sprout/issues/21)
- [ ] [BLS specification conformance](https://github.com/edera-dev/sprout/issues/2)
- [ ] [Full-featured boot menu](https://github.com/edera-dev/sprout/issues/1)
- [ ] [UKI support](https://github.com/edera-dev/sprout/issues/6): partial
- [ ] [multiboot2 support](https://github.com/edera-dev/sprout/issues/7)
@@ -147,10 +162,6 @@ autoconfigure = true
```
[Edera]: https://edera.dev
[Fedora Setup Guide]: ./docs/fedora-setup.md
[Generic Linux Setup Guide]: ./docs/generic-linux-setup.md
[Alpine Edge Setup Guide]: ./docs/alpine-edge-setup.md
[Windows Setup Guide]: ./docs/windows-setup.md
[Development Guide]: ./DEVELOPMENT.md
[Contributing Guide]: ./CONTRIBUTING.md
[Sprout License]: ./LICENSE
-57
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();
}
+18
View File
@@ -0,0 +1,18 @@
# This crate explicitly does not have uefi/uefi-raw dependencies,
# so that the contents can be unit-testable on non-UEFI target hosts.
# Do not add uefi or uefi-raw as dependencies.
[package]
name = "edera-sprout-bls"
description = "Sprout BLS Utilities (UEFI-free)"
license.workspace = true
version.workspace = true
homepage.workspace = true
repository.workspace = true
edition.workspace = true
[dependencies]
anyhow.workspace = true
[lib]
name = "edera_sprout_bls"
path = "src/lib.rs"
+800
View File
@@ -0,0 +1,800 @@
#![no_std]
extern crate alloc;
use alloc::string::{String, ToString};
use anyhow::{Error, Result};
use core::{cmp::Ordering, iter::Peekable, str::FromStr};
/// Represents a parsed BLS entry.
/// Fields unrelated to Sprout are not included.
#[derive(Default, Debug, Clone)]
pub struct BlsEntry {
/// The title of the entry.
pub title: Option<String>,
/// The options to pass to the entry.
pub options: Option<String>,
/// The path to the linux kernel.
pub linux: Option<String>,
/// The path to the initrd.
pub initrd: Option<String>,
/// The path to an EFI image.
pub efi: Option<String>,
/// The sort key for the entry.
pub sort_key: Option<String>,
/// The version of the entry.
pub version: Option<String>,
/// The machine id of the entry.
pub machine_id: Option<String>,
}
/// Parser for a BLS entry.
impl FromStr for BlsEntry {
type Err = Error;
/// Parses the `input` as a BLS entry file.
fn from_str(input: &str) -> Result<Self> {
// All the fields in a BLS entry we understand.
// Set all to None initially.
let mut title: Option<String> = None;
let mut options: Option<String> = None;
let mut linux: Option<String> = None;
let mut initrd: Option<String> = None;
let mut efi: Option<String> = None;
let mut sort_key: Option<String> = None;
let mut version: Option<String> = None;
let mut machine_id: Option<String> = None;
// Iterate over each line in the input and parse it.
for line in input.lines() {
let line = line.trim();
// Skip over empty lines and comments.
if line.is_empty() || line.starts_with('#') {
continue;
}
// Split the line once by whitespace. This technically includes newlines but since
// the lines iterator is used, there should never be a newline here.
let Some((key, value)) = line.split_once(char::is_whitespace) else {
continue;
};
// Match the key to a field we understand.
match key {
// The title of the entry.
"title" => {
title = Some(value.trim().to_string());
}
// The options to pass to the entry.
"options" => {
options = Some(value.trim().to_string());
}
// The path to the linux kernel.
"linux" => {
linux = Some(value.trim().to_string());
}
// The path to the initrd.
"initrd" => {
initrd = Some(value.trim().to_string());
}
// The path to an EFI image.
"efi" => {
efi = Some(value.trim().to_string());
}
"sort-key" => {
sort_key = Some(value.trim().to_string());
}
"version" => {
version = Some(value.trim().to_string());
}
"machine-id" => {
machine_id = Some(value.trim().to_string());
}
// Ignore any other key.
_ => {
continue;
}
}
}
// Produce a BLS entry from the parsed fields.
Ok(Self {
title,
options,
linux,
initrd,
efi,
sort_key,
version,
machine_id,
})
}
}
impl BlsEntry {
/// Checks if this BLS entry is something we can actually boot in Sprout.
pub fn is_valid(&self) -> bool {
self.linux.is_some() || self.efi.is_some()
}
/// Fetches the path to an EFI bootable image to boot, if any.
/// This prioritizes the linux field over efi.
/// It also converts / to \\ to match EFI path style.
pub fn chainload_path(&self) -> Option<String> {
self.linux
.clone()
.or(self.efi.clone())
.map(|path| path.replace('/', "\\").trim_start_matches('\\').to_string())
}
/// Fetches the path to an initrd to pass to the kernel, if any.
/// It also converts / to \\ to match EFI path style.
pub fn initrd_path(&self) -> Option<String> {
self.initrd
.clone()
.map(|path| path.replace('/', "\\").trim_start_matches('\\').to_string())
}
/// Fetches the options to pass to the kernel, if any.
pub fn options(&self) -> Option<String> {
self.options.clone()
}
/// Fetches the title of the entry, if any.
pub fn title(&self) -> Option<String> {
self.title.clone()
}
/// Fetches the sort key of the entry, if any.
pub fn sort_key(&self) -> Option<String> {
self.sort_key.clone()
}
/// Fetches the version of the entry, if any.
pub fn version(&self) -> Option<String> {
self.version.clone()
}
/// Fetches the machine id of the entry, if any.
pub fn machine_id(&self) -> Option<String> {
self.machine_id.clone()
}
}
/// Sorts two BLS entries according to the BLS sort system.
/// `a_name` and `b_name` are the entry filenames (without `.conf`) used as the
/// final tiebreaker when all other fields are equal.
/// Reference: <https://uapi-group.org/specifications/specs/boot_loader_specification/#sorting>
pub fn sort_bls(a_bls: &BlsEntry, a_name: &str, b_bls: &BlsEntry, b_name: &str) -> Ordering {
// Grab the sort keys from both entries.
let a_sort_key = a_bls.sort_key();
let b_sort_key = b_bls.sort_key();
// Sort keys, machine-id and version are only considered when both entries have a sort key.
// If only one entry has a sort key, that entry sorts first.
let ordering = match (a_sort_key, b_sort_key) {
(Some(a_sort_key), Some(b_sort_key)) => a_sort_key
.cmp(&b_sort_key)
// If the sort keys are equal, sort by machine-id.
.then_with(|| a_bls.machine_id().cmp(&b_bls.machine_id()))
// If the machine-id values are equal, sort by version, sorting newer versions first.
.then_with(|| {
compare_versions_optional(a_bls.version().as_deref(), b_bls.version().as_deref())
.reverse()
}),
(Some(_), None) => Ordering::Less,
(None, Some(_)) => Ordering::Greater,
(None, None) => Ordering::Equal,
};
// If all else is equal, compare the file names of both entries, sorting newer entries first.
ordering.then_with(|| compare_versions(a_name, b_name).reverse())
}
/// Handles single character advancement and comparison.
macro_rules! handle_single_char {
($ca: expr, $cb:expr, $a_chars:expr, $b_chars:expr, $c:expr) => {
match ($ca == $c, $cb == $c) {
(true, false) => return Ordering::Less,
(false, true) => return Ordering::Greater,
(true, true) => {
$a_chars.next();
$b_chars.next();
continue;
}
_ => {}
}
};
}
/// Compares two strings using the BLS version comparison specification.
/// Handles optional values as well by comparing only if both are specified.
/// A missing value is considered older than any specified value.
pub fn compare_versions_optional(a: Option<&str>, b: Option<&str>) -> Ordering {
match (a, b) {
// If both have values, compare them.
(Some(a), Some(b)) => compare_versions(a, b),
// If the second value is None, then `a` is greater than `b`.
(Some(_a), None) => Ordering::Greater,
// If the first value is None, the `a` is less than `b`.
(None, Some(_b)) => Ordering::Less,
// If both values are None, return that they are equal.
(None, None) => Ordering::Equal,
}
}
/// Compares two strings using the BLS version comparison specification.
/// See: <https://uapi-group.org/specifications/specs/version_format_specification/>
pub fn compare_versions(a: &str, b: &str) -> Ordering {
// An empty string is older than any other string.
if a.is_empty() || b.is_empty() {
return a.len().cmp(&b.len());
}
// Acquire a peekable iterator for each string.
let mut a_chars = a.chars().peekable();
let mut b_chars = b.chars().peekable();
// Loop until we have reached the end of one of the strings.
loop {
// Skip invalid characters in both strings.
skip_invalid(&mut a_chars);
skip_invalid(&mut b_chars);
// Handle the ~ character. This must happen before the end of string check,
// as a string with ~ is older than a string that has ended (1~rc1 < 1).
match (a_chars.peek() == Some(&'~'), b_chars.peek() == Some(&'~')) {
(true, false) => return Ordering::Less,
(false, true) => return Ordering::Greater,
(true, true) => {
a_chars.next();
b_chars.next();
continue;
}
(false, false) => {}
}
// Check if either string has ended.
match (a_chars.peek(), b_chars.peek()) {
// No more characters in either string.
(None, None) => return Ordering::Equal,
// One string has ended, the other hasn't.
(None, Some(_)) => return Ordering::Less,
(Some(_), None) => return Ordering::Greater,
// Both strings have characters left.
(Some(&ca), Some(&cb)) => {
// Handle '-' character.
handle_single_char!(ca, cb, a_chars, b_chars, '-');
// Handle the '^' character.
handle_single_char!(ca, cb, a_chars, b_chars, '^');
// Handle the '.' character.
handle_single_char!(ca, cb, a_chars, b_chars, '.');
// Handle digits with numerical comparison.
// If only one string has digits at this position, that string is newer.
if ca.is_ascii_digit() || cb.is_ascii_digit() {
if !ca.is_ascii_digit() {
return Ordering::Less;
}
if !cb.is_ascii_digit() {
return Ordering::Greater;
}
let result = compare_numeric(&mut a_chars, &mut b_chars);
if result != Ordering::Equal {
return result;
}
continue;
}
// Handle letters with alphabetical comparison.
// We key off of the A character being alphabetical intentionally as we presume
// this indicates it will be the same at this position.
if ca.is_ascii_alphabetic() || cb.is_ascii_alphabetic() {
let result = compare_alphabetic(&mut a_chars, &mut b_chars);
if result != Ordering::Equal {
return result;
}
continue;
}
}
}
}
}
/// Skips characters that are not in the valid character set.
fn skip_invalid<I: Iterator<Item = char>>(iter: &mut Peekable<I>) {
while let Some(&c) = iter.peek() {
if is_valid_char(c) {
break;
}
iter.next();
}
}
/// Checks if a character is in the valid character set for comparison.
fn is_valid_char(c: char) -> bool {
matches!(c, 'a'..='z' | 'A'..='Z' | '0'..='9' | '-' | '.' | '~' | '^')
}
/// Compares numerical prefixes by extracting numbers.
/// Numbers are compared by their digit count first and then digit by digit,
/// which supports numbers of any length.
fn compare_numeric<I: Iterator<Item = char>>(
iter_a: &mut Peekable<I>,
iter_b: &mut Peekable<I>,
) -> Ordering {
let num_a = extract_number(iter_a);
let num_b = extract_number(iter_b);
num_a
.len()
.cmp(&num_b.len())
.then_with(|| num_a.cmp(&num_b))
}
/// Extracts the digits of a number from the iterator, skipping leading zeros.
fn extract_number<I: Iterator<Item = char>>(iter: &mut Peekable<I>) -> String {
// Skip leading zeros
while let Some(&'0') = iter.peek() {
iter.next();
}
let mut num = String::new();
while let Some(&c) = iter.peek() {
if c.is_ascii_digit() {
iter.next();
num.push(c);
} else {
break;
}
}
num
}
/// Compares alphabetical prefixes.
/// Capital letters compare lower than lowercase letters (B < a).
fn compare_alphabetic<I: Iterator<Item = char>>(
iter_a: &mut Peekable<I>,
iter_b: &mut Peekable<I>,
) -> Ordering {
loop {
return match (iter_a.peek(), iter_b.peek()) {
(Some(&ca), Some(&cb)) if ca.is_ascii_alphabetic() && cb.is_ascii_alphabetic() => {
if ca == cb {
// Same character, we should continue.
iter_a.next();
iter_b.next();
continue;
}
// Different characters found.
// All capital letters compare lower than lowercase letters.
match (ca.is_ascii_uppercase(), cb.is_ascii_uppercase()) {
(true, false) => Ordering::Less, // uppercase < lowercase
(false, true) => Ordering::Greater, // lowercase > uppercase
(true, true) => ca.cmp(&cb), // both are uppercase
(false, false) => ca.cmp(&cb), // both are lowercase
}
}
(Some(&ca), Some(_)) if ca.is_ascii_alphabetic() => {
// a has letters, b doesn't
Ordering::Greater
}
(Some(_), Some(&cb)) if cb.is_ascii_alphabetic() => {
// b has letters, a doesn't
Ordering::Less
}
(Some(&ca), None) if ca.is_ascii_alphabetic() => Ordering::Greater,
(None, Some(&cb)) if cb.is_ascii_alphabetic() => Ordering::Less,
_ => Ordering::Equal,
};
}
}
#[cfg(test)]
mod tests {
use super::*;
use core::cmp::Ordering;
fn sort_entry(
sort_key: Option<&str>,
machine_id: Option<&str>,
version: Option<&str>,
) -> BlsEntry {
BlsEntry {
sort_key: sort_key.map(|s| s.to_string()),
machine_id: machine_id.map(|s| s.to_string()),
version: version.map(|s| s.to_string()),
linux: Some("/vmlinuz".to_string()),
..BlsEntry::default()
}
}
#[test]
fn parse_empty_input_gives_all_none() {
let entry: BlsEntry = "".parse().unwrap();
assert!(entry.title.is_none());
assert!(entry.linux.is_none());
assert!(entry.efi.is_none());
assert!(entry.initrd.is_none());
assert!(entry.options.is_none());
assert!(entry.sort_key.is_none());
assert!(entry.version.is_none());
assert!(entry.machine_id.is_none());
}
#[test]
fn parse_all_known_fields() {
let input = "\
title Fedora Linux 6.5.6
version 6.5.6-300.fc39.x86_64
machine-id abc123def456
linux /boot/vmlinuz-6.5.6
initrd /boot/initrd-6.5.6.img
options root=/dev/sda1 ro quiet
sort-key fedora
efi /EFI/fedora/shimx64.efi
";
let entry: BlsEntry = input.parse().unwrap();
assert_eq!(entry.title.as_deref(), Some("Fedora Linux 6.5.6"));
assert_eq!(entry.version.as_deref(), Some("6.5.6-300.fc39.x86_64"));
assert_eq!(entry.machine_id.as_deref(), Some("abc123def456"));
assert_eq!(entry.linux.as_deref(), Some("/boot/vmlinuz-6.5.6"));
assert_eq!(entry.initrd.as_deref(), Some("/boot/initrd-6.5.6.img"));
assert_eq!(entry.options.as_deref(), Some("root=/dev/sda1 ro quiet"));
assert_eq!(entry.sort_key.as_deref(), Some("fedora"));
assert_eq!(entry.efi.as_deref(), Some("/EFI/fedora/shimx64.efi"));
}
#[test]
fn parse_skips_blank_lines() {
let input = "\n\ntitle My Entry\n\n\nlinux /vmlinuz\n\n";
let entry: BlsEntry = input.parse().unwrap();
assert_eq!(entry.title.as_deref(), Some("My Entry"));
assert_eq!(entry.linux.as_deref(), Some("/vmlinuz"));
}
#[test]
fn parse_skips_comment_lines() {
let input = "# this is a comment\ntitle My Entry\n# another comment\nlinux /vmlinuz\n";
let entry: BlsEntry = input.parse().unwrap();
assert_eq!(entry.title.as_deref(), Some("My Entry"));
assert_eq!(entry.linux.as_deref(), Some("/vmlinuz"));
}
#[test]
fn parse_trims_leading_whitespace_from_value() {
let input = "title Padded Title\nlinux /boot/vmlinuz\n";
let entry: BlsEntry = input.parse().unwrap();
assert_eq!(entry.title.as_deref(), Some("Padded Title"));
assert_eq!(entry.linux.as_deref(), Some("/boot/vmlinuz"));
}
#[test]
fn parse_ignores_unknown_keys() {
let input = "title My Entry\nunknown-key some-value\nfuture-field value\nlinux /vmlinuz\n";
let entry: BlsEntry = input.parse().unwrap();
assert_eq!(entry.title.as_deref(), Some("My Entry"));
assert_eq!(entry.linux.as_deref(), Some("/vmlinuz"));
}
#[test]
fn parse_skips_lines_without_whitespace_separator() {
// A line with no whitespace cannot be split into key+value, so it is skipped
let input = "title My Entry\nnovalueline\nlinux /vmlinuz\n";
let entry: BlsEntry = input.parse().unwrap();
assert_eq!(entry.title.as_deref(), Some("My Entry"));
assert_eq!(entry.linux.as_deref(), Some("/vmlinuz"));
}
#[test]
fn is_valid_when_linux_present() {
let entry: BlsEntry = "linux /vmlinuz\n".parse().unwrap();
assert!(entry.is_valid());
}
#[test]
fn is_valid_when_efi_present() {
let entry: BlsEntry = "efi /EFI/boot/bootx64.efi\n".parse().unwrap();
assert!(entry.is_valid());
}
#[test]
fn not_valid_without_linux_or_efi() {
let entry: BlsEntry = "title Just a Title\noptions quiet\n".parse().unwrap();
assert!(!entry.is_valid());
}
#[test]
fn chainload_path_normalises_forward_slashes_to_backslashes() {
let entry: BlsEntry = "linux /boot/vmlinuz\n".parse().unwrap();
assert_eq!(entry.chainload_path().as_deref(), Some("boot\\vmlinuz"));
}
#[test]
fn chainload_path_strips_leading_backslash() {
let entry: BlsEntry = "linux \\EFI\\boot\\kernel\n".parse().unwrap();
assert_eq!(entry.chainload_path().as_deref(), Some("EFI\\boot\\kernel"));
}
#[test]
fn chainload_path_prefers_linux_over_efi() {
let input = "linux /boot/vmlinuz\nefi /EFI/boot/bootx64.efi\n";
let entry: BlsEntry = input.parse().unwrap();
assert_eq!(entry.chainload_path().as_deref(), Some("boot\\vmlinuz"));
}
#[test]
fn chainload_path_falls_back_to_efi_when_no_linux() {
let entry: BlsEntry = "efi /EFI/Microsoft/Boot/bootmgfw.efi\n".parse().unwrap();
assert_eq!(
entry.chainload_path().as_deref(),
Some("EFI\\Microsoft\\Boot\\bootmgfw.efi")
);
}
#[test]
fn chainload_path_none_when_neither_linux_nor_efi() {
let entry: BlsEntry = "title Only Title\n".parse().unwrap();
assert!(entry.chainload_path().is_none());
}
#[test]
fn initrd_path_normalises_slashes() {
let entry: BlsEntry = "linux /vmlinuz\ninitrd /boot/initrd.img\n".parse().unwrap();
assert_eq!(entry.initrd_path().as_deref(), Some("boot\\initrd.img"));
}
#[test]
fn initrd_path_none_when_not_set() {
let entry: BlsEntry = "linux /vmlinuz\n".parse().unwrap();
assert!(entry.initrd_path().is_none());
}
#[test]
fn sort_key_is_primary_criterion() {
let a = sort_entry(Some("alpine"), None, None);
let b = sort_entry(Some("fedora"), None, None);
assert_eq!(sort_bls(&a, "a", &b, "b"), Ordering::Less);
assert_eq!(sort_bls(&b, "b", &a, "a"), Ordering::Greater);
}
#[test]
fn machine_id_is_secondary_criterion() {
let a = sort_entry(Some("linux"), Some("aaa"), None);
let b = sort_entry(Some("linux"), Some("bbb"), None);
assert_eq!(sort_bls(&a, "a", &b, "b"), Ordering::Less);
assert_eq!(sort_bls(&b, "b", &a, "a"), Ordering::Greater);
}
#[test]
fn version_is_tertiary_criterion_newer_first() {
let a = sort_entry(Some("linux"), Some("abc"), Some("6.5.0"));
let b = sort_entry(Some("linux"), Some("abc"), Some("6.4.0"));
// newer version (a) sorts before older version (b), so a < b in sort order
assert_eq!(sort_bls(&a, "a", &b, "b"), Ordering::Less);
assert_eq!(sort_bls(&b, "b", &a, "a"), Ordering::Greater);
}
#[test]
fn name_is_final_tiebreaker_newer_first() {
let a = sort_entry(Some("linux"), Some("abc"), Some("6.5.0"));
let b = sort_entry(Some("linux"), Some("abc"), Some("6.5.0"));
// name comparison via compare_versions, reversed — higher name sorts first
assert_eq!(sort_bls(&a, "entry-2", &b, "entry-1"), Ordering::Less);
assert_eq!(sort_bls(&a, "entry-1", &b, "entry-2"), Ordering::Greater);
}
#[test]
fn identical_entries_are_equal() {
let a = sort_entry(Some("linux"), Some("abc"), Some("6.5.0"));
let b = sort_entry(Some("linux"), Some("abc"), Some("6.5.0"));
assert_eq!(sort_bls(&a, "entry-1", &b, "entry-1"), Ordering::Equal);
}
#[test]
fn equal_strings_are_equal() {
assert_eq!(compare_versions("1.0.0", "1.0.0"), Ordering::Equal);
}
#[test]
fn empty_strings_are_equal() {
assert_eq!(compare_versions("", ""), Ordering::Equal);
}
#[test]
fn simple_numeric_less() {
assert_eq!(compare_versions("1", "2"), Ordering::Less);
}
#[test]
fn simple_numeric_greater() {
assert_eq!(compare_versions("2", "1"), Ordering::Greater);
}
#[test]
fn numeric_is_not_lexicographic() {
// "10" > "9", not "10" < "9" as in lexicographic order
assert_eq!(compare_versions("10", "9"), Ordering::Greater);
assert_eq!(compare_versions("1.10", "1.9"), Ordering::Greater);
}
#[test]
fn leading_zeros_are_ignored() {
assert_eq!(compare_versions("01", "1"), Ordering::Equal);
assert_eq!(compare_versions("1.00", "1.0"), Ordering::Equal);
assert_eq!(compare_versions("007", "7"), Ordering::Equal);
}
#[test]
fn multi_component_comparison() {
assert_eq!(compare_versions("1.0.0", "1.0.1"), Ordering::Less);
assert_eq!(compare_versions("1.0.1", "1.0.0"), Ordering::Greater);
assert_eq!(compare_versions("2.0.0", "1.9.9"), Ordering::Greater);
}
#[test]
fn more_components_is_greater() {
assert_eq!(compare_versions("1.0.0", "1.0"), Ordering::Greater);
assert_eq!(compare_versions("1.0", "1.0.0"), Ordering::Less);
assert_eq!(compare_versions("1.0.0.0", "1.0.0"), Ordering::Greater);
}
#[test]
fn alphabetic_sections_compare_lexicographically() {
assert_eq!(compare_versions("1.0a", "1.0b"), Ordering::Less);
assert_eq!(compare_versions("1.0b", "1.0a"), Ordering::Greater);
assert_eq!(compare_versions("1.0abc", "1.0abd"), Ordering::Less);
}
#[test]
fn uppercase_letters_sort_below_lowercase() {
// Capital letters compare lower than lowercase (B < a)
assert_eq!(compare_versions("B", "a"), Ordering::Less);
assert_eq!(compare_versions("a", "B"), Ordering::Greater);
assert_eq!(compare_versions("Z", "a"), Ordering::Less);
}
#[test]
fn uppercase_letters_compare_among_themselves() {
assert_eq!(compare_versions("A", "B"), Ordering::Less);
assert_eq!(compare_versions("B", "A"), Ordering::Greater);
assert_eq!(compare_versions("A", "A"), Ordering::Equal);
}
#[test]
fn invalid_characters_are_skipped() {
// Characters not in [a-zA-Z0-9.-~^] are skipped before comparison
assert_eq!(compare_versions("##1.0", "1.0"), Ordering::Equal);
assert_eq!(compare_versions("1.0##", "1.0"), Ordering::Equal);
}
#[test]
fn tilde_between_two_present_tilde_strings() {
// When both have ~ at the same position, they are consumed and comparison continues
assert_eq!(compare_versions("1~alpha", "1~beta"), Ordering::Less);
assert_eq!(compare_versions("1~rc1", "1~rc2"), Ordering::Less);
assert_eq!(compare_versions("1~rc1", "1~rc1"), Ordering::Equal);
}
#[test]
fn tilde_when_only_one_side_has_it() {
// When a has ~ but b doesn't at the same position, a < b
assert_eq!(compare_versions("1~rc1", "1.0"), Ordering::Less);
// When b has ~ but a doesn't, a > b
assert_eq!(compare_versions("1.0", "1~rc1"), Ordering::Greater);
}
#[test]
fn tilde_is_older_than_end_of_string() {
// A pre-release is older than the release it precedes.
assert_eq!(compare_versions("1~rc1", "1"), Ordering::Less);
assert_eq!(compare_versions("1", "1~rc1"), Ordering::Greater);
assert_eq!(compare_versions("6.1~rc3", "6.1"), Ordering::Less);
}
#[test]
fn empty_string_is_oldest() {
assert_eq!(compare_versions("", "~1"), Ordering::Less);
assert_eq!(compare_versions("~1", ""), Ordering::Greater);
assert_eq!(compare_versions("", "1"), Ordering::Less);
}
#[test]
fn digits_are_newer_than_letters() {
assert_eq!(compare_versions("0", "a"), Ordering::Greater);
assert_eq!(compare_versions("a", "0"), Ordering::Less);
assert_eq!(compare_versions("1.0", "1.a"), Ordering::Greater);
assert_eq!(compare_versions("1.a", "1.0"), Ordering::Less);
}
#[test]
fn large_numbers_do_not_saturate() {
assert_eq!(
compare_versions("99999999999999999999", "99999999999999999998"),
Ordering::Greater
);
assert_eq!(
compare_versions("1.99999999999999999998", "1.99999999999999999999"),
Ordering::Less
);
}
#[test]
fn entry_with_sort_key_sorts_before_entry_without() {
let with_key = sort_entry(Some("fedora"), None, None);
let without_key = sort_entry(None, None, None);
assert_eq!(sort_bls(&with_key, "a", &without_key, "b"), Ordering::Less);
assert_eq!(
sort_bls(&without_key, "b", &with_key, "a"),
Ordering::Greater
);
}
#[test]
fn entries_without_sort_key_sort_by_name_only() {
// Without a sort key, machine-id and version are ignored.
let a = sort_entry(None, Some("aaa"), Some("1.0"));
let b = sort_entry(None, Some("bbb"), Some("2.0"));
assert_eq!(sort_bls(&a, "entry-2", &b, "entry-1"), Ordering::Less);
assert_eq!(sort_bls(&b, "entry-1", &a, "entry-2"), Ordering::Greater);
}
#[test]
fn entry_with_version_sorts_before_entry_without() {
let a = sort_entry(Some("linux"), Some("abc"), Some("6.5.0"));
let b = sort_entry(Some("linux"), Some("abc"), None);
assert_eq!(sort_bls(&a, "a", &b, "b"), Ordering::Less);
assert_eq!(sort_bls(&b, "b", &a, "a"), Ordering::Greater);
}
#[test]
fn optional_both_none_equal() {
assert_eq!(compare_versions_optional(None, None), Ordering::Equal);
}
#[test]
fn optional_some_vs_none_is_greater() {
// A missing version is older than any specified version.
assert_eq!(
compare_versions_optional(Some("1.0"), None),
Ordering::Greater
);
}
#[test]
fn optional_none_vs_some_is_less() {
assert_eq!(compare_versions_optional(None, Some("1.0")), Ordering::Less);
}
#[test]
fn optional_both_some_delegates_to_compare_versions() {
assert_eq!(
compare_versions_optional(Some("1.0"), Some("2.0")),
Ordering::Less
);
assert_eq!(
compare_versions_optional(Some("2.0"), Some("1.0")),
Ordering::Greater
);
assert_eq!(
compare_versions_optional(Some("1.0"), Some("1.0")),
Ordering::Equal
);
}
}
+30
View File
@@ -0,0 +1,30 @@
[package]
name = "edera-sprout-boot"
description = "Sprout: Modern UEFI Bootloader"
license.workspace = true
version.workspace = true
homepage.workspace = true
repository.workspace = true
edition.workspace = true
[dependencies]
anyhow.workspace = true
edera-sprout-config.path = "../config"
edera-sprout-eficore.path = "../eficore"
edera-sprout-bls.path = "../bls"
edera-sprout-parsing.path = "../parsing"
hex.workspace = true
jaarg.workspace = true
sha2.workspace = true
toml.workspace = true
log.workspace = true
uefi.workspace = true
uefi-raw.workspace = true
[build-dependencies]
edera-sprout-build.path = "../build"
[[bin]]
name = "sprout"
path = "src/main.rs"
test = false
+3
View File
@@ -0,0 +1,3 @@
# Sprout Bootloader
The main bootable crate of the Sprout bootloader.
+7
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();
}
+1 -38
View File
@@ -1,7 +1,6 @@
use crate::context::SproutContext;
use alloc::rc::Rc;
use anyhow::{Context, Result, bail};
use serde::{Deserialize, Serialize};
use std::rc::Rc;
/// EFI chainloader action.
pub mod chainload;
@@ -10,36 +9,6 @@ pub mod edera;
/// EFI console print action.
pub mod print;
/// Splash screen action.
#[cfg(feature = "splash")]
pub mod splash;
/// Declares an action that sprout can execute.
/// Actions allow configuring sprout's internal runtime mechanisms with values
/// that you can specify via other concepts.
///
/// Actions are the main work that Sprout gets done, like booting Linux.
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
pub struct ActionDeclaration {
/// Chainload to another EFI application.
/// This allows you to load any EFI application, either to boot an operating system
/// or to perform more EFI actions and return to sprout.
#[serde(default)]
pub chainload: Option<chainload::ChainloadConfiguration>,
/// Print a string to the EFI console.
#[serde(default)]
pub print: Option<print::PrintConfiguration>,
/// Show an image as a fullscreen splash screen.
#[serde(default)]
#[cfg(feature = "splash")]
pub splash: Option<splash::SplashConfiguration>,
/// Boot the Edera hypervisor and the root operating system.
/// This action is an extension on top of the Xen EFI stub that
/// is specific to Edera.
#[serde(default, rename = "edera")]
pub edera: Option<edera::EderaConfiguration>,
}
/// Execute the action specified by `name` which should be stored in the
/// root context of the provided `context`. This function may not return
/// if the provided action executes an operating system or an EFI application
@@ -67,12 +36,6 @@ pub fn execute(context: Rc<SproutContext>, name: impl AsRef<str>) -> Result<()>
return Ok(());
}
#[cfg(feature = "splash")]
if let Some(splash) = &action.splash {
splash::splash(context.clone(), splash)?;
return Ok(());
}
// If we reach here, we don't know how to execute the action that was configured.
// This is likely unreachable, but we should still return an error just in case.
bail!("unknown action configuration");
+127
View File
@@ -0,0 +1,127 @@
use crate::context::SproutContext;
use crate::phases::before_handoff;
use alloc::boxed::Box;
use alloc::rc::Rc;
use anyhow::{Context, Result, bail};
use edera_sprout_config::actions::chainload::ChainloadConfiguration;
use edera_sprout_parsing::{combine_options, empty_is_none};
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 uefi::CString16;
use uefi::proto::loaded_image::LoadedImage;
/// Executes the chainload action using the specified `configuration` inside the provided `context`.
pub fn chainload(context: Rc<SproutContext>, configuration: &ChainloadConfiguration) -> Result<()> {
// Retrieve the current image handle of sprout.
let sprout_image = uefi::boot::image_handle();
// Resolve the path to the image to chainload.
let resolved = eficore::path::resolve_path(
Some(context.root().loaded_image_path()?),
context.stamp(&configuration.path),
)
.context("unable to resolve chainload path")?;
// 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.
let image = ImageLoader::load(request)?;
// Stamp and combine the options to pass to the image.
let options = combine_options(context.stamp_iter(configuration.options.iter()));
// Pass the load options to the image.
// If no options are provided, the resulting string will be empty.
// The options are pinned and boxed to ensure that they are valid for the lifetime of this
// function, which ensures the lifetime of the options for the image runtime.
let options = Box::pin(
CString16::try_from(&options[..])
.context("unable to convert chainloader options to CString16")?,
);
// Ensure the chainloader options limit is not exceeded.
if options.num_bytes() > u32::MAX as usize {
bail!("chainloader options too large");
}
// Open the LoadedImage protocol of the image to chainload to pass the load options.
// This is done in a block to release the protocol before the image is started, as the
// image may itself need to open its LoadedImage protocol exclusively.
{
let mut loaded_image_protocol =
uefi::boot::open_protocol_exclusive::<LoadedImage>(*image.handle())
.context("unable to open loaded image protocol")?;
// SAFETY: option size is checked to validate it is safe to pass.
// Additionally, the pointer is allocated and retained on heap, which makes
// passing the `options` pointer safe to the next image.
unsafe {
loaded_image_protocol
.set_load_options(options.as_ptr() as *const u8, options.num_bytes() as u32);
}
}
// Stamp the initrd path, if provided.
let initrd = configuration
.linux_initrd
.as_ref()
.map(|item| context.stamp(item));
// The initrd can be None or empty, so we need to collapse that into a single Option.
let initrd = empty_is_none(initrd);
// If an initrd is provided, register it with the EFI stack.
let mut initrd_handle = None;
if let Some(linux_initrd) = initrd {
let resolved_initrd =
eficore::path::resolve_path(Some(context.root().loaded_image_path()?), &linux_initrd)
.context("unable to resolve linux initrd path")?;
// A path without a file component refers to the root of the filesystem, not an initrd.
// This happens when a path template like "$root\\$initrd" is stamped with an empty
// initrd value, such as a BLS entry without an initrd.
let initrd_subpath = eficore::path::device_path_subpath(&resolved_initrd.full_path)
.context("unable to get linux initrd subpath")?;
if !initrd_subpath.trim_matches('\\').is_empty() {
let content = resolved_initrd
.read_file()
.context("unable to read linux initrd")?;
let handle = MediaLoaderHandle::register(
LINUX_EFI_INITRD_MEDIA_GUID,
content.into_boxed_slice(),
)
.context("unable to register linux initrd")?;
initrd_handle = Some(handle);
}
}
// Mark execution of an entry in the bootloader interface.
BootloaderInterface::mark_exec(context.root().timer())
.context("unable to mark execution of boot entry in bootloader interface")?;
// Since we are about to hand off control to another image, we need to execute the handoff hook.
// This will perform operations like clearing the screen.
before_handoff(&context).context("unable to execute before handoff hook")?;
// Start the loaded image.
// This call might return, or it may pass full control to another image that will never return.
// Capture the result to ensure we can return an error if the image fails to start, but only
// after the optional initrd has been unregistered.
let result = uefi::boot::start_image(*image.handle());
// Assert there was no error starting the image.
result.context("unable to start image")?;
// Explicitly drop the options to clarify the lifetime.
drop(options);
// Explicitly drop the initrd handle to clarify when it should be unregistered.
drop(initrd_handle);
// Return control to sprout.
Ok(())
}
+122
View File
@@ -0,0 +1,122 @@
use crate::{actions, context::SproutContext};
use alloc::rc::Rc;
use alloc::vec::Vec;
use alloc::{format, vec};
use anyhow::{Context, Result};
use edera_sprout_config::actions::chainload::ChainloadConfiguration;
use edera_sprout_config::actions::edera::EderaConfiguration;
use edera_sprout_parsing::{build_xen_config, combine_options, empty_is_none};
use eficore::media_loader::{
MediaLoaderHandle,
constants::xen::{
XEN_EFI_CONFIG_MEDIA_GUID, XEN_EFI_KERNEL_MEDIA_GUID, XEN_EFI_RAMDISK_MEDIA_GUID,
},
};
use eficore::platform::tpm::PlatformTpm;
use uefi::Guid;
/// Register a media loader for the provided `bytes` with the vendor `guid`.
/// `what` should indicate some identifying value for error messages
/// like `config` or `kernel`.
/// Provides a [MediaLoaderHandle] that can be used to unregister the media loader.
fn register_media_loader_bytes(
guid: Guid,
what: &str,
bytes: Vec<u8>,
) -> Result<MediaLoaderHandle> {
MediaLoaderHandle::register(guid, bytes.into_boxed_slice())
.context(format!("unable to register {} media loader", what))
}
/// Read the contents of the loader payload at `path` relative to the sprout image.
/// `what` should indicate some identifying value for error messages
/// like `kernel` or `initrd`.
fn read_loader_payload(context: &Rc<SproutContext>, what: &str, path: &str) -> Result<Vec<u8>> {
let path = context.stamp(path);
eficore::path::read_file_contents(Some(context.root().loaded_image_path()?), &path)
.context(format!("unable to read {} file", what))
}
/// Executes the edera action which will boot the Edera hypervisor with the specified
/// `configuration` and `context`. This action uses Edera-specific Xen EFI stub functionality.
pub fn edera(context: Rc<SproutContext>, configuration: &EderaConfiguration) -> Result<()> {
// Only register the initrd media loader if the user actually configured one.
let xen_opts = combine_options(context.stamp_iter(configuration.xen_options.iter()));
let dom0_args = combine_options(context.stamp_iter(configuration.kernel_options.iter()));
// Measure xen options and dom0 cmdline into PCR 12 in a fixed order.
PlatformTpm::log_event(
PlatformTpm::PCR_KERNEL_PARAMETERS,
xen_opts.as_bytes(),
"sprout: xen options",
)
.context("unable to measure xen options into the TPM")?;
PlatformTpm::log_event(
PlatformTpm::PCR_KERNEL_PARAMETERS,
dom0_args.as_bytes(),
"sprout: dom0 cmdline",
)
.context("unable to measure dom0 cmdline into the TPM")?;
// Build the Xen config text and register it as the config media loader. The
// assembled text is a derived artifact, intentionally not measured.
let config_handle = register_media_loader_bytes(
XEN_EFI_CONFIG_MEDIA_GUID,
"config",
build_xen_config(&xen_opts, &dom0_args).into_bytes(),
)
.context("unable to register config media loader")?;
// Read the dom0 kernel, measure it into PCR 11, then register the kernel
// media loader.
let kernel_bytes = read_loader_payload(&context, "kernel", &configuration.kernel)?;
PlatformTpm::log_event(
PlatformTpm::PCR_KERNEL,
&kernel_bytes,
"sprout: dom0 kernel",
)
.context("unable to measure dom0 kernel into the TPM")?;
let kernel_handle =
register_media_loader_bytes(XEN_EFI_KERNEL_MEDIA_GUID, "kernel", kernel_bytes)
.context("unable to register kernel media loader")?;
// Extend PCR 9 with the initrd bytes (empty when no initrd is
// configured).
let initrd_bytes = match empty_is_none(configuration.initrd.as_ref()) {
Some(p) => read_loader_payload(&context, "initrd", p)?,
None => Vec::new(),
};
PlatformTpm::log_event(
PlatformTpm::PCR_INITRD,
&initrd_bytes,
"sprout: dom0 initrd",
)
.context("unable to measure dom0 initrd into the TPM")?;
// Create a vector of media loaders to drop them only after this function completes.
let mut media_loaders = vec![config_handle, kernel_handle];
// Register the initrd if it is provided.
if !initrd_bytes.is_empty() {
let initrd_handle =
register_media_loader_bytes(XEN_EFI_RAMDISK_MEDIA_GUID, "initrd", initrd_bytes)
.context("unable to register initrd media loader")?;
media_loaders.push(initrd_handle);
}
// Chainload to the Xen EFI stub.
let result = actions::chainload::chainload(
context.clone(),
&ChainloadConfiguration {
path: configuration.xen.clone(),
options: vec![],
linux_initrd: None,
},
)
.context("unable to chainload to xen");
// Explicitly drop the media loaders to clarify when they should be unregistered.
drop(media_loaders);
result
}
@@ -1,16 +1,8 @@
use crate::context::SproutContext;
use alloc::rc::Rc;
use anyhow::Result;
use edera_sprout_config::actions::print::PrintConfiguration;
use log::info;
use serde::{Deserialize, Serialize};
use std::rc::Rc;
/// The configuration of the print action.
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
pub struct PrintConfiguration {
/// The text to print to the console.
#[serde(default)]
pub text: String,
}
/// Executes the print action with the specified `configuration` inside the provided `context`.
pub fn print(context: Rc<SproutContext>, configuration: &PrintConfiguration) -> Result<()> {
@@ -1,5 +1,6 @@
use crate::config::RootConfiguration;
use anyhow::{Context, Result};
use edera_sprout_config::RootConfiguration;
use log::warn;
use uefi::fs::FileSystem;
use uefi::proto::device_path::DevicePath;
use uefi::proto::media::fs::SimpleFileSystem;
@@ -25,15 +26,24 @@ pub fn autoconfigure(config: &mut RootConfiguration) -> Result<()> {
// For each filesystem that was detected, scan it for supported autoconfig mechanisms.
for handle in filesystem_handles {
// Acquire the device path root for the filesystem.
let root = {
uefi::boot::open_protocol_exclusive::<DevicePath>(handle)
.context("unable to get root for filesystem")?
.to_boxed()
// A filesystem that can't be opened is skipped, as it should not prevent
// autoconfiguring the other filesystems.
let root = match uefi::boot::open_protocol_exclusive::<DevicePath>(handle) {
Ok(root) => root.to_boxed(),
Err(error) => {
warn!("skipping filesystem, unable to get its root: {}", error);
continue;
}
};
// Open the filesystem that was detected.
let filesystem = uefi::boot::open_protocol_exclusive::<SimpleFileSystem>(handle)
.context("unable to open filesystem")?;
let filesystem = match uefi::boot::open_protocol_exclusive::<SimpleFileSystem>(handle) {
Ok(filesystem) => filesystem,
Err(error) => {
warn!("skipping filesystem, unable to open it: {}", error);
continue;
}
};
// Trade the filesystem protocol for the uefi filesystem helper.
let mut filesystem = FileSystem::new(filesystem);
@@ -1,11 +1,13 @@
use crate::actions::ActionDeclaration;
use crate::actions::chainload::ChainloadConfiguration;
use crate::config::RootConfiguration;
use crate::entries::EntryDeclaration;
use crate::generators::GeneratorDeclaration;
use crate::generators::bls::BlsConfiguration;
use crate::utils;
use alloc::string::ToString;
use alloc::{format, vec};
use anyhow::{Context, Result};
use edera_sprout_config::RootConfiguration;
use edera_sprout_config::actions::ActionDeclaration;
use edera_sprout_config::actions::chainload::ChainloadConfiguration;
use edera_sprout_config::entries::EntryDeclaration;
use edera_sprout_config::generators::GeneratorDeclaration;
use edera_sprout_config::generators::bls::BlsConfiguration;
use edera_sprout_parsing::unique_hash;
use uefi::cstr16;
use uefi::fs::{FileSystem, Path};
use uefi::proto::device_path::DevicePath;
@@ -28,14 +30,14 @@ pub fn scan(
// Convert the device path root to a string we can use in the configuration.
let mut root = root
.to_string(DisplayOnly(false), AllowShortcuts(false))
.to_string16(DisplayOnly(false), AllowShortcuts(false))
.context("unable to convert device root to string")?
.to_string();
// Add a trailing forward-slash to the root to ensure the device root is completed.
root.push('/');
// Generate a unique hash of the root path.
let root_unique_hash = utils::unique_hash(&root);
let root_unique_hash = unique_hash(&root);
// Whether we have a loader.conf file.
let has_loader_conf = filesystem
@@ -1,12 +1,18 @@
use crate::actions::ActionDeclaration;
use crate::actions::chainload::ChainloadConfiguration;
use crate::config::RootConfiguration;
use crate::entries::EntryDeclaration;
use crate::generators::GeneratorDeclaration;
use crate::generators::list::ListConfiguration;
use crate::utils;
use alloc::collections::BTreeMap;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use alloc::{format, vec};
use anyhow::{Context, Result};
use std::collections::BTreeMap;
use edera_sprout_config::RootConfiguration;
use edera_sprout_config::actions::ActionDeclaration;
use edera_sprout_config::actions::chainload::ChainloadConfiguration;
use edera_sprout_config::entries::EntryDeclaration;
use edera_sprout_config::generators::GeneratorDeclaration;
use edera_sprout_config::generators::list::ListConfiguration;
use edera_sprout_parsing::{
LINUX_INITRAMFS_PREFIXES, LINUX_KERNEL_PREFIXES, initramfs_candidates, match_kernel_prefix,
unique_hash,
};
use uefi::CString16;
use uefi::fs::{FileSystem, Path, PathBuf};
use uefi::proto::device_path::DevicePath;
@@ -20,11 +26,17 @@ const LINUX_CHAINLOAD_ACTION_PREFIX: &str = "linux-chainload-";
/// The empty string represents the root of the filesystem.
const SCAN_LOCATIONS: &[&str] = &["\\boot", "\\"];
/// Prefixes of kernel files to scan for.
const KERNEL_PREFIXES: &[&str] = &["vmlinuz"];
/// Prefixes of initramfs files to match to.
const INITRAMFS_PREFIXES: &[&str] = &["initramfs", "initrd", "initrd.img"];
/// This is really silly, but if what we are booting is the Canonical stubble stub,
/// there is a chance it will assert that the load options are non-empty.
/// Technically speaking, load options can be empty. However, it assumes load options
/// have something in it. Canonical's stubble copied code from systemd that does this
/// and then uses that code improperly by asserting that the pointer is non-null.
/// To give a good user experience, we place a placeholder value here to ensure it's non-empty.
/// For stubble, this code ensures the command line pointer becomes null:
/// <https://github.com/ubuntu/stubble/blob/e56643979addfb98982266018e08921c07424a0c/stub.c#L61-L64>
/// Then this code asserts on it, stopping the boot process:
/// <https://github.com/ubuntu/stubble/blob/e56643979addfb98982266018e08921c07424a0c/stub.c#L27>
const DEFAULT_LINUX_OPTIONS: &str = "placeholder";
/// Pair of kernel and initramfs.
/// This is what scanning a directory is meant to find.
@@ -93,9 +105,7 @@ fn scan_directory(filesystem: &mut FileSystem, path: &str) -> Result<Vec<KernelP
// Find a kernel prefix that matches, if any.
// This is case-insensitive to ensure we pick up all possibilities.
let Some(prefix) = KERNEL_PREFIXES.iter().find(|prefix| {
name_for_match == **prefix || name_for_match.starts_with(&format!("{}-", prefix))
}) else {
let Some(prefix) = match_kernel_prefix(&name_for_match, LINUX_KERNEL_PREFIXES) else {
// Skip over anything that doesn't match a kernel prefix.
continue;
};
@@ -103,15 +113,14 @@ fn scan_directory(filesystem: &mut FileSystem, path: &str) -> Result<Vec<KernelP
// Acquire the suffix of the name, this will be used to match an initramfs.
let suffix = &name[prefix.len()..];
// Find a matching initramfs, if any.
let mut initramfs_prefix_iter = INITRAMFS_PREFIXES.iter();
// Find a matching initramfs by trying each candidate name, if any.
let mut candidates = initramfs_candidates(suffix, LINUX_INITRAMFS_PREFIXES);
let matched_initramfs_path = loop {
let Some(prefix) = initramfs_prefix_iter.next() else {
let Some(candidate) = candidates.next() else {
break None;
};
// Construct an initramfs path.
let initramfs = format!("{}{}", prefix, suffix);
let initramfs = CString16::try_from(initramfs.as_str())
let initramfs = CString16::try_from(candidate.as_str())
.context("unable to convert initramfs name to CString16")?;
let mut initramfs_path = path_for_join.clone();
initramfs_path.push(Path::new(&initramfs));
@@ -149,14 +158,14 @@ pub fn scan(
// Convert the device path root to a string we can use in the configuration.
let mut root = root
.to_string(DisplayOnly(false), AllowShortcuts(false))
.to_string16(DisplayOnly(false), AllowShortcuts(false))
.context("unable to convert device root to string")?
.to_string();
// Add a trailing forward-slash to the root to ensure the device root is completed.
root.push('/');
// Generate a unique hash of the root path.
let root_unique_hash = utils::unique_hash(&root);
let root_unique_hash = unique_hash(&root);
// Scan all locations for kernel pairs, adding them to the list.
for location in SCAN_LOCATIONS {
@@ -171,13 +180,14 @@ pub fn scan(
}
// Generate a unique name for the linux chainload action.
let chainload_action_name = format!("{}{}", LINUX_CHAINLOAD_ACTION_PREFIX, root_unique_hash,);
let chainload_action_name = format!("{}{}", LINUX_CHAINLOAD_ACTION_PREFIX, root_unique_hash);
// Kernel pairs are detected, generate a list configuration for it.
let generator = ListConfiguration {
entry: EntryDeclaration {
title: "Boot Linux $name".to_string(),
actions: vec![chainload_action_name.clone()],
sort_key: Some("$kernel".to_string()),
..Default::default()
},
values: pairs
@@ -208,9 +218,10 @@ pub fn scan(
// Insert a default value for the linux-options if it doesn't exist.
if !config.values.contains_key("linux-options") {
config
.values
.insert("linux-options".to_string(), "".to_string());
config.values.insert(
"linux-options".to_string(),
DEFAULT_LINUX_OPTIONS.to_string(),
);
}
// Generate a chainload configuration for the list generator.
@@ -1,9 +1,11 @@
use crate::actions::ActionDeclaration;
use crate::actions::chainload::ChainloadConfiguration;
use crate::config::RootConfiguration;
use crate::entries::EntryDeclaration;
use crate::utils;
use alloc::string::ToString;
use alloc::{format, vec};
use anyhow::{Context, Result};
use edera_sprout_config::RootConfiguration;
use edera_sprout_config::actions::ActionDeclaration;
use edera_sprout_config::actions::chainload::ChainloadConfiguration;
use edera_sprout_config::entries::EntryDeclaration;
use edera_sprout_parsing::unique_hash;
use uefi::CString16;
use uefi::fs::{FileSystem, Path};
use uefi::proto::device_path::DevicePath;
@@ -36,26 +38,27 @@ pub fn scan(
// Convert the device path root to a string we can use in the configuration.
let mut root = root
.to_string(DisplayOnly(false), AllowShortcuts(false))
.to_string16(DisplayOnly(false), AllowShortcuts(false))
.context("unable to convert device root to string")?
.to_string();
// Add a trailing forward-slash to the root to ensure the device root is completed.
root.push('/');
// Generate a unique hash of the root path.
let root_unique_hash = utils::unique_hash(&root);
let root_unique_hash = unique_hash(&root);
// Generate a unique name for the Windows chainload action.
let chainload_action_name = format!("{}{}", WINDOWS_CHAINLOAD_ACTION_PREFIX, root_unique_hash,);
// Generate an entry name for Windows.
let entry_name = format!("auto-windows-{}", root_unique_hash,);
let entry_name = format!("auto-windows-{}", root_unique_hash);
// Create an entry for Windows and insert it into the configuration.
let entry = EntryDeclaration {
title: "Boot Windows".to_string(),
actions: vec![chainload_action_name.clone()],
values: Default::default(),
sort_key: None, // Use the default sort key.
};
config.entries.insert(entry_name, entry);
+2
View File
@@ -0,0 +1,2 @@
/// The configuration loader mechanisms.
pub mod loader;
@@ -1,10 +1,10 @@
use crate::config::{RootConfiguration, latest_version};
use crate::options::SproutOptions;
use crate::platform::tpm::PlatformTpm;
use crate::utils;
use alloc::vec::Vec;
use anyhow::{Context, Result, bail};
use core::ops::Deref;
use edera_sprout_config::{RootConfiguration, latest_version};
use eficore::platform::tpm::PlatformTpm;
use log::info;
use std::ops::Deref;
use toml::Value;
use uefi::proto::device_path::LoadedImageDevicePath;
@@ -20,7 +20,7 @@ fn load_raw_config(options: &SproutOptions) -> Result<Vec<u8>> {
info!("configuration file: {}", options.config);
// Read the contents of the sprout config file.
let content = utils::read_file_contents(Some(&path), &options.config)
let content = eficore::path::read_file_contents(Some(&path), &options.config)
.context("unable to read sprout config file")?;
// Measure the sprout.toml into the TPM, if needed and possible.
+19 -48
View File
@@ -1,11 +1,14 @@
use crate::actions::ActionDeclaration;
use crate::options::SproutOptions;
use crate::platform::timer::PlatformTimer;
use alloc::boxed::Box;
use alloc::collections::{BTreeMap, BTreeSet};
use alloc::rc::Rc;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use anyhow::anyhow;
use anyhow::{Result, bail};
use std::cmp::Reverse;
use std::collections::{BTreeMap, BTreeSet};
use std::rc::Rc;
use edera_sprout_config::actions::ActionDeclaration;
use edera_sprout_parsing::stamp_values;
use eficore::platform::timer::PlatformTimer;
use uefi::proto::device_path::DevicePath;
/// The maximum number of iterations that can be performed in [SproutContext::finalize].
@@ -193,7 +196,7 @@ impl SproutContext {
let mut did_change = false;
let mut values = BTreeMap::new();
for (key, value) in &current_values {
let (changed, result) = Self::stamp_values(&current_values, value);
let (changed, result) = stamp_values(&current_values, value);
if changed {
// If the value changed, we need to re-stamp it.
did_change = true;
@@ -217,52 +220,20 @@ impl SproutContext {
})
}
/// Stamps the `text` value with the specified `values` map. The returned value indicates
/// whether the `text` has been changed and the value that was stamped and changed.
///
/// Stamping works like this:
/// - Start with the input text.
/// - Sort all the keys in reverse length order (longest keys first)
/// - For each key, if the key is not empty, replace $KEY in the text.
/// - Each follow-up iteration acts upon the last iterations result.
/// - We keep track if the text changes during the replacement.
/// - We return both whether the text changed during any iteration and the final result.
fn stamp_values(values: &BTreeMap<String, String>, text: impl AsRef<str>) -> (bool, String) {
let mut result = text.as_ref().to_string();
let mut did_change = false;
// Sort the keys by length. This is to ensure that we stamp the longest keys first.
// If we did not do this, "$abc" could be stamped by "$a" into an invalid result.
let mut keys = values.keys().collect::<Vec<_>>();
// Sort by key length, reversed. This results in the longest keys appearing first.
keys.sort_by_key(|key| Reverse(key.len()));
for key in keys {
// Empty keys are not supported.
if key.is_empty() {
continue;
}
// We can fetch the value from the map. It is verifiable that the key exists.
let Some(value) = values.get(key) else {
unreachable!("keys iterated over is collected on a map that cannot be modified");
};
let next_result = result.replace(&format!("${key}"), value);
if result != next_result {
did_change = true;
}
result = next_result;
}
(did_change, result)
}
/// Stamps the input `text` with all the values in this [SproutContext] and it's parents.
/// For example, if this context contains {"a":"b"}, and the text "hello\\$a", it will produce
/// "hello\\b" as an output string.
pub fn stamp(&self, text: impl AsRef<str>) -> String {
Self::stamp_values(&self.all_values(), text.as_ref()).1
stamp_values(&self.all_values(), text.as_ref()).1
}
/// Stamps all the items from the iterator `input` with all the values in this [SproutContext]
/// and it's parents. This calls [self.stamp] on each item.
pub fn stamp_iter(
&self,
input: impl Iterator<Item = impl AsRef<str>>,
) -> impl Iterator<Item = String> {
input.map(|item| self.stamp(item))
}
/// Unloads a [SproutContext] back into an owned context. This
+16 -22
View File
@@ -1,44 +1,38 @@
use crate::context::SproutContext;
use crate::integrations::shim::{ShimInput, ShimSupport};
use crate::utils;
use alloc::collections::BTreeMap;
use alloc::format;
use alloc::rc::Rc;
use alloc::string::String;
use anyhow::{Context, Result};
use edera_sprout_config::drivers::DriverDeclaration;
use eficore::loader::source::ImageSource;
use eficore::loader::{ImageLoadRequest, ImageLoader};
use log::info;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::rc::Rc;
use uefi::boot::SearchType;
/// Declares a driver configuration.
/// Drivers allow extending the functionality of Sprout.
/// Drivers are loaded at runtime and can provide extra functionality like filesystem support.
/// Drivers are loaded by their name, which is used to reference them in other concepts.
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
pub struct DriverDeclaration {
/// The filesystem path to the driver.
/// This file should be an EFI executable that can be located and executed.
pub path: String,
}
/// Loads the driver specified by the `driver` declaration.
fn load_driver(context: Rc<SproutContext>, driver: &DriverDeclaration) -> Result<()> {
// Acquire the handle and device path of the loaded image.
let sprout_image = uefi::boot::image_handle();
// Resolve the path to the driver image.
let resolved = utils::resolve_path(
let resolved = eficore::path::resolve_path(
Some(context.root().loaded_image_path()?),
&context.stamp(&driver.path),
context.stamp(&driver.path),
)
.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.
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.
// There is no guarantee that the driver will actually return control as it is
// 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(())
}
@@ -55,7 +49,7 @@ fn reconnect() -> Result<()> {
for handle in handles.iter() {
// Ignore the result as there is nothing we can do if reconnecting a controller fails.
// This is also likely to fail in some cases but should fail safely.
let _ = uefi::boot::connect_controller(*handle, None, None, true);
let _ = uefi::boot::connect_controller(*handle, &[], None, true);
}
Ok(())
+36 -20
View File
@@ -1,24 +1,7 @@
use crate::context::SproutContext;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::rc::Rc;
/// Declares a boot entry to display in the boot menu.
///
/// Entries are the user-facing concept of Sprout, making it possible
/// to run a set of actions with a specific context.
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
pub struct EntryDeclaration {
/// The title of the entry which will be display in the boot menu.
/// This is the pre-stamped value.
pub title: String,
/// The actions to run when the entry is selected.
#[serde(default)]
pub actions: Vec<String>,
/// The values to insert into the context when the entry is selected.
#[serde(default)]
pub values: BTreeMap<String, String>,
}
use alloc::rc::Rc;
use alloc::string::{String, ToString};
use edera_sprout_config::entries::EntryDeclaration;
/// Represents an entry that is stamped and ready to be booted.
#[derive(Clone)]
@@ -29,6 +12,7 @@ pub struct BootableEntry {
declaration: EntryDeclaration,
default: bool,
pin_name: bool,
sort_key: Option<String>,
}
impl BootableEntry {
@@ -46,6 +30,7 @@ impl BootableEntry {
declaration,
default: false,
pin_name: false,
sort_key: None,
}
}
@@ -94,6 +79,11 @@ impl BootableEntry {
self.default = true;
}
// Unmark this entry as the default entry.
pub fn unmark_default(&mut self) {
self.default = false;
}
/// Mark this entry as being pinned, which prevents prefixing.
pub fn mark_pin_name(&mut self) {
self.pin_name = true;
@@ -105,10 +95,36 @@ impl BootableEntry {
}
/// 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 {
// 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
}
/// Set the sort key of the entry. This is used to sort entries via version comparison.
pub fn set_sort_key(&mut self, sort_key: String) {
self.sort_key = Some(sort_key);
}
/// Retrieve a reference to the sort key of the entry. If one is not specified, we will use the
/// name of the entry.
pub fn sort_key(&self) -> &str {
// Use the sort key specified in the bootable entry, or use the declaration sort key,
// or use the name of the entry.
self.sort_key
.as_deref()
.or(self.declaration.sort_key.as_deref())
.unwrap_or(&self.name)
}
/// Find an entry by `needle` inside the entry iterator `haystack`.
/// This will search for an entry by name, title, or index.
pub fn find<'a>(
+19
View File
@@ -0,0 +1,19 @@
use crate::context::SproutContext;
use alloc::rc::Rc;
use alloc::string::String;
use anyhow::{Result, bail};
use edera_sprout_config::extractors::ExtractorDeclaration;
/// The filesystem device match extractor.
pub mod filesystem_device_match;
/// Extracts the value using the specified `extractor` under the provided `context`.
/// The extractor must return a value, and if a value cannot be determined, an error
/// should be returned.
pub fn extract(context: Rc<SproutContext>, extractor: &ExtractorDeclaration) -> Result<String> {
if let Some(filesystem) = &extractor.filesystem_device_match {
filesystem_device_match::extract(context, filesystem)
} else {
bail!("unknown extractor configuration");
}
}
@@ -1,44 +1,17 @@
use crate::context::SproutContext;
use crate::utils;
use alloc::rc::Rc;
use alloc::string::String;
use anyhow::{Context, Result, anyhow, bail};
use serde::{Deserialize, Serialize};
use std::ops::Deref;
use std::rc::Rc;
use std::str::FromStr;
use core::ops::Deref;
use core::str::FromStr;
use edera_sprout_config::extractors::filesystem_device_match::FilesystemDeviceMatchExtractor;
use eficore::partition::PartitionGuidForm;
use uefi::fs::{FileSystem, Path};
use uefi::proto::device_path::DevicePath;
use uefi::proto::media::file::{File, FileSystemVolumeLabel};
use uefi::proto::media::fs::SimpleFileSystem;
use uefi::{CString16, Guid};
/// The filesystem device match extractor.
/// This extractor finds a filesystem using some search criteria and returns
/// the device root path that can concatenated with subpaths to access files
/// on a particular filesystem.
/// The fallback value can be used to provide a value if no match is found.
///
/// This extractor requires all the criteria to match. If no criteria is provided,
/// an error is returned.
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
pub struct FilesystemDeviceMatchExtractor {
/// Matches a filesystem that has the specified label.
#[serde(default, rename = "has-label")]
pub has_label: Option<String>,
/// Matches a filesystem that has the specified item.
/// An item is either a directory or file.
#[serde(default, rename = "has-item")]
pub has_item: Option<String>,
/// Matches a filesystem that has the specified partition UUID.
#[serde(default, rename = "has-partition-uuid")]
pub has_partition_uuid: Option<String>,
/// Matches a filesystem that has the specified partition type UUID.
#[serde(default, rename = "has-partition-type-uuid")]
pub has_partition_type_uuid: Option<String>,
/// The fallback value to use if no filesystem matches the criteria.
#[serde(default)]
pub fallback: Option<String>,
}
/// Extract a filesystem device path using the specified `context` and `extractor` configuration.
pub fn extract(
context: Rc<SproutContext>,
@@ -75,8 +48,9 @@ pub fn extract(
.to_boxed();
// Fetch the partition uuid for this filesystem.
let partition_uuid = utils::partition_guid(&root, utils::PartitionGuidForm::Partition)
.context("unable to fetch the partition uuid of the filesystem")?;
let partition_uuid =
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.
// If it does not match, continue to the next filesystem.
@@ -100,7 +74,7 @@ pub fn extract(
// Fetch the partition type uuid for this filesystem.
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")?;
// Compare the partition type uuid to the parsed uuid.
// If it does not match, continue to the next filesystem.
@@ -138,14 +112,11 @@ pub fn extract(
let mut filesystem = FileSystem::new(filesystem);
// Check the metadata of the item.
let metadata = filesystem.metadata(Path::new(&want_item));
// Ignore filesystem errors as we can't do anything useful with the error.
if metadata.is_err() {
let Some(metadata) = filesystem.metadata(Path::new(&want_item)).ok() else {
continue;
}
};
let metadata = metadata?;
// Only check directories and files.
if !(metadata.is_directory() || metadata.is_regular_file()) {
continue;
@@ -163,7 +134,7 @@ pub fn extract(
.context("unable to open filesystem device path")?;
let path = path.deref();
// 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.
+34
View File
@@ -0,0 +1,34 @@
use crate::context::SproutContext;
use crate::entries::BootableEntry;
use alloc::rc::Rc;
use alloc::vec::Vec;
use anyhow::Result;
use anyhow::bail;
use edera_sprout_config::generators::GeneratorDeclaration;
/// The BLS generator.
pub mod bls;
/// The list generator.
pub mod list;
/// The matrix generator.
pub mod matrix;
/// Runs the generator specified by the `generator` option.
/// It uses the specified `context` as the parent context for
/// the generated entries, injecting more values if needed.
pub fn generate(
context: Rc<SproutContext>,
generator: &GeneratorDeclaration,
) -> Result<Vec<BootableEntry>> {
if let Some(matrix) = &generator.matrix {
matrix::generate(context, matrix)
} else if let Some(bls) = &generator.bls {
bls::generate(context, bls)
} else if let Some(list) = &generator.list {
list::generate(context, list)
} else {
bail!("unknown generator configuration");
}
}
+202
View File
@@ -0,0 +1,202 @@
use crate::context::SproutContext;
use crate::entries::BootableEntry;
use alloc::{
format,
rc::Rc,
string::{String, ToString},
vec::Vec,
};
use anyhow::{Context, Result};
use core::{cmp::Ordering, str::FromStr};
use edera_sprout_bls::{BlsEntry, sort_bls};
use edera_sprout_config::generators::bls::BlsConfiguration;
use log::warn;
use uefi::{
cstr16,
fs::{FileSystem, PathBuf},
proto::device_path::text::{AllowShortcuts, DisplayOnly},
proto::media::fs::SimpleFileSystem,
};
// TODO(azenla): remove this once variable substitution is implemented.
/// This function is used to remove the `tuned_initrd` variable from entry values.
/// Fedora uses tuned which adds an initrd that shouldn't be used.
fn quirk_initrd_remove_tuned(input: String) -> String {
input.replace("$tuned_initrd", "").trim().to_string()
}
/// Sorts two entries according to the BLS sort system.
/// Reference: <https://uapi-group.org/specifications/specs/boot_loader_specification/#sorting>
fn sort_entries(a: &(BlsEntry, BootableEntry), b: &(BlsEntry, BootableEntry)) -> Ordering {
let (a_bls, a_boot) = a;
let (b_bls, b_boot) = b;
sort_bls(a_bls, a_boot.name(), b_bls, b_boot.name())
}
/// Generates entries from the BLS entries directory using the specified `bls` configuration and
/// `context`. The BLS conversion is best-effort and will ignore any unsupported entries.
pub fn generate(context: Rc<SproutContext>, bls: &BlsConfiguration) -> Result<Vec<BootableEntry>> {
let mut entries = Vec::new();
// Stamp the path to the BLS directory.
let path = context.stamp(&bls.path);
// Resolve the path to the BLS directory.
let bls_resolved =
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.
let mut entries_path = PathBuf::from(
bls_resolved
.sub_path
.to_string16(DisplayOnly(false), AllowShortcuts(false))
.context("unable to convert bls path to string")?,
);
entries_path.push(cstr16!("entries"));
// Open exclusive access to the BLS filesystem.
let fs =
uefi::boot::open_protocol_exclusive::<SimpleFileSystem>(bls_resolved.filesystem_handle)
.context("unable to open bls filesystem")?;
let mut fs = FileSystem::new(fs);
// The entries directory is optional, as a BLS directory may only contain a loader.conf file.
// If it does not exist, there are no entries to generate.
if !fs
.try_exists(&entries_path)
.context("unable to check for bls entries directory")?
{
return Ok(Vec::new());
}
// Read the BLS entries directory.
let entries_iter = fs
.read_dir(&entries_path)
.context("unable to read bls entries")?;
// For each entry in the BLS entries directory, parse the entry and add it to the list.
for entry in entries_iter {
// Unwrap the entry file info.
let entry = entry.context("unable to read bls item entry")?;
// Skip items that are not regular files.
if !entry.is_regular_file() {
continue;
}
// Get the file name of the filesystem item.
let mut name = entry.file_name().to_string();
// Ignore files that are not .conf files.
if !name.to_lowercase().ends_with(".conf") {
continue;
}
// Remove the .conf extension.
name.truncate(name.len() - 5);
// Skip over files that are named just ".conf" as they are not valid entry files.
if name.is_empty() {
continue;
}
// Create a mutable path so we can append the file name to produce the full path.
let mut full_entry_path = entries_path.to_path_buf();
full_entry_path.push(entry.file_name());
// Read the entry file.
// Entries that can't be read or parsed are skipped, as one broken entry
// should not prevent booting any of the other entries.
let content = match fs.read(full_entry_path) {
Ok(content) => content,
Err(error) => {
warn!("unable to read bls entry {}: {}", name, error);
continue;
}
};
// Parse the entry file as a UTF-8 string.
let content = match String::from_utf8(content) {
Ok(content) => content,
Err(error) => {
warn!("unable to read bls entry {} as utf8: {}", name, error);
continue;
}
};
// Parse the entry file as a BLS entry.
let entry = match BlsEntry::from_str(&content) {
Ok(entry) => entry,
Err(error) => {
warn!("unable to parse bls entry {}: {}", name, error);
continue;
}
};
// Ignore entries that are not valid for Sprout.
if !entry.is_valid() {
continue;
}
// Produce a new sprout context for the entry with the extracted values.
let mut context = context.fork();
let title_base = entry.title().unwrap_or_else(|| name.clone());
let chainload = entry.chainload_path().unwrap_or_default();
let options = entry.options().unwrap_or_default();
let version = entry.version().unwrap_or_default();
let machine_id = entry.machine_id().unwrap_or_default();
// Put the initrd through a quirk modifier to support Fedora.
let initrd = quirk_initrd_remove_tuned(entry.initrd_path().unwrap_or_default());
// Combine the title with the version if a version is present, except if it already contains it.
// Sometimes BLS will have a version in the title already, and this makes it unique.
let title_full = if !version.is_empty() && !title_base.contains(&version) {
format!("{} {}", title_base, version)
} else {
title_base.clone()
};
context.set("title-base", title_base);
context.set("title", title_full);
context.set("chainload", chainload);
context.set("options", options);
context.set("initrd", initrd);
context.set("version", version);
context.set("machine-id", machine_id);
// Produce a new bootable entry.
let mut boot = BootableEntry::new(
name,
bls.entry.title.clone(),
context.freeze(),
bls.entry.clone(),
);
// Pin the entry name to prevent prefixing.
// This is needed as the bootloader interface requires the name to be
// the same as the entry file name, minus the .conf extension.
boot.mark_pin_name();
// Add the BLS entry to the list, along with the bootable entry.
entries.push((entry, boot));
}
// Sort all the entries according to the BLS sort system.
entries.sort_by(sort_entries);
// Grab the number of entries that we have, so we can calculate a reverse index.
let entry_count = entries.len();
// Set the sort keys of all the bootable entries to a semi-unique prefix + the BLS sort order.
// The final comparison happens using version comparison, so this will sort
// things properly.
for (idx, (_bls, boot)) in entries.iter_mut().enumerate() {
boot.set_sort_key(format!("bls-{}-{}", path, entry_count - idx - 1));
}
// Collect all the bootable entries and return them.
Ok(entries.into_iter().map(|(_, boot)| boot).collect())
}
@@ -1,22 +1,10 @@
use crate::context::SproutContext;
use crate::entries::{BootableEntry, EntryDeclaration};
use crate::entries::BootableEntry;
use alloc::rc::Rc;
use alloc::string::ToString;
use alloc::vec::Vec;
use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::rc::Rc;
/// List generator configuration.
/// The list generator produces multiple entries based
/// on a set of input maps.
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
pub struct ListConfiguration {
/// The template entry to use for each generated entry.
#[serde(default)]
pub entry: EntryDeclaration,
/// The values to use as the input for the matrix.
#[serde(default)]
pub values: Vec<BTreeMap<String, String>>,
}
use edera_sprout_config::generators::list::ListConfiguration;
/// Generates a set of entries using the specified `list` configuration in the `context`.
pub fn generate(
@@ -34,11 +22,10 @@ pub fn generate(
// Stamp the entry title and actions from the template.
let mut entry = list.entry.clone();
entry.actions = entry
.actions
.into_iter()
.map(|action| context.stamp(action))
.collect();
// Stamp all the actions this entry references.
entry.actions = context.stamp_iter(entry.actions.into_iter()).collect();
// Push the entry into the list with the new context.
entries.push(BootableEntry::new(
index.to_string(),
+26
View File
@@ -0,0 +1,26 @@
use crate::context::SproutContext;
use crate::entries::BootableEntry;
use crate::generators::list;
use alloc::rc::Rc;
use alloc::vec::Vec;
use anyhow::Result;
use edera_sprout_config::generators::list::ListConfiguration;
use edera_sprout_config::generators::matrix::MatrixConfiguration;
use edera_sprout_parsing::build_matrix;
/// Generates a set of entries using the specified `matrix` configuration in the `context`.
pub fn generate(
context: Rc<SproutContext>,
matrix: &MatrixConfiguration,
) -> Result<Vec<BootableEntry>> {
// Produce all the combinations of the input values.
let combinations = build_matrix(&matrix.values);
// Use the list generator to generate entries for each combination.
list::generate(
context,
&ListConfiguration {
entry: matrix.entry.clone(),
values: combinations,
},
)
}
+120 -72
View File
@@ -1,27 +1,29 @@
#![doc = include_str!("../README.md")]
#![feature(uefi_std)]
extern crate core;
#![no_std]
#![no_main]
extern crate alloc;
/// The delay to wait for when an error occurs in Sprout.
const DELAY_ON_ERROR: Duration = Duration::from_secs(10);
use crate::config::RootConfiguration;
use crate::context::{RootContext, SproutContext};
use crate::entries::BootableEntry;
use crate::integrations::bootloader_interface::BootloaderInterface;
use crate::options::SproutOptions;
use crate::options::parser::OptionsRepresentable;
use crate::phases::phase;
use crate::platform::timer::PlatformTimer;
use crate::platform::tpm::PlatformTpm;
use crate::secure::SecureBoot;
use crate::utils::PartitionGuidForm;
use crate::{
context::{RootContext, SproutContext},
entries::BootableEntry,
options::SproutOptions,
phases::phase,
};
use alloc::{collections::BTreeMap, format, string::ToString, vec::Vec};
use anyhow::{Context, Result, bail};
use core::{ops::Deref, time::Duration};
use edera_sprout_bls::compare_versions;
use edera_sprout_config::RootConfiguration;
use eficore::{
bootloader_interface::{BootloaderInterface, BootloaderInterfaceTimeout},
partition::PartitionGuidForm,
platform::{timer::PlatformTimer, tpm::PlatformTpm},
secure::SecureBoot,
setup,
};
use log::{error, info, warn};
use std::collections::BTreeMap;
use std::ops::Deref;
use std::time::Duration;
use uefi::proto::device_path::LoadedImageDevicePath;
use uefi::{entry, proto::device_path::LoadedImageDevicePath};
use uefi_raw::Status;
/// actions: Code that can be configured and executed by Sprout.
pub mod actions;
@@ -47,14 +49,11 @@ pub mod extractors;
/// generators: Runtime code that can generate entries with specific values.
pub mod generators;
/// platform: Integration or support code for specific hardware platforms.
pub mod platform;
/// menu: Display a boot menu to select an entry to boot.
pub mod menu;
/// integrations: Code that interacts with other systems.
pub mod integrations;
/// options: Parse the options of the Sprout executable.
pub mod options;
/// phases: Hooks into specific parts of the boot process.
pub mod phases;
@@ -62,23 +61,14 @@ pub mod phases;
/// sbat: Secure Boot Attestation section.
pub mod sbat;
/// secure: Secure Boot support.
pub mod secure;
/// setup: Code that initializes the UEFI environment for Sprout.
pub mod setup;
/// options: Parse the options of the Sprout executable.
pub mod options;
/// utils: Utility functions that are used by other parts of Sprout.
pub mod utils;
/// The delay to wait for when an error occurs in Sprout.
const DELAY_ON_ERROR: Duration = Duration::from_secs(10);
/// Run Sprout, returning an error if one occurs.
fn run() -> Result<()> {
// For safety reasons, we will note that Secure Boot is in beta on Sprout.
if SecureBoot::enabled().context("unable to determine Secure Boot status")? {
warn!("Secure Boot is enabled. Sprout Secure Boot is in beta.");
warn!("Sprout Secure Boot is in beta. Some functionality may not work as expected.");
}
// Start the platform timer.
@@ -129,7 +119,7 @@ fn run() -> Result<()> {
// Grab the partition GUID of the ESP that sprout was loaded from.
let loaded_image_partition_guid =
utils::partition_guid(&loaded_image_path, PartitionGuidForm::Partition)
eficore::partition::partition_guid(&loaded_image_path, PartitionGuidForm::Partition)
.context("unable to retrieve loaded image partition guid")?;
// Set the partition GUID of the ESP that sprout was loaded from in the bootloader interface.
@@ -263,6 +253,81 @@ fn run() -> Result<()> {
}
}
// Sort the entries by their sort key, finalizing the order to show entries. This happens
// in reverse order so that entries that would come last show up first in the menu.
entries.sort_by(|a, b| compare_versions(a.sort_key(), b.sort_key()).reverse());
// Tell the bootloader interface what entries are available.
BootloaderInterface::set_entries(entries.iter().map(|entry| entry.name()))
.context("unable to set entries in bootloader interface")?;
// Acquire the timeout setting from the bootloader interface.
let bootloader_interface_timeout =
BootloaderInterface::get_timeout().context("unable to get bootloader interface timeout")?;
// Acquire the default entry from the bootloader interface.
let bootloader_interface_default_entry = BootloaderInterface::get_default_entry()
.context("unable to get bootloader interface default entry")?;
// Acquire the oneshot entry from the bootloader interface.
let bootloader_interface_oneshot_entry = BootloaderInterface::get_oneshot_entry()
.context("unable to get bootloader interface oneshot entry")?;
// If --boot is specified, boot that entry immediately.
let mut force_boot_entry = context.root().options().boot.clone();
// If --force-menu is specified, show the boot menu regardless of the value of --boot.
let mut force_boot_menu = context.root().options().force_menu;
// Determine the menu timeout in seconds based on the options or configuration.
// We prefer the options over the configuration to allow for overriding.
let mut menu_timeout = context
.root()
.options()
.menu_timeout
.unwrap_or(config.options.menu_timeout);
// Apply bootloader interface timeout settings.
match bootloader_interface_timeout {
BootloaderInterfaceTimeout::MenuForce => {
// Force the boot menu.
force_boot_menu = true;
}
BootloaderInterfaceTimeout::MenuHidden | BootloaderInterfaceTimeout::MenuDisabled => {
// Hide the boot menu by setting the timeout to zero.
menu_timeout = 0;
}
BootloaderInterfaceTimeout::Timeout(timeout) => {
// Configure the timeout to the specified value.
menu_timeout = timeout;
}
BootloaderInterfaceTimeout::Unspecified => {
// Do nothing.
}
}
// Apply bootloader interface default entry settings.
if let Some(ref bootloader_interface_default_entry) = bootloader_interface_default_entry {
// Iterate over all the entries and mark the default entry as the one specified.
for entry in &mut entries {
// Mark the entry as the default entry if it matches the specified entry.
// If the entry does not match the specified entry, unmark it as the default entry.
if entry.is_match(bootloader_interface_default_entry) {
entry.mark_default();
} else {
entry.unmark_default();
}
}
}
// Apply bootloader interface oneshot entry settings.
// If set, we will force booting the oneshot entry.
if let Some(ref bootloader_interface_oneshot_entry) = bootloader_interface_oneshot_entry {
force_boot_entry = Some(bootloader_interface_oneshot_entry.clone());
}
// If no entries were the default, pick the first entry as the default entry.
if entries.iter().all(|entry| !entry.is_default())
&& let Some(entry) = entries.first_mut()
@@ -270,36 +335,7 @@ fn run() -> Result<()> {
entry.mark_default();
}
// Iterate over all the entries and tell the bootloader interface what the entries are.
for entry in &entries {
// If the entry is the default entry, tell the bootloader interface it is the default.
if entry.is_default() {
// Tell the bootloader interface what the default entry is.
BootloaderInterface::set_default_entry(entry.name().to_string())
.context("unable to set default entry in bootloader interface")?;
break;
}
}
// Tell the bootloader interface what entries are available.
BootloaderInterface::set_entries(entries.iter().map(|entry| entry.name()))
.context("unable to set entries in bootloader interface")?;
// Execute the late phase.
phase(context.clone(), &config.phases.late).context("unable to execute late phase")?;
// If --boot is specified, boot that entry immediately.
let force_boot_entry = context.root().options().boot.as_ref();
// If --force-menu is specified, show the boot menu regardless of the value of --boot.
let force_boot_menu = context.root().options().force_menu;
// Determine the menu timeout in seconds based on the options or configuration.
// We prefer the options over the configuration to allow for overriding.
let menu_timeout = context
.root()
.options()
.menu_timeout
.unwrap_or(config.options.menu_timeout);
// Convert the menu timeout to a duration.
let menu_timeout = Duration::from_secs(menu_timeout);
// Use the forced boot entry if possible, otherwise pick the first entry using a boot menu.
@@ -316,6 +352,9 @@ fn run() -> Result<()> {
BootloaderInterface::set_selected_entry(entry.name().to_string())
.context("unable to set selected entry in bootloader interface")?;
// Execute the late phase, now that the entry is chosen but before its actions are executed.
phase(context.clone(), &config.phases.late).context("unable to execute late phase")?;
// Execute all the actions for the selected entry.
for action in &entry.declaration().actions {
let action = entry.context().stamp(action);
@@ -329,23 +368,32 @@ fn run() -> Result<()> {
/// The main entrypoint of sprout.
/// It is possible this function will not return if actions that are executed
/// exit boot services or do not return control to sprout.
fn main() -> Result<()> {
#[entry]
fn efi_main() -> Status {
// Initialize the basic UEFI environment.
setup::init()?;
// If initialization fails, we will return ABORTED.
// NOTE: This function will also initialize the logger.
// The logger will panic if it is unable to initialize.
// It is guaranteed that if this returns, the logger is initialized.
if let Err(error) = setup::init() {
error!("unable to initialize environment: {}", error);
return Status::ABORTED;
}
// Run Sprout, then handle the error.
let result = run();
if let Err(ref error) = result {
// Print an error trace.
error!("sprout encountered an error");
error!("sprout encountered an error: {}", error);
for (index, stack) in error.chain().enumerate() {
error!("[{}]: {}", index, stack);
}
// Sleep to allow the user to read the error.
uefi::boot::stall(DELAY_ON_ERROR);
return Status::ABORTED;
}
// Sprout doesn't necessarily guarantee anything was booted.
// If we reach here, we will exit back to whoever called us.
Ok(())
Status::SUCCESS
}
+35 -20
View File
@@ -1,9 +1,10 @@
use crate::entries::BootableEntry;
use crate::integrations::bootloader_interface::BootloaderInterface;
use crate::platform::timer::PlatformTimer;
use anyhow::{Context, Result, bail};
use log::info;
use std::time::Duration;
use alloc::vec;
use anyhow::{Context, Result};
use core::time::Duration;
use eficore::bootloader_interface::BootloaderInterface;
use eficore::platform::timer::PlatformTimer;
use log::{info, warn};
use uefi::ResultExt;
use uefi::boot::TimerTrigger;
use uefi::proto::console::text::{Input, Key, ScanCode};
@@ -43,29 +44,41 @@ fn read(input: &mut Input, timeout: &Duration) -> Result<MenuOperation> {
.context("unable to create timer event")?
};
// The timeout is in increments of 100 nanoseconds.
let timeout_hundred_nanos = timeout.as_nanos() / 100;
// Check if the timeout is too large to fit into an u64.
if timeout_hundred_nanos > u64::MAX as u128 {
bail!("timeout duration overflow");
}
// Set a timer to trigger after the specified duration.
let trigger = TimerTrigger::Relative(timeout_hundred_nanos as u64);
let trigger = TimerTrigger::Relative(*timeout);
uefi::boot::set_timer(&timer_event, trigger).context("unable to set timeout timer")?;
let mut events = vec![timer_event, key_event];
let event = uefi::boot::wait_for_event(&mut events)
let events = vec![timer_event, key_event];
// Wait for either the timer event or the key event to trigger.
// Store the result so that we can free the timer event.
let event_result = uefi::boot::wait_for_event(&events)
.discard_errdata()
.context("unable to wait for event")?;
.context("unable to wait for event");
// Close the timer event that we acquired.
// We don't need to close the key event because it is owned globally.
// 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() {
uefi::boot::close_event(timer_event).context("unable to close timer event")?;
// Store the result of the close event so we can determine if we can safely assert it.
let close_event_result =
uefi::boot::close_event(timer_event).context("unable to close timer event");
if event_result.is_err()
&& let Err(ref close_event_error) = close_event_result
{
// Log a warning if we failed to close the timer event.
// This is done to ensure we don't mask the wait_for_event error.
warn!("unable to close timer event: {}", close_event_error);
} else {
// If we reach here, we can safely assert that the close event succeeded without
// masking the wait_for_event error.
close_event_result?;
}
}
// Acquire the event that triggered.
let event = event_result?;
// The first event is the timer event.
// If it has triggered, the user did not select a numbered entry.
if event == 0 {
@@ -73,8 +86,10 @@ fn read(input: &mut Input, timeout: &Duration) -> Result<MenuOperation> {
}
// If we reach here, there is a key event.
// Some firmware signals the key event without a key being available,
// in which case there is nothing to do.
let Some(key) = input.read_key().context("unable to read key")? else {
bail!("no key was pressed");
return Ok(MenuOperation::Nop);
};
match key {
@@ -114,7 +129,7 @@ fn select_with_input<'a>(
info!("Boot Menu:");
for (index, entry) in entries.iter().enumerate() {
let title = entry.context().stamp(&entry.declaration().title);
info!(" [{}] {} ({})", index, title, entry.name());
info!(" [{}] {}", index, title);
}
}
+146
View File
@@ -0,0 +1,146 @@
use alloc::string::{String, ToString};
use anyhow::{Context, Result, anyhow};
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>,
/// Retains the boot console before boot.
pub retain_boot_console: bool,
}
/// 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,
retain_boot_console: false,
}
}
}
/// The options parser mechanism for Sprout.
impl SproutOptions {
/// Produces [SproutOptions] from the arguments provided by the UEFI core.
/// Internally, we use the `jaarg` argument parser which has excellent no_std support.
pub fn parse() -> Result<Self> {
enum ArgID {
Help,
AutoConfigure,
Config,
Boot,
ForceMenu,
MenuTimeout,
RetainBootConsole,
}
// 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"),
Opt::flag(ArgID::RetainBootConsole, &["--retain-boot-console"])
.help_text("Retain boot console before boot"),
]);
// 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::RetainBootConsole => {
// Retain the boot console before booting.
result.retain_boot_console = true;
}
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())
.context("unable to exit")?;
Err(anyhow!("unable to exit"))
},
ParseResult::ExitError => unsafe {
uefi::boot::exit(uefi::boot::image_handle(), Status::ABORTED, 0, null_mut())
.context("unable to exit")?;
Err(anyhow!("unable to exit"))
},
}
}
}
+41
View File
@@ -0,0 +1,41 @@
use crate::actions;
use crate::context::SproutContext;
use alloc::format;
use alloc::rc::Rc;
use anyhow::{Context, Result};
use edera_sprout_config::phases::PhaseConfiguration;
/// Executes the specified [phase] of the boot process.
/// The value [phase] should be a reference of a specific phase in the `PhasesConfiguration`.
/// Any error from the actions is propagated into the [Result] and will interrupt further
/// execution of phase actions.
pub fn phase(context: Rc<SproutContext>, phase: &[PhaseConfiguration]) -> Result<()> {
for item in phase {
let mut context = context.fork();
// Insert the values into the context.
context.insert(&item.values);
let context = context.freeze();
// Execute all the actions in this phase configuration.
for action in item.actions.iter() {
actions::execute(context.clone(), action)
.context(format!("unable to execute action '{}'", action))?;
}
}
Ok(())
}
/// Manual hook called by code in the bootloader that hands off to another image.
/// This is used to perform actions like clearing the screen.
pub fn before_handoff(context: &SproutContext) -> Result<()> {
// If we have not been asked to retain the boot console, then we should clear the screen.
if !context.root().options().retain_boot_console {
// Clear the screen. We use clear here instead of reset because some firmware,
// particularly Dell firmware, does not clear the screen on reset.
// We clear both stdout and stderr because it's not guaranteed that they are the same
// text output.
uefi::system::with_stdout(|stdout| stdout.clear()).context("unable to clear screen")?;
uefi::system::with_stderr(|stderr| stderr.clear()).context("unable to clear screen")?;
}
Ok(())
}
File renamed without changes.
Internal Server Error - Bacon: Gitea
500 Internal Server Error

An error occurred:

An error occurred

Gitea Version: 28.2.0