Compare commits

..
388 Commits
Author SHA1 Message Date
alex 1cd9f67c48 sprout: version 0.0.30 (#103) 2026-09-26 21:17:37 -07:00
alex b39436cbe2 fix(menu): fall back to the basic menu if the simple menu fails
The simple menu failed, and boot was aborted, when the console couldn't
set the cursor position or colors or clear the screen, which some
serial consoles and minimal console drivers don't support. Use the
basic menu, which only prints text, when the simple menu fails.
2026-09-26 20:52:15 -07:00
alex 5fd2dbb4a7 docs(development): list every crate in the crate structure
The crate list left out edera-sprout-bls and edera-sprout-parsing, and
said edera-sprout-boot was "as" crates/boot instead of "at".
2026-09-26 20:52:15 -07:00
alex 22dde5d0ac docs(readme): link the Linux boot protocol roadmap item to its issue
The Linux boot protocol item linked to the multiboot2 issue (#7)
instead of its own issue (#8).
2026-09-26 20:52:15 -07:00
alex 660a2df454 docs(setup): fix step headings and the name of the downloaded file
The Ubuntu guide repeated the Step 7 heading, and the Windows guide went
from Step 1 to Step 3. The guides also said to download
sprout-<arch>.efi, then to copy the downloaded sprout.efi file, which
doesn't exist under that name.
2026-09-26 20:52:15 -07:00
alex 5427d621c5 docs(setup): describe the filesystem of /boot on openSUSE
The openSUSE guide said most Debian systems use ext4 for /boot, copied
from the Debian guide. openSUSE uses Btrfs by default, so say so and
point to the Btrfs driver, keeping ext4 as the example.
2026-09-26 20:52:15 -07:00
alex 3f0c124b22 docs(setup): install efifs on Alpine with apk add
apk has no install command, so the command in the Alpine guide failed.
2026-09-26 20:52:15 -07:00
alex ebad8342ee docs(setup): copy Fedora ARM64 GRUB modules to the arm64-efi directory
The ARM64 step copied the arm64-efi GRUB modules into
/boot/grub2/x86_64-efi, where GRUB on ARM64 doesn't look for them, so
insmod chain and insmod fat failed.
2026-09-26 20:52:15 -07:00
alex f293bc50a8 docs(setup): pass the disk and partition number to efibootmgr
The unsigned setup guides passed the EFI System Partition device to
efibootmgr -d, which expects the disk, and left out -p, which defaults
to partition 1. The command failed, or created a boot entry for the
wrong partition when the ESP isn't the first one. Pass the disk and the
partition number, as the signed guides do.
2026-09-26 20:52:15 -07:00
alex 4e08a078d9 docs(contributing): open work in progress as a draft pull request
GitHub doesn't mark a pull request as a draft when its title starts
with WIP:, so contributors following the guide got a normal pull
request. Say to open a draft pull request instead.
2026-09-26 20:52:15 -07:00
alex b67baf5a65 fix(github): remove the team from issue template assignees
Issue forms can only assign users, so assigning edera-dev/engineering
did nothing. Remove it rather than leave an assignment that never
happens.
2026-09-26 20:52:15 -07:00
alex c9586ff68d fix(hack): use the host CPU model for accelerated aarch64 VMs
The aarch64 dev VM always used -cpu cortex-a57, even with -accel hvf or
-accel kvm. QEMU rejects named CPU models with hvf, and with KVM unless
the host has that CPU, so booting natively on Apple Silicon or an
aarch64 Linux host with KVM failed. Use -cpu host when accelerated.
2026-09-26 20:52:15 -07:00
alex 592d7b41f6 fix(hack): don't override QEMU_ACCEL on macOS
On macOS, common.sh always set QEMU_ACCEL to hvf when it was available,
replacing any value the user set, like tcg. Only pick an accelerator
when QEMU_ACCEL isn't set, as the KVM check already does.
2026-09-26 20:52:15 -07:00
alex 406ad8dede fix(hack): format every shell script in hack
format.sh ran shfmt on hack/**/*.sh without enabling globstar, so ** only
matched a single directory. Scripts directly in hack and in deeper
directories like hack/dev/kernel were never formatted.
2026-09-26 20:52:15 -07:00
alex 2b8113f103 fix(hack): build the requested profile in the dev Docker build
The dev build passed RUST_TARGET_SUBDIR to the Docker build, but not
RUST_PROFILE, so the Dockerfile always built the release profile and
then copied from the directory of the requested profile. Any profile
other than release, like ./hack/dev/boot.sh x86_64 debug, failed. Pass
the profile as well.
2026-09-26 20:52:15 -07:00
alex 358cf92f65 fix(hack): point the dev Edera config at the kernel in the dev image
The dev Edera config loaded the kernel from \EFI\BOOT\kernel.efi, but
the dev image copies the kernel to \VMLINUZ, so booting with
SPROUT_CONFIG_NAME=edera always failed to read the kernel.
2026-09-26 20:52:15 -07:00
alex fe3de401ff fix(ci): don't cancel a release that is already running
Dispatching the release workflow again for the same tag cancelled the
running release, which could stop it in the middle of uploading assets
and leave the release with only some of them. Queue the new run behind
the running one instead.
2026-09-26 20:52:15 -07:00
alex 2e643d8081 fix(ci): cancel superseded pull request runs
The concurrency groups included the commit SHA, so every run was in a
group of its own and cancel-in-progress never cancelled a run that a
newer push had replaced. Group runs by pull request or ref instead.
Only pull request runs are cancelled, so every push to main still gets
fully checked and published.
2026-09-26 20:52:15 -07:00
alex cf1ea65414 fix(ci): don't give pull request builds attestation permissions
The publish job ran for pull requests with contents: write, id-token:
write, and attestations: write, while running build code from the pull
request. None of these were needed there: uploading artifacts doesn't
need contents: write, and attestation was already skipped for pull
requests. Build with read-only permissions, and attest in a separate
job that only runs on pushes to main.
2026-09-26 20:52:15 -07:00
alex 5b10fe9576 fix(ci): build releases from the release tag
The release workflow checked out the ref it was dispatched on, usually
main, instead of the release tag. The published binaries and their
provenance attestation could come from a different commit than the tag
they were attached to. Check out the tag instead.
2026-09-26 20:52:15 -07:00
alex 8f09c94857 docs(autoconfigure): correct the comment on Linux scan locations
The comment said symlinked scan locations are skipped, but nothing
checks for symlinks, and that the empty string is the root of the
filesystem, while the root location is "\\".
2026-09-26 20:52:15 -07:00
alex f6064d8751 fix(config): warn about unknown keys in the configuration
Keys that aren't part of the configuration were silently ignored, so a
misspelled key like linux_initrd instead of linux-initrd, or
menu_timeout instead of menu-timeout, quietly did nothing. Warn about
each ignored key with its full path while loading the configuration.

Also warn when an action or generator configures more than one kind,
as only the first one found is used.
2026-09-26 20:52:15 -07:00
alex 0d2eaa37c1 fix(media-loader): treat any non-zero boot policy as true
LoadFile2 compared the boot policy to Boolean::TRUE, which only matches
the value 1. UEFI treats any non-zero BOOLEAN as TRUE, so check it the
same way.
2026-09-26 20:52:15 -07:00
alex 9595fd53aa fix(shim): let the firmware decide on images the shim can't verify
The security hooks returned their own errors for calls the shim can't
verify, instead of passing them to the original firmware hooks. The
Security2 hook rejected a missing device path or file buffer, which are
optional in the PI specification, and a boot policy of TRUE. The
Security hook returned NOT_FOUND for any file it couldn't read, like
one in a firmware volume.

Pass these calls to the original hook, which is what the firmware does
without the hook installed. This matches systemd-boot, and the
firmware still enforces its own verification, so nothing is let
through that the firmware would reject.
2026-09-26 20:52:15 -07:00
alex 079bf0c3e8 fix(path): tell device and file path nodes apart by their type
Device path nodes were treated as device nodes when their text
contained "(", and as file path nodes otherwise. A file name with
parentheses, like \EFI\Linux\vmlinuz(1).efi, was then treated as a
device node, so it couldn't be resolved or loaded. Check whether each
node is a media file path node instead.
2026-09-26 20:52:15 -07:00
alex 8447861985 fix(chainload): unload the image if it fails before being started
If anything failed between loading the image and starting it, like
reading an initrd, registering the media loader, or setting the load
options, the loaded image was left in memory. Unload it in that case.
Once the image is started, the firmware unloads it when it exits.
2026-09-26 20:52:15 -07:00
alex 5dcfbdc8c5 fix(chainload): don't register an empty initrd
An initrd file, or chain of initrd files, with no contents was still
registered with the initrd media loader, which reports NOT_FOUND for
empty data. The Linux EFI stub treats anything other than
BUFFER_TOO_SMALL as a load error and fails to boot, rather than booting
without an initrd. Skip registering an empty initrd, as the edera
action already does.
2026-09-26 20:52:15 -07:00
alex a77803dfe8 fix(edera): stamp initrd paths before skipping empty ones
The edera action checked whether an initrd path was empty before
stamping it, so a path like "$initrd" that stamped to an empty value
was still read, which failed and aborted boot. Stamp the paths first,
and skip paths that stamp to the root of a filesystem, the same as the
chainload action.
2026-09-26 20:52:15 -07:00
alex e96a5a64c9 fix(initrd): pad each chained initrd to a 4-byte boundary
Chained initrds were concatenated back to back. Linux only finds a cpio
archive at a 4-byte aligned offset when it unpacks concatenated
initrds, so a compressed initramfs with an odd length followed by
another archive, like a microcode or overlay cpio, failed to unpack.
Pad each initrd with zeros to a 4-byte boundary, as GRUB and
systemd-boot do.
2026-09-26 20:52:15 -07:00
alex cd38f0271d fix(main): skip entries whose context can't be finalized
A value that refers to itself, such as a BLS entry with "options $options",
never stops changing when the context is finalized. After the iteration
limit, finalizing failed and every entry was lost, although the BLS
generator already skips broken entries. Skip just that entry with a
warning.

A value that refers to itself twice doubles in length every iteration,
so it could use up all memory before the limit was reached. Fail once a
value grows past 64 KiB.
2026-09-26 20:52:15 -07:00
alex aed9903180 fix(extractors): skip filesystems that can't be checked for a match
filesystem-device-match failed as soon as any filesystem couldn't be
checked, such as a removable device without media or a filesystem that
couldn't be opened exclusively. The fallback value was never used and
boot was aborted. Log a warning and check the next filesystem instead.

The filesystem protocol is now only opened when the label or item
criteria need it, and the partition UUIDs are parsed once up front.
2026-09-26 20:52:15 -07:00
alex 642236edec fix(autoconfigure): skip filesystems and directories that can't be scanned
Commit 2da1cea skipped filesystems that couldn't be opened, but an error
while scanning an opened filesystem, such as a driver returning
DEVICE_ERROR or ACCESS_DENIED, still aborted autoconfiguration for every
filesystem. Log a warning and move on to the next scan instead, and do
the same for each directory scanned for Linux kernels.
2026-09-26 20:52:15 -07:00
alex b4e765279b fix(autoconfigure): don't treat an empty BLS entries directory as BLS
BLS was detected when reading \loader\entries produced any item, but a
FAT directory always lists "." first, so an empty entries directory
counted as having entries. On an ESP with a leftover empty entries
directory, Linux autoconfiguration was skipped and no Linux entries
were shown. Only count the directory when it has a .conf file.
2026-09-26 20:52:15 -07:00
alex 133247c05a fix(autoconfigure): only pair kernels with an initramfs that is a file
The initramfs candidates were checked with try_exists, which also
accepts a directory, and fails the whole scan on any error other than
the file not being found. Only accept a regular file, and treat a
candidate that can't be checked as missing.
2026-09-26 20:52:15 -07:00
alex 9438c99c38 fix(autoconfigure): pair kernels with initramfs files ending in .img
Linux autoconfiguration only looked for an initramfs named with the
kernel suffix, like initrd.img-6.1 or initramfs-lts. Arch Linux
(initramfs-linux.img), and Fedora or Gentoo using dracut without BLS
(initramfs-<version>.img) add .img to the name, so their kernels were
booted without an initramfs and failed to mount the root filesystem.
Also try each candidate with .img added.
2026-09-26 20:52:15 -07:00
alex c54a597917 fix(bls): ignore a byte order mark at the start of an entry
A UTF-8 byte order mark isn't whitespace, so it became part of the first
key of the entry, and that line was ignored. Entries written by editors
that add one lost their title or kernel. Strip it before parsing.
2026-09-26 20:52:15 -07:00
alex c898a1499c fix(bls): combine the options of every options line
The Boot Loader Specification allows the options key to appear more
than once, with all of the options combined in order. Each options line
replaced the previous one instead, so an entry that set root= on one
line and other options on the next booted without its root filesystem.
2026-09-26 20:52:15 -07:00
alex 85f35a9a3e fix(config): make the default configuration match an empty one
RootConfiguration derived Default, which set menu-timeout to 0 and the
version to 0, while an empty configuration file gets a 10 second
timeout and the latest version. --autoconfigure uses the default
configuration, so it booted the first entry without showing the menu.
Implement Default with the same values serde uses.
2026-09-26 20:52:15 -07:00
alex ac69037111 fix(entries): stamp the sort key of entries
The sort-key of an entry was compared as written, without stamping the
values into it. Autoconfigured Linux entries all use "$kernel" as the
sort key, so they compared equal and kept the order the filesystem
listed them in, which made the default kernel arbitrary instead of the
newest one. Stamp the sort key along with the title.
2026-09-26 20:52:15 -07:00
alex 9fa57ee197 fix(bootloader-interface): don't fail boot when a variable can't be used
Every Boot Loader Interface variable that couldn't be read, written, or
removed aborted boot. The interface only exchanges information with the
operating system, so log a warning and continue instead. This includes
failing to remove LoaderEntryOneShot or LoaderConfigTimeoutOneShot,
which made every following boot fail the same way, as the variable was
never removed.
2026-09-26 20:52:15 -07:00
alex 8fce1738f2 fix(main): use the boot menu when the entry to boot can't be found
An entry from --boot or LoaderEntryOneShot that didn't match any entry
aborted boot, which happens when the one-shot entry was removed or
misspelled. Log a warning and use the boot menu instead.
2026-09-26 20:52:15 -07:00
alex 20ca279fca fix(main): ignore a LoaderEntryDefault that matches no entry
When LoaderEntryDefault was set, every entry that didn't match it was
unmarked as the default, including the one set by default-entry. If the
variable named an entry that no longer exists, such as a removed kernel,
the configured default was lost and the first entry booted instead.
Only apply the variable when some entry matches it.
2026-09-26 20:52:15 -07:00
alex 2b5cc4e70f fix(bootloader-interface): show the menu for LoaderConfigTimeoutOneShot
LoaderConfigTimeoutOneShot asks the boot loader to show the menu on the
next boot, and a value of 0 means the menu waits for the user. Sprout
treated it like LoaderConfigTimeout, so 0 hid the menu, the opposite of
what systemctl reboot --boot-loader-menu=0 asks for, and other values
only changed the timeout without forcing the menu.

Force the menu when the one-shot timeout is set, waiting for the user
when it is 0.
2026-09-26 20:52:15 -07:00
alex e8e3361764 fix(menu): show the boot menu when it is forced
--force-menu and LoaderConfigTimeout=menu-force only stopped --boot and
LoaderEntryOneShot from skipping the menu. The timeout was left as is,
so with a zero timeout the menu booted the default entry without being
drawn, and menu-force counted down and booted the default entry, where
the Boot Loader Interface says it should wait for the user.

Menus now take an optional timeout. menu-force waits for the user, and
--force-menu keeps the configured timeout unless it is zero, in which
case it waits for the user too.
2026-09-26 20:52:15 -07:00
alex df0bda04c0 fix(menu): let the basic menu select entries numbered 10 and above
The basic menu listed entries as [10], [11] and so on, but only read a
single digit, so those entries could never be selected.

Keep reading digits while another digit could still select a different
entry, and boot once it can't or when enter is pressed. With ten
entries or fewer, a single digit boots the entry right away as before.
2026-09-26 20:52:15 -07:00
alex 9e595bf8d5 fix(menu): stop the basic menu countdown on a key press
Every key that didn't select an entry, like enter or an arrow key, made
the basic menu wait for the full timeout again. Stop the countdown on
any key instead, like the simple menu, so the menu waits for the user
once they start using it.
2026-09-26 20:52:15 -07:00
alex 78bcaa79ff fix(menu): keep the countdown going after a spurious key event
Some firmware signals the key event without a key being available. The
simple menu then skipped the countdown for that tick, and the basic menu
started waiting for the full timeout again, so firmware that kept
signaling the key event would never boot the default entry.

Read the key inside the wait, and when no key is available keep waiting
on the same timer.
2026-09-26 20:52:15 -07:00
alex 11736ccabb fix(menu): don't panic on very large timeouts
The basic menu set a single timer for the whole timeout, and converting
a timeout of more than about 1.8e12 seconds into 100ns timer units
panics. The timeout can come from --menu-timeout or from
LoaderConfigTimeout, which persists, so the panic would repeat on every
boot. Limit a single timer to u32::MAX seconds.
2026-09-26 20:52:15 -07:00
alex 98f1a2c8c5 fix(tpm): treat a full event log as a successful measurement
The TCG2 specification says HashLogExtendEvent returns VOLUME_FULL when
the PCR was extended but the event log had no room for the event. This
was treated as a failure, which aborted loading the configuration and
booting Edera. Log a warning and continue instead.
2026-09-26 20:52:15 -07:00
alex 3490ec24f2 fix(tpm): don't fail boot when the active PCR banks can't be read
Any error opening the TCG2 protocol, reading its capability, or calling
GetActivePcrBanks aborted boot, although the value is only reported in
LoaderTpm2ActivePcrBanks. TPM firmware bugs are common, so log a
warning and report zero banks instead, as systemd-boot does.
2026-09-26 20:52:15 -07:00
alex 69fd8a0064 fix(timer): don't panic when the timer frequency is zero
PlatformTimer::start panicked if the timer frequency was zero. On
aarch64 the frequency comes from CNTFRQ_EL0, which some firmware leaves
unprogrammed, and on x86_64 a Stall that returns immediately measures
zero. The timer is only used for bootloader interface timestamps, so
log a warning and report zero durations instead.
2026-09-26 20:52:15 -07:00
alex afbba2c6d3 fix(options): ignore arguments that can't be parsed
An unknown option, an unexpected positional argument, or an invalid
option value made Sprout exit with ABORTED, so a typo in a boot entry or
junk passed by the firmware made the machine unbootable.

Log a warning and drop the argument the parser rejected, then parse the
rest again. Invalid --menu-timeout and --menu-style values are ignored
with a warning too, which also replaces the misleading "invalid integer"
error for an unknown menu style.
2026-09-26 20:52:15 -07:00
alex 8b94ebe5bd fix(env): decode load options that aren't a clean UCS-2 string
Load options were decoded with CStr16::from_u16_with_nul, which needs an
even byte count and exactly one null at the very end. Firmware that
passed an odd size, left off the null terminator, or padded the string
with extra nulls made argument parsing fail, and Sprout refused to boot.

Decode the raw bytes as little-endian UTF-16 up to the first null
instead, replacing anything that isn't valid.
2026-09-26 20:52:15 -07:00
alex dd8ae14da8 feat(initrd): support chaining multiple initrds (#101)
Add linux-initrd-chain to the chainload action and initrd-chain to the
edera action. Each file in the list is read in order and concatenated
into a single initrd. The chain can't be combined with the single
initrd option on the same action.

BLS entries with multiple initrds, either as repeated initrd lines or
as multiple paths on one line, previously only loaded the last one.
Parse all of them and set them in the initrd-0 through initrd-7 slots,
which the autoconfigured BLS action now chains. Unused slots stamp to
the filesystem root and are skipped. Entries with more than 8 initrds
are skipped with a warning.
2026-09-26 19:45:47 -07:00
alex 7b62d414ed feat(menu): add a simple boot menu similar to systemd-boot (#100)
The boot menu is now a BootMenu trait with two styles. The basic menu is
the existing one that prints entries and selects them by number. The new
simple menu is full-screen: entries are centered and highlighted, the
arrow keys, home, end, page up and page down move the selection, enter
boots it, and any key stops the countdown.

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


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

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

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


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

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

Signed-off-by: dependabot[bot] <[email protected]>
2025-10-27 12:48:29 +00:00
alex 2a76e4f798 chore(code): move bls autoconfigure to separate module 2025-10-27 04:28:25 -04:00
alex a10a5cb342 sprout: version 0.0.11 2025-10-27 04:00:37 -04:00
alex fdc5f0e1d2 chore(docs): mention autoconfiguration support for bls and use it in the docs 2025-10-27 03:52:39 -04:00
alex f60cf4b365 Merge pull request #18 from edera-dev/azenla/autoconfigure
Autoconfiguration Support
2025-10-27 03:48:51 -04:00
alex 0ca9ff4fec fix(bls-autoconfigure): generate a unique chainload action for each filesystem 2025-10-27 03:45:10 -04:00
alex 1799419bfa fix(autoconfigure): apply the actions properly in the root 2025-10-27 03:37:09 -04:00
alex facd2000a5 feat(autoconfigure): initial attempt at bls autoconfiguration 2025-10-27 02:38:40 -04:00
alex 7dd910a74f sprout: version 0.0.10 2025-10-27 00:53:35 -04:00
alex 187a84fcf8 fix(bls): entries path should be constructed from the subpath, not the device path 2025-10-27 00:50:17 -04:00
alex e47c813536 fix(bls): swap from intaking \\loader\\entries to just \\loader
This prepares for further BLS integration.
2025-10-27 00:31:07 -04:00
alex 094128de58 fix(log): swap everything to use logging 2025-10-27 00:21:24 -04:00
alex e8a4fa5053 Revert "fix(log): make all logging debug! instead of info!"
This reverts commit 717e7716ba.
2025-10-27 00:20:42 -04:00
alex 717e7716ba fix(log): make all logging debug! instead of info! 2025-10-27 00:16:00 -04:00
alex 1d32855d22 Merge pull request #16 from edera-dev/azenla/alpine-edge
chore(docs): alpine edge setup guide
2025-10-27 00:15:28 -04:00
alex 93c7a35c62 Merge pull request #17 from edera-dev/azenla/basic-menu
feat(boot): basic boot menu
2025-10-27 00:04:54 -04:00
alex 8b6317f221 chore(doc): document the basic boot menu as a supported feature 2025-10-27 00:01:05 -04:00
alex 4bbac3e4d5 feat(boot): implement basic boot menu 2025-10-26 23:59:50 -04:00
alex 1f48d26385 chore(docs): alpine edge setup guide 2025-10-26 22:29:30 -04:00
alex 9d2e25183b Merge pull request #15 from edera-dev/azenla/defaults-boot
feat(config): support for setting the default entry to boot
2025-10-24 21:30:03 -07:00
alex 734ff117db feat(config): support for setting the default entry to boot 2025-10-24 21:19:38 -07:00
alex fbebedd66a fix(hack): format should use bash to use glob 2025-10-24 20:09:32 -07:00
alex b3bf564b65 fix(hack): formatting fixes 2025-10-24 20:08:46 -07:00
alex 340c280c00 fix(hack): check kvm with /dev/kvm instead of cpu flags 2025-10-24 20:07:41 -07:00
alex 7a72b7af5b Merge pull request #14 from edera-dev/azenla/fixes
Repair automated bug analysis issues.
2025-10-24 20:00:49 -07:00
alex 0c2303d789 fix(framebuffer): add proper bounds checking for accessing a pixel 2025-10-24 19:54:28 -07:00
alex 6cd502ef18 fix(actions): if edera action returns successfully, an intended unreachable line could be reached 2025-10-24 19:51:08 -07:00
alex e243228f15 fix(framebuffer): check width, height and implement proper checking when accessing pixels 2025-10-24 19:44:26 -07:00
alex 2253fa2a1f fix(context): make sure to actually iterate longest first for key replacement 2025-10-24 19:40:40 -07:00
alex 057c48f9f7 fix(bls): parser should skip over empty lines and comments 2025-10-24 19:31:01 -07:00
alex 45d7cd2d3b fix(doc): incorrect comment for startup phase execution 2025-10-24 19:28:38 -07:00
alex 482db0b763 fix(media-loader): eliminate usage of unwrap and swap to result 2025-10-24 19:27:43 -07:00
alex a15c92a749 fix(context): ensure longer keys are replaced first, fixing key replacement edge case 2025-10-24 19:24:29 -07:00
alex 7d5248e2ee fix(context): skip over empty keys to avoid replacing $ and breaking other values 2025-10-24 19:12:43 -07:00
alex 41fbca6f76 fix(utils): clarify that the to_string().contains() is necessary due to CString16 2025-10-24 19:11:17 -07:00
alex d39fbae168 fix(splash): check for zero-sized images 2025-10-24 19:08:02 -07:00
alex 0b0b4dc19d chore(perf): replace some string replacement and comparison with characters for performance 2025-10-24 18:59:15 -07:00
alex 86fa00928e fix(bls): convert less safe path concatenation to use path buffer 2025-10-24 18:56:11 -07:00
alex 4c7b1d70ef fix(bls): parsing of entries should split by whitespace, not just spaces 2025-10-24 18:51:34 -07:00
alex 9d2c31f77f fix(options): clarify code that checks for --abc=123 option form 2025-10-24 18:49:14 -07:00
alex fc710ec391 fix(options): --help should exit with code zero 2025-10-24 18:47:34 -07:00
alex 9f7ca672ea chore(filesystem-device-match): add clarity to statement which is unreachable 2025-10-24 18:46:45 -07:00
alex 2a2aa74c09 fix(context): add context finalization iteration limit
This prevents any possibility of an infinite loop during finalization.
2025-10-24 18:44:54 -07:00
alex 2e3399f33f chore(deps): upgrade to uefi v0.36.0 2025-10-24 18:34:18 -07:00
alex 160a7737fb sprout: version 0.0.9 2025-10-24 18:17:27 -07:00
alex 68220d0de1 chore(workflows): repin all actions 2025-10-24 18:09:24 -07:00
alex e9b842a81f chore(workflows): separate release workflow 2025-10-24 18:04:55 -07:00
alex efb357d62b chore(workflows): add concurrency limiter 2025-10-24 18:01:50 -07:00
alex 30600f0c81 fix(main): repair contextual replacement for entries 2025-10-24 17:16:42 -07:00
alex e10e98d669 chore(assets): update sprout logo 2025-10-24 16:49:34 -07:00
alex 911b617d92 feat(options): --boot now supports selecting by entry name or index, not just title 2025-10-24 16:32:48 -07:00
alex d3f9e876fb fix(docs): cleanup rustdoc warnings 2025-10-24 15:55:56 -07:00
alex e096f8e236 sprout: version 0.0.8 2025-10-24 14:53:17 -07:00
alex a14686a286 update README with background section 2025-10-24 14:49:11 -07:00
alex 5108b61a15 implement new argument parser with --help support 2025-10-21 19:12:16 -07:00
alex 2aeb0474e6 dev: add support for automatic hypervisor.framework usage on macOS 2025-10-21 05:40:22 -07:00
alex 22c8884f7e booting windows is supported! 2025-10-20 20:35:13 -07:00
alex 3a2b314669 chore(options): move parsing code to a parser module 2025-10-20 19:42:39 -07:00
alex 1171959a52 sprout: version v0.0.7 2025-10-20 19:23:05 -07:00
alex c5ec8dc6a6 add readme information about sprout command line arguments 2025-10-20 18:21:41 -07:00
alex c749c8d38e implement a new sprout command line options mechanism 2025-10-20 18:17:29 -07:00
alex 3d2c31ee1a add a windows setup guide 2025-10-20 13:21:43 -07:00
alex a02ee88afd add a generic linux setup guide 2025-10-20 12:07:22 -07:00
alex b59626888e add bls quirk to support fedora out of the box 2025-10-20 11:33:33 -07:00
alex e3bae1dc63 repair fedora setup guide to include copying grub modules 2025-10-20 11:24:42 -07:00
alex 3cd3491df0 fix bad autocomplete in fedora setup guide 2025-10-20 10:37:28 -07:00
alex e08f6e629f add fedora setup guide 2025-10-20 10:34:55 -07:00
alex 398be12ac4 fix release tag 2025-10-20 09:26:03 -07:00
alex 7407150bff attempt new release mechanism using draft releases and manual job 2025-10-20 09:24:27 -07:00
alex c23b11469d attempt at codeql config for the right target 2025-10-20 09:08:29 -07:00
alex 29529ddacd remove broken codeql configs, will figure this out later 2025-10-20 01:51:20 -07:00
alex d2f47dcad6 attempt to configure rust extractor for the right target 2025-10-20 01:44:27 -07:00
alex f3b7007432 codeql advanced support 2025-10-20 01:29:03 -07:00
alex 4f30d51bb2 make sure rust toolchain is from rust-toolchain.toml in github workflows 2025-10-20 01:18:28 -07:00
alex 48e3644977 fix pull request actions 2025-10-20 01:14:08 -07:00
alex 345e1c800c Merge pull request #11 from edera-dev/dependabot/cargo/cargo-updates-d94e05c250
Bump toml from 0.9.7 to 0.9.8 in the cargo-updates group
2025-10-20 00:33:30 -07:00
alex e5ae612398 Merge pull request #10 from edera-dev/dependabot/docker/docker-updates-d0b0844295
Bump rustlang/rust from `b8107fa` to `141e9a7` in the docker-updates group
2025-10-20 00:31:54 -07:00
dependabot[bot] f0427faab2 Bump toml from 0.9.7 to 0.9.8 in the cargo-updates group
Bumps the cargo-updates group with 1 update: [toml](https://github.com/toml-rs/toml).


Updates `toml` from 0.9.7 to 0.9.8
- [Commits](https://github.com/toml-rs/toml/compare/toml-v0.9.7...toml-v0.9.8)

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

Signed-off-by: dependabot[bot] <[email protected]>
2025-10-20 07:29:29 +00:00
dependabot[bot] 6278274288 Bump rustlang/rust in the docker-updates group
Bumps the docker-updates group with 1 update: rustlang/rust.


Updates `rustlang/rust` from `b8107fa` to `141e9a7`

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

Signed-off-by: dependabot[bot] <[email protected]>
2025-10-20 07:29:15 +00:00
alex 4cd27a5010 add docker to dependabot 2025-10-20 00:17:49 -07:00
alex ca23b93071 sprout: version 0.0.4 2025-10-20 00:09:53 -07:00
alex 3453826e9d document ( by hand :( ) all of the code of sprout 2025-10-20 00:06:46 -07:00
alex 106064d3e7 document by hand much more of the sprout code 2025-10-19 23:03:28 -07:00
alex 8997e417b3 update cargo.toml homepage to https://sprout.edera.dev 2025-10-19 22:16:43 -07:00
alex 111c40534c fix typo in CONTRIBUTING.md 2025-10-19 21:54:44 -07:00
alex 5b1daf256b add basic development guide 2025-10-19 21:53:52 -07:00
alex 08da6dd390 add more documentation to some actions and configurations 2025-10-19 21:44:05 -07:00
149 changed files with 9663 additions and 2063 deletions

No files matched your search

+40
View File
@@ -0,0 +1,40 @@
name: Report a bug
description: File a bug report.
title: "bug: "
labels: ["bug", "triage"]
type: bug
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this bug report!
- type: input
id: version
attributes:
label: Version / Commit
description: What version of Sprout are you running?
validations:
required: true
- type: textarea
id: what-happened
attributes:
label: What happened?
description: Tell us what you expected to happen.
placeholder: Tell us what you see!
value: "A bug happened!"
validations:
required: true
- type: textarea
id: logs
attributes:
label: Log output
description: Please provide any relevant log output.
render: log
- type: checkboxes
id: terms
attributes:
label: Code of Conduct
description: By submitting this report, you agree to follow our [Code of Conduct](https://github.com/edera-dev/sprout/blob/main/CODE_OF_CONDUCT.md).
options:
- label: I agree to follow the Sprout Code of Conduct.
required: true
+6
View File
@@ -0,0 +1,6 @@
blank_issues_enabled: false
contact_links:
- name: Ask a question
url: https://github.com/edera-dev/sprout/discussions
about: Please ask and answer questions here.
# Note that GitHub will automatically display our security policy in the new issue form.
@@ -0,0 +1,27 @@
name: Request a feature
description: Request a feature.
title: "want: "
labels: ["enhancement", "triage"]
type: feature
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out a feature request!
- type: textarea
id: description
attributes:
label: Description
description:
placeholder: Tell us what you want to see!
value: "Your hopes and dreams here!"
validations:
required: true
- type: checkboxes
id: terms
attributes:
label: Code of Conduct
description: By submitting this report, you agree to follow our [Code of Conduct](https://github.com/edera-dev/sprout/blob/main/CODE_OF_CONDUCT.md).
options:
- label: I agree to follow the Sprout Code of Conduct.
required: true
+5
View File
@@ -0,0 +1,5 @@
name: "codeql-config"
extractor-options:
rust:
cargo_target: x86_64-unknown-uefi
@@ -26,3 +26,16 @@ updates:
cargo-dev-updates:
dependency-type: development
applies-to: version-updates
- package-ecosystem: docker
directory: /
schedule:
interval: weekly
cooldown:
default-days: 7
groups:
docker-updates:
dependency-type: production
applies-to: version-updates
docker-dev-updates:
dependency-type: development
applies-to: version-updates
@@ -1,14 +1,21 @@
name: zizmor
on:
push:
branches: ["main"]
pull_request:
branches: ["**"]
branches:
- main
push:
branches:
- main
permissions:
contents: read # Needed to checkout the repository.
concurrency:
group: "${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}"
# Only cancel superseded pull request runs, so every push to main is fully checked.
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
zizmor:
name: zizmor
@@ -19,17 +26,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
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: setup uv
uses: astral-sh/setup-uv@3259c6206f993105e3a61b142c2d97bf4b9ef83d # v7
uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
- name: zizmor
run: uvx zizmor --pedantic --format sarif . > results.sarif
@@ -37,7 +44,7 @@ jobs:
GH_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
- name: upload
uses: github/codeql-action/upload-sarif@16140ae1a102900babc80a33c44059580f687047 # v4
uses: github/codeql-action/upload-sarif@1c5b675653bb5c22dbe9b12b556ec555138e09fd # v4.38.1
with:
sarif_file: results.sarif
category: zizmor
@@ -11,25 +11,29 @@ on:
permissions:
contents: read # Needed to checkout the repository.
concurrency:
group: "${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}"
# Only cancel superseded pull request runs, so every push to main is fully checked.
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
rustfmt:
name: rustfmt
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
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: 'install nightly rust toolchain with rustfmt'
- name: 'install rust toolchain with rustfmt'
run: |
rustup update --no-self-update nightly
rustup default nightly
cargo version
rustup component add rustfmt
- name: 'cargo fmt'
@@ -48,19 +52,18 @@ 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
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: 'install nightly rust toolchain'
- name: 'install rust toolchain'
run: |
rustup update --no-self-update nightly
rustup default nightly
cargo version
- name: cargo build
run: cargo build --target "${TARGET_ARCH}-unknown-uefi"
@@ -78,19 +81,18 @@ 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
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: 'install nightly rust toolchain with clippy'
- name: 'install rust toolchain with clippy'
run: |
rustup update --no-self-update nightly
rustup default stable
cargo version
rustup component add clippy
- name: cargo clippy
+60
View File
@@ -0,0 +1,60 @@
name: codeql
on:
pull_request:
branches:
- main
push:
branches:
- main
schedule:
- cron: '33 16 * * 0'
permissions:
contents: read # Needed to checkout the repository.
concurrency:
group: "${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}"
# Only cancel superseded pull request runs, so every push to main is fully checked.
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
analyze:
name: analyze (${{ matrix.language }})
runs-on: 'ubuntu-latest'
permissions:
security-events: write # Needed to upload results.
packages: read # Needed to fetch internal or private CodeQL packs.
actions: read # Needed to read workflows.
contents: read # Needed to checkout the repository.
strategy:
fail-fast: false
matrix:
include:
- language: actions
build-mode: none
- language: rust
build-mode: none
steps:
- name: harden runner
uses: step-security/harden-runner@e14015d583714f6e62063499dc959a02595150a1 # v2.21.1
with:
egress-policy: audit
- name: checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: initialize codeql
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@1c5b675653bb5c22dbe9b12b556ec555138e09fd # v4.38.1
with:
category: "/language:${{matrix.language}}"
-74
View File
@@ -1,74 +0,0 @@
name: publish
on:
release:
types:
- created
push:
branches:
- main
pull_request:
branches:
- main
paths:
- bin/**
- src/**
- Cargo.*
- rust-toolchain.toml
- .github/workflows/publish.yaml
permissions:
contents: read # Needed to checkout the repository.
jobs:
artifacts:
name: artifacts
permissions:
contents: write # Needed to upload release assets and artifacts.
runs-on: ubuntu-latest
steps:
- name: harden runner
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
with:
egress-policy: audit
- name: checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
with:
persist-credentials: false
- name: 'install nightly rust toolchain'
run: |
rustup update --no-self-update nightly
rustup default nightly
- name: 'assemble artifacts'
run: ./hack/assemble.sh
- name: 'upload sprout-x86_64.efi artifact'
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: sprout-x86_64.efi
path: target/assemble/sprout-x86_64.efi
- name: 'upload sprout-aarch64.efi artifact'
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: sprout-aarch64.efi
path: target/assemble/sprout-aarch64.efi
- name: 'generate cultivator token'
uses: actions/create-github-app-token@67018539274d69449ef7c02e8e71183d1719ab42 # v2.1.4
id: generate-token
with:
app-id: "${{ secrets.EDERA_CULTIVATION_APP_ID }}"
private-key: "${{ secrets.EDERA_CULTIVATION_APP_PRIVATE_KEY }}"
- name: 'upload release artifacts'
run: ./hack/ci/upload-release-assets.sh
env:
GITHUB_TOKEN: "${{ steps.generate-token.outputs.token }}"
RELEASE_TAG: "${{ github.event.release.tag_name }}"
if: ${{ github.event_name == 'release' }}
+72
View File
@@ -0,0 +1,72 @@
name: publish
on:
pull_request:
branches:
- main
push:
branches:
- main
permissions:
contents: read # Needed to checkout the repository.
concurrency:
group: "${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}"
# Only cancel superseded pull request runs, so every push to main is fully checked.
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
artifacts:
name: artifacts
permissions:
contents: read # Needed to checkout the repository.
runs-on: ubuntu-latest
outputs:
artifact-digest: "${{ steps.upload.outputs.artifact-digest }}"
steps:
- name: harden runner
uses: step-security/harden-runner@e14015d583714f6e62063499dc959a02595150a1 # v2.21.1
with:
egress-policy: audit
- name: checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: 'install rust toolchain'
run: |
cargo version
- name: 'assemble artifacts'
run: ./hack/assemble.sh
- name: 'upload artifacts'
id: upload
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: artifacts
path: target/assemble/*
# Attestation runs in its own job so that pull requests, which run the build with
# code from the pull request, never get the permissions needed to attest.
attest:
name: attest
needs: artifacts
if: github.event_name != 'pull_request'
permissions:
id-token: write # Needed for attestation.
attestations: write # Needed for attestations.
runs-on: ubuntu-latest
steps:
- name: harden runner
uses: step-security/harden-runner@e14015d583714f6e62063499dc959a02595150a1 # v2.21.1
with:
egress-policy: audit
- name: 'attest artifacts'
uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2
with:
subject-name: artifacts.zip
subject-digest: "sha256:${{ needs.artifacts.outputs.artifact-digest }}"
+75
View File
@@ -0,0 +1,75 @@
name: release
on:
workflow_dispatch:
inputs:
release-tag:
description: 'Release Tag'
required: true
type: string
permissions:
contents: read # Needed to checkout the repository.
concurrency:
group: "${{ github.workflow }}-${{ github.event.inputs.release-tag }}"
# Let a running release finish, so its assets are never left partially uploaded.
cancel-in-progress: false
jobs:
release:
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@e14015d583714f6e62063499dc959a02595150a1 # v2.21.1
with:
egress-policy: audit
- name: checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
# Build the tagged commit, not whichever ref the workflow was dispatched on.
ref: "refs/tags/${{ github.event.inputs.release-tag }}"
persist-credentials: false
- name: 'install rust toolchain'
run: |
cargo version
- 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@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'
run: ./hack/ci/upload-release-assets.sh
env:
GITHUB_TOKEN: "${{ steps.generate-token.outputs.token }}"
RELEASE_TAG: "${{ github.event.inputs.release-tag }}"
if: ${{ github.event.inputs.release-tag != '' }}
- name: 'mark release as published'
run: gh release edit "${RELEASE_TAG}" --draft=false --verify-tag
env:
GITHUB_TOKEN: "${{ steps.generate-token.outputs.token }}"
RELEASE_TAG: "${{ github.event.inputs.release-tag }}"
if: ${{ github.event.inputs.release-tag != '' }}
+6 -6
View File
@@ -1,8 +1,8 @@
# Contributing to Sprout
Welcome! We're very glad you're reading this; Edera is excited for all kinds of contributions! Please read the following to ensure you're aware of our flow and policies.
Welcome! We're happy you're reading this; Edera is excited for all kinds of contributions! Please read the following to ensure you're aware of our flow and policies.
## Before contributing
## Before Contributing
1. Please read our [Code of Conduct](CODE_OF_CONDUCT.md), which applies to all interactions in/with all Edera projects and venues.
2. Before opening an issue or PR, please try a few searches to see if there is overlap with existing conversations or WIP contributions.
@@ -11,7 +11,7 @@ Welcome! We're very glad you're reading this; Edera is excited for all kinds of
## Contributing Code
To get started with technical contributions, please read out [Development Guide]. If you're looking for something easy to tackle, [look for issues labeled `good first issue`][good-first-issue].
To get started with technical contributions, please read our [Development Guide]. If you're looking for something easy to tackle, [look for issues labeled `good first issue`][good-first-issues].
## Reporting bugs and other issues
@@ -20,13 +20,13 @@ We encourage opening an issue on GitHub to report bugs.
## Pull Requests
1. For anything more than simple bug/doc fixes, please open a GitHub issue for tracking purposes.
- Else skip to step 3.
2. Discuss the change with the teams to ensure we have consensus on the change being welcome.
3. We encourage opening the PR sooner than later, and prefixing with `WIP:` so GitHub labels it as a Draft.
3. We encourage opening the PR sooner than later, as a draft pull request.
4. Please include a detailed list of changes that the PR makes.
5. Once the PR is ready for review, remove the Draft status, and request a review from `edera-dev/engineering`.
6. After the review cycle concludes and we know you are ready for merging, a team member will submit the PR to the merge queue.
6. After the review cycle concludes, and we know you are ready for merging, a team member will submit the PR to the merge queue.
[Code of Conduct]: ./CODE_OF_CONDUCT.md
[Development Guide]: ./DEVELOPMENT.md
[Security Policy]: ./SECURITY.md
[good-first-issues]: https://github.com/edera-dev/sprout/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22
Generated
+192 -175
View File
@@ -2,23 +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 = "autocfg"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
[[package]]
name = "bit_field"
@@ -28,203 +16,203 @@ checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6"
[[package]]
name = "bitflags"
version = "2.9.4"
version = "2.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394"
checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06"
[[package]]
name = "bytemuck"
version = "1.24.0"
name = "block-buffer"
version = "0.12.1"
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 = "cfg-if"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9"
[[package]]
name = "crc32fast"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa"
dependencies = [
"cfg-if",
"hybrid-array",
]
[[package]]
name = "edera-sprout"
version = "0.0.3"
name = "cfg-if"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600"
[[package]]
name = "cpufeatures"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566"
dependencies = [
"libc",
]
[[package]]
name = "crypto-common"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453"
dependencies = [
"hybrid-array",
]
[[package]]
name = "digest"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2"
dependencies = [
"block-buffer",
"crypto-common",
]
[[package]]
name = "edera-sprout-bls"
version = "0.0.30"
dependencies = [
"anyhow",
"image",
]
[[package]]
name = "edera-sprout-boot"
version = "0.0.30"
dependencies = [
"anyhow",
"edera-sprout-bls",
"edera-sprout-build",
"edera-sprout-config",
"edera-sprout-eficore",
"edera-sprout-parsing",
"hex",
"jaarg",
"log",
"serde",
"serde_ignored",
"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.30"
[[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.30"
dependencies = [
"simd-adler32",
"serde",
]
[[package]]
name = "flate2"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04bcaeafafdd3cd1cb5d986ff32096ad1136630207c49b9091e3ae541090d938"
name = "edera-sprout-eficore"
version = "0.0.30"
dependencies = [
"crc32fast",
"miniz_oxide",
"anyhow",
"bitflags",
"log",
"shlex",
"spin",
"uefi",
"uefi-raw",
]
[[package]]
name = "hashbrown"
version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d"
[[package]]
name = "image"
version = "0.25.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "529feb3e6769d234375c4cf1ee2ce713682b8e76538cb13f9fc23e1400a591e7"
name = "edera-sprout-parsing"
version = "0.0.30"
dependencies = [
"bytemuck",
"byteorder-lite",
"moxcms",
"num-traits",
"png",
"hex",
"sha2",
]
[[package]]
name = "indexmap"
version = "2.11.4"
name = "hex"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "hybrid-array"
version = "0.4.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "27f864f10dfb56725ce5ce5472bc52252c8f93a4ab86327122cebf62c5f59a17"
dependencies = [
"equivalent",
"hashbrown",
"typenum",
]
[[package]]
name = "jaarg"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "534d589df1ef528a238f4bc4b1db081a1280f3aedf2695fd8971e9853a7fa4f6"
[[package]]
name = "libc"
version = "0.2.189"
source = "registry+https://github.com/rust-lang/crates.io-index"
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.101"
version = "1.0.107"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de"
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.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83f9b339b02259ada5c0f4a389b7fb472f933aa17ce176fd2ad98f28bb401fde"
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",
@@ -232,18 +220,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",
@@ -251,24 +239,55 @@ dependencies = [
]
[[package]]
name = "serde_spanned"
version = "1.0.2"
name = "serde_ignored"
version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5417783452c2be558477e104686f7de5dae53dba813c28435e0e70f82d9b04ee"
checksum = "115dffd5f3853e06e746965a20dcbae6ee747ae30b543d91b0e089668bb07798"
dependencies = [
"serde",
"serde_core",
]
[[package]]
name = "serde_spanned"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26"
dependencies = [
"serde_core",
]
[[package]]
name = "simd-adler32"
version = "0.3.7"
source = "git+https://github.com/edera-dev/sprout-patched-deps.git?rev=2c4fcc84b50d40c28f540d4271109ea0ca7e1268#2c4fcc84b50d40c28f540d4271109ea0ca7e1268"
name = "sha2"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4"
dependencies = [
"cfg-if",
"cpufeatures",
"digest",
]
[[package]]
name = "shlex"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
[[package]]
name = "spin"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0134f9043ed38b087ac4f7d4af44c79e2c9e5094421fe3164f435ce585953b10"
dependencies = [
"lock_api",
]
[[package]]
name = "syn"
version = "2.0.106"
version = "3.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6"
checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee"
dependencies = [
"proc-macro2",
"quote",
@@ -277,42 +296,40 @@ dependencies = [
[[package]]
name = "toml"
version = "0.9.7"
version = "1.1.6+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00e5e5d9bf2475ac9d4f0d9edab68cc573dc2fd644b0dba36b0c30a92dd9eaa0"
checksum = "920602543f0911ab71da12c50d59701da54c196d1a2bf5cb4b75667f137a406a"
dependencies = [
"indexmap",
"serde_core",
"serde_spanned",
"toml_datetime",
"toml_parser",
"toml_writer",
"winnow",
]
[[package]]
name = "toml_datetime"
version = "0.7.2"
version = "1.1.1+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32f1085dec27c2b6632b04c80b3bb1b4300d6495d1e129693bdda7d91e72eec1"
checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7"
dependencies = [
"serde_core",
]
[[package]]
name = "toml_parser"
version = "1.0.3"
version = "1.1.3+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4cf893c33be71572e0e9aa6dd15e6677937abd686b066eac3f8cd3531688a627"
checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56"
dependencies = [
"winnow",
]
[[package]]
name = "toml_writer"
version = "1.0.3"
name = "typenum"
version = "1.20.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d163a63c116ce562a22cda521fcc4d79152e7aba014456fb5eb442f6d6a10109"
checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
[[package]]
name = "ucs2"
@@ -325,9 +342,9 @@ dependencies = [
[[package]]
name = "uefi"
version = "0.35.0"
version = "0.41.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da7569ceafb898907ff764629bac90ac24ba4203c38c33ef79ee88c74aa35b11"
checksum = "368cef7d94514897e6dad4350945c7910a9f5bf9021191d1c58ee3ec09962697"
dependencies = [
"bitflags",
"cfg-if",
@@ -341,9 +358,9 @@ dependencies = [
[[package]]
name = "uefi-macros"
version = "0.18.1"
version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3dad47b3af8f99116c0f6d4d669c439487d9aaf1c8d9480d686cda6f3a8aa23"
checksum = "643efa8b64be89780bc498b290fab04337c7c4d171cc82cce2e133a74616dcc4"
dependencies = [
"proc-macro2",
"quote",
@@ -352,9 +369,9 @@ dependencies = [
[[package]]
name = "uefi-raw"
version = "0.11.0"
version = "0.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7cad96b8baaf1615d3fdd0f03d04a0b487d857c1b51b19dcbfe05e2e3c447b78"
checksum = "3636327a6d88a38c0f2567fb695ec7ba27b3483d1877730486605aa0866da201"
dependencies = [
"bitflags",
"uguid",
@@ -362,18 +379,18 @@ dependencies = [
[[package]]
name = "uguid"
version = "2.2.0"
version = "2.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab14ea9660d240e7865ce9d54ecdbd1cd9fa5802ae6f4512f093c7907e921533"
checksum = "0c8352f8c05e47892e7eaf13b34abd76a7f4aeaf817b716e88789381927f199c"
[[package]]
name = "unicode-ident"
version = "1.0.19"
version = "1.0.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d"
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"
+61 -36
View File
@@ -1,37 +1,73 @@
[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.3"
homepage = "https://github.com/edera-dev/sprout"
version = "0.0.30"
homepage = "https://sprout.edera.dev"
repository = "https://github.com/edera-dev/sprout"
edition = "2024"
[dependencies]
anyhow = "1.0.100"
toml = "0.9.7"
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.6"
[workspace.dependencies.anyhow]
version = "1.0.104"
default-features = false
features = ["png"]
optional = true
[dependencies.serde]
version = "1.0.228"
features = ["derive"]
[workspace.dependencies.hex]
version = "0.4.3"
default-features = false
features = ["alloc"]
[dependencies.uefi]
version = "0.35.0"
features = ["alloc", "logger"]
[workspace.dependencies.jaarg]
version = "0.2.2"
default-features = false
features = ["alloc"]
[dependencies.uefi-raw]
version = "0.11.0"
[workspace.dependencies.serde]
version = "1.0.229"
default-features = false
features = ["alloc", "derive"]
[features]
default = ["splash"]
splash = ["dep:image"]
[workspace.dependencies.serde_ignored]
version = "0.1.14"
default-features = false
[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]
lto = "thin"
@@ -46,15 +82,4 @@ debug = 1
inherits = "dev"
strip = "debuginfo"
debug = 0
[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"
opt-level = 2
+60
View File
@@ -0,0 +1,60 @@
# Sprout Development Guide
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.
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` at `crates/boot`: Bootloader entrypoint for Sprout.
- `edera-sprout-bls` at `crates/bls`: Parsing and sorting of Boot Loader Specification entries.
- `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.
- `edera-sprout-parsing` at `crates/parsing`: Value stamping and other parsing helpers for Sprout.
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:
### ./hack/build.sh
Builds the Sprout binary for the target that would support your current machine.
### ./hack/assemble.sh
Builds both x86_64 and aarch64 binaries into `target/assemble`.
### ./hack/clean.sh
Cleans the target directory and any docker images that were built.
### ./hack/format.sh
Formats the code using `rustfmt` and shell scripts with `shfmt`.
### ./hack/autofix.sh
Applies Clippy and `rustfmt` fixes to the code, and formats shell scripts with `shfmt`.
## Dev Scripts
### ./hack/dev/build.sh
Build Sprout as OCI images using Docker, including a kernel, initramfs, xen, and other supporting files.
### ./hack/dev/boot.sh
Boot Sprout's dev environment using QEMU for testing. This will let you test your changes in a real environment booting
Alpine Linux with an initramfs.
+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:b8107fa66d3e5ad7f729d3347c7feedbd3f4b60b01006edce39eb6b994ff00bd 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
+89 -36
View File
@@ -1,16 +1,16 @@
<div align="center">
![Sprout Logo](assets/logo.png)
![Sprout Logo](assets/logo-small.png)
# Sprout
</div>
Sprout is an **EXPERIMENTAL** programmable UEFI bootloader written in Rust.
Sprout is a programmable UEFI bootloader written in Rust.
Sprout is in use at Edera today in development environments and is intended to ship to production soon.
It is in use at Edera today in development environments and is intended to ship to production soon.
The name "sprout" is derived from our company name "Edera" which means "ivy."
The name "Sprout" is derived from our company name "Edera" which means "ivy."
Given that Sprout is the first thing intended to start on an Edera system, the name was apt.
It supports `x86_64` and `ARM64` EFI-capable systems. It is designed to require UEFI and can be chainloaded from an
@@ -18,32 +18,79 @@ existing UEFI bootloader or booted by the hardware directly.
Sprout is licensed under Apache 2.0 and is open to modifications and contributions.
## Features
**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).
NOTE: Currently, Sprout is experimental and is not intended for production use. For example, it doesn't currently
have secure boot support. In fact, as of writing, it doesn't even have a boot menu. Instead, it boots the first entry it sees, or fails.
## Background
At [Edera] we make compute isolation technology for a wide variety of environments, often ones we do not fully control.
Our technology uses a hypervisor to boot the host system to provide a new isolation mechanism that works
with or without hardware virtualization support. To do this, we need to inject our hypervisor at boot time.
Unfortunately, GRUB, the most common bootloader on Linux systems today, uses a shell-script like
configuration system. Both the code that runs to generate a GRUB config and the GRUB config
itself is fully turing complete. This makes modifying boot configuration difficult and error-prone.
Sprout was designed to take in a machine-readable, writable, and modifiable configuration that treats boot information
like data plus configuration, and can be chained from both UEFI firmware and GRUB alike.
Sprout aims to be flexible, secure, and modern. Written in Rust, it handles data safely and uses unsafe code as little
as possible. It also critically must be easy to install into all common distributions, relying on simple principles to
simplify installation and usage.
## Documentation
### 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]
- [Code of Conduct]
- [Security Policy]
## Features
### Current
- [x] Loadable driver support
- [x] [Bootloader specification (BLS)](https://uapi-group.org/specifications/specs/boot_loader_specification/) support
- [x] Basic [Bootloader specification (BLS)](https://uapi-group.org/specifications/specs/boot_loader_specification/) support
- [x] Chainload support
- [x] Linux boot support via EFI stub
- [x] Windows boot support via chainload
- [x] Load Linux initrd from disk
- [x] Boot first configured entry
- [x] Basic boot menu
- [x] BLS autoconfiguration support
- [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
- [ ] Boot menu
- [ ] Secure Boot support: work in progress
- [ ] UKI support: partial
- [ ] Windows boot support (untested via chainload)
- [ ] multiboot2 support
- [ ] Linux boot protocol (boot without EFI stub)
- [ ] [Full-featured boot menu](https://github.com/edera-dev/sprout/issues/1)
- [ ] [UKI support](https://github.com/edera-dev/sprout/issues/6): partial
- [ ] [multiboot2 support](https://github.com/edera-dev/sprout/issues/7)
- [ ] [Linux boot protocol (boot without EFI stub)](https://github.com/edera-dev/sprout/issues/8)
## Concepts
- drivers: loadable EFI modules that can add functionality to the EFI system.
- autoconfiguration: code that can automatically generate sprout.toml based on the EFI environment.
- actions: executable code with a configuration that can be run by various other sprout concepts.
- generators: code that can generate boot entries based on inputs or runtime code.
- extractors: code that can extract values from the EFI environment.
@@ -62,6 +109,21 @@ See [Configuration](#configuration) for how to configure sprout.
Sprout is configured using a TOML file at `\sprout.toml` on the root of the EFI partition sprout was booted from.
### Command Line Options
Sprout supports some command line options that can be combined to modify behavior without the configuration file.
```bash
# Boot Sprout with a specific configuration file.
$ sprout.efi --config=\path\to\config.toml
# Boot a specific entry, bypassing the menu.
$ sprout.efi --boot="Boot Xen"
# Autoconfigure Sprout, without loading a configuration file.
$ sprout.efi --autoconfigure
# Use the basic boot menu instead of the simple one.
$ sprout.efi --menu-style=basic
```
### Boot Linux from ESP
```toml
@@ -94,25 +156,16 @@ version = 1
[drivers.ext4]
path = "\\sprout\\drivers\\ext4.efi"
# extract the full path of the first filesystem
# that contains \loader\entries as a directory
# into the value called "boot"
[extractors.boot.filesystem-device-match]
has-item = "\\loader\\entries"
# use the sprout bls module to scan a bls
# directory for entries and load them as boot
# entries in sprout, using the entry template
# as specified here. the bls action below will
# be passed the extracted values from bls.
[generators.boot.bls]
path = "$boot\\loader\\entries"
entry.title = "$title"
entry.actions = ["bls"]
# the action that is used for each bls entry above.
[actions.bls]
chainload.path = "$boot\\$chainload"
chainload.options = ["$options"]
chainload.linux-initrd = "$boot\\$initrd"
# global options.
[options]
# enable autoconfiguration by detecting bls enabled
# filesystems and generating boot entries for them.
autoconfigure = true
```
[Edera]: https://edera.dev
[Development Guide]: ./DEVELOPMENT.md
[Contributing Guide]: ./CONTRIBUTING.md
[Sprout License]: ./LICENSE
[Code of Conduct]: ./CODE_OF_CONDUCT.md
[Security Policy]: ./SECURITY.md
Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

+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"
+842
View File
@@ -0,0 +1,842 @@
#![no_std]
extern crate alloc;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
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 paths to the initrds, in the order they appear.
pub initrd: Vec<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: Vec<String> = Vec::new();
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;
// Some editors write a UTF-8 byte order mark at the start of the file.
// It isn't whitespace, so it would otherwise be part of the first key.
let input = input.strip_prefix('\u{feff}').unwrap_or(input);
// 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. The options key can appear more than once,
// and all the options are combined in the order they appear.
"options" => {
let value = value.trim();
match options.as_mut() {
Some(options) => {
options.push(' ');
options.push_str(value);
}
None => options = Some(value.to_string()),
}
}
// The path to the linux kernel.
"linux" => {
linux = Some(value.trim().to_string());
}
// The path to an initrd. An entry can have multiple initrds, either as
// repeated initrd lines or as multiple paths separated by whitespace.
"initrd" => {
initrd.extend(value.split_whitespace().map(|path| path.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 paths to the initrds to pass to the kernel, in order.
/// It also converts / to \\ to match EFI path style.
pub fn initrd_paths(&self) -> Vec<String> {
self.initrd
.iter()
.map(|path| path.replace('/', "\\").trim_start_matches('\\').to_string())
.collect()
}
/// 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_empty());
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, ["/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 parse_multiple_initrd_lines_in_order() {
let input = "linux /vmlinuz\ninitrd /intel-ucode.img\ninitrd /initramfs.img\n";
let entry: BlsEntry = input.parse().unwrap();
assert_eq!(entry.initrd, ["/intel-ucode.img", "/initramfs.img"]);
}
#[test]
fn parse_multiple_initrds_on_one_line() {
let input = "linux /vmlinuz\ninitrd /intel-ucode.img /initramfs.img\n";
let entry: BlsEntry = input.parse().unwrap();
assert_eq!(entry.initrd, ["/intel-ucode.img", "/initramfs.img"]);
}
#[test]
fn parse_multiple_options_lines_combined_in_order() {
let input = "linux /vmlinuz\noptions root=/dev/sda1 ro\noptions quiet\n";
let entry: BlsEntry = input.parse().unwrap();
assert_eq!(entry.options().as_deref(), Some("root=/dev/sda1 ro quiet"));
}
#[test]
fn parse_ignores_byte_order_mark() {
let entry: BlsEntry = "\u{feff}title Linux\nlinux /vmlinuz\n".parse().unwrap();
assert_eq!(entry.title().as_deref(), Some("Linux"));
}
#[test]
fn initrd_paths_normalises_slashes() {
let entry: BlsEntry = "linux /vmlinuz\ninitrd /boot/initrd.img\n".parse().unwrap();
assert_eq!(entry.initrd_paths(), ["boot\\initrd.img"]);
}
#[test]
fn initrd_paths_empty_when_not_set() {
let entry: BlsEntry = "linux /vmlinuz\n".parse().unwrap();
assert!(entry.initrd_paths().is_empty());
}
#[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
);
}
}
+31
View File
@@ -0,0 +1,31 @@
[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
serde_ignored.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();
}
+42
View File
@@ -0,0 +1,42 @@
use crate::context::SproutContext;
use alloc::rc::Rc;
use anyhow::{Context, Result, bail};
/// EFI chainloader action.
pub mod chainload;
/// Edera hypervisor action.
pub mod edera;
/// EFI console print action.
pub mod print;
/// 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
/// that does not return control to sprout.
pub fn execute(context: Rc<SproutContext>, name: impl AsRef<str>) -> Result<()> {
// Retrieve the action from the root context.
let Some(action) = context.root().actions().get(name.as_ref()) else {
bail!("unknown action '{}'", name.as_ref());
};
// Finalize the context and freeze it.
let context = context
.finalize()
.context("unable to finalize context")?
.freeze();
// Execute the action.
if let Some(chainload) = &action.chainload {
chainload::chainload(context.clone(), chainload)?;
return Ok(());
} else if let Some(print) = &action.print {
print::print(context.clone(), print)?;
return Ok(());
} else if let Some(edera) = &action.edera {
edera::edera(context.clone(), edera)?;
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");
}
+180
View File
@@ -0,0 +1,180 @@
use crate::context::SproutContext;
use crate::phases::before_handoff;
use alloc::boxed::Box;
use alloc::rc::Rc;
use alloc::vec::Vec;
use anyhow::{Context, Result, bail};
use edera_sprout_config::actions::chainload::ChainloadConfiguration;
use edera_sprout_parsing::{append_initrd, 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 log::warn;
use uefi::proto::loaded_image::LoadedImage;
use uefi::{CString16, Handle};
/// Read the initrd at the stamped `path` relative to the sprout image.
/// Provides [None] if the path refers to the root of a filesystem rather than a file.
pub fn read_initrd(context: &Rc<SproutContext>, path: &str) -> Result<Option<Vec<u8>>> {
let resolved = eficore::path::resolve_path(Some(context.root().loaded_image_path()?), path)
.context("unable to resolve 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-0" is stamped with an empty
// initrd value, such as a BLS entry without an initrd or an unused BLS initrd slot.
let subpath = eficore::path::device_path_subpath(&resolved.full_path)
.context("unable to get initrd subpath")?;
if subpath.trim_matches('\\').is_empty() {
return Ok(None);
}
let content = resolved.read_file().context("unable to read initrd")?;
Ok(Some(content))
}
/// Unloads the image with the contained handle when dropped, unless the handle is taken.
/// This ensures that an image that is loaded but never started is not left in memory.
struct UnloadGuard(Option<Handle>);
impl Drop for UnloadGuard {
fn drop(&mut self) {
if let Some(handle) = self.0.take()
&& let Err(error) = uefi::boot::unload_image(handle)
{
warn!("unable to unload image: {}", error);
}
}
}
/// Executes the chainload action using the specified `configuration` inside the provided `context`.
pub fn chainload(context: Rc<SproutContext>, configuration: &ChainloadConfiguration) -> Result<()> {
// The initrd can be provided as either a single initrd or a chain, but not both.
if configuration.linux_initrd.is_some() && !configuration.linux_initrd_chain.is_empty() {
bail!("linux-initrd and linux-initrd-chain cannot be used together");
}
// 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)?;
// Unload the image if an error occurs before it is started.
let mut unload_guard = UnloadGuard(Some(*image.handle()));
// 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 paths. A single linux-initrd is read the same way as a chain of one.
let initrd_paths = configuration
.linux_initrd
.iter()
.chain(configuration.linux_initrd_chain.iter())
.map(|item| context.stamp(item));
// Read each initrd and concatenate the contents in order.
// Paths that are empty after stamping are skipped.
let mut initrd: Option<Vec<u8>> = None;
for path in initrd_paths {
let Some(path) = empty_is_none(Some(path)) else {
continue;
};
let Some(content) = read_initrd(&context, &path)? else {
continue;
};
match initrd.as_mut() {
Some(initrd) => append_initrd(initrd, &content),
None => initrd = Some(content),
}
}
// If an initrd was read, register it with the EFI stack.
// An empty initrd is not registered, as Linux fails to boot if the initrd it is
// given can't be loaded, instead of booting without one.
let mut initrd_handle = None;
if let Some(content) = initrd.filter(|content| !content.is_empty()) {
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.
// This is only informational, so it should not prevent booting.
if let Err(error) = BootloaderInterface::mark_exec(context.root().timer()) {
warn!(
"unable to mark execution of boot entry in bootloader interface: {:#}",
error
);
}
// 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")?;
// The image is about to be started, so it is no longer ours to unload.
// Once started, the image is unloaded by the firmware when it exits.
unload_guard.0 = None;
// 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(())
}
+143
View File
@@ -0,0 +1,143 @@
use crate::{actions, context::SproutContext};
use alloc::rc::Rc;
use alloc::vec::Vec;
use alloc::{format, vec};
use anyhow::{Context, Result, bail};
use edera_sprout_config::actions::chainload::ChainloadConfiguration;
use edera_sprout_config::actions::edera::EderaConfiguration;
use edera_sprout_parsing::{append_initrd, 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<()> {
// The initrd can be provided as either a single initrd or a chain, but not both.
if configuration.initrd.is_some() && !configuration.initrd_chain.is_empty() {
bail!("initrd and initrd-chain cannot be used together");
}
// 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")?;
// Read the initrd, or each initrd in the chain concatenated in order, then
// extend PCR 9 with the combined bytes (empty when no initrd is configured).
// Paths are stamped first, and paths that are empty after stamping or that refer
// to the root of a filesystem are skipped, the same as the chainload action.
let mut initrd_bytes = Vec::new();
let initrd_paths = configuration
.initrd
.iter()
.chain(configuration.initrd_chain.iter())
.map(|item| context.stamp(item));
for path in initrd_paths {
let Some(path) = empty_is_none(Some(path)) else {
continue;
};
let Some(content) = actions::chainload::read_initrd(&context, &path)
.context("unable to read initrd file")?
else {
continue;
};
append_initrd(&mut initrd_bytes, &content);
}
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,
linux_initrd_chain: vec![],
},
)
.context("unable to chainload to xen");
// Explicitly drop the media loaders to clarify when they should be unregistered.
drop(media_loaders);
result
}
+11
View File
@@ -0,0 +1,11 @@
use crate::context::SproutContext;
use alloc::rc::Rc;
use anyhow::Result;
use edera_sprout_config::actions::print::PrintConfiguration;
use log::info;
/// Executes the print action with the specified `configuration` inside the provided `context`.
pub fn print(context: Rc<SproutContext>, configuration: &PrintConfiguration) -> Result<()> {
info!("{}", context.stamp(&configuration.text));
Ok(())
}
+84
View File
@@ -0,0 +1,84 @@
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;
/// bls: autodetect and configure BLS-enabled filesystems.
pub mod bls;
/// linux: autodetect and configure Linux kernels.
/// This autoconfiguration module should not be activated
/// on BLS-enabled filesystems as it may make duplicate entries.
pub mod linux;
/// windows: autodetect and configure Windows boot configurations.
pub mod windows;
/// Generate a [RootConfiguration] based on the environment.
/// Intakes a `config` to use as the basis of the autoconfiguration.
pub fn autoconfigure(config: &mut RootConfiguration) -> Result<()> {
// Find all the filesystems that are on the system.
let filesystem_handles =
uefi::boot::find_handles::<SimpleFileSystem>().context("unable to scan filesystems")?;
// For each filesystem that was detected, scan it for supported autoconfig mechanisms.
for handle in filesystem_handles {
// Acquire the device path root for the filesystem.
// 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 = 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);
// A scan that fails is skipped, as a filesystem that can't be read should not
// prevent autoconfiguring the other filesystems.
// Scan the filesystem for BLS supported configurations.
let bls_found = match bls::scan(&mut filesystem, &root, config) {
Ok(found) => found,
Err(error) => {
warn!(
"unable to scan filesystem for bls configurations: {:#}",
error
);
false
}
};
// If BLS was not found, scan for Linux configurations.
if !bls_found && let Err(error) = linux::scan(&mut filesystem, &root, config) {
warn!(
"unable to scan filesystem for linux configurations: {:#}",
error
);
}
// Always look for Windows configurations.
if let Err(error) = windows::scan(&mut filesystem, &root, config) {
warn!(
"unable to scan filesystem for windows configurations: {:#}",
error
);
}
}
Ok(())
}
+119
View File
@@ -0,0 +1,119 @@
use crate::generators::bls::BLS_INITRD_SLOTS;
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;
use uefi::proto::device_path::text::{AllowShortcuts, DisplayOnly};
/// The name prefix of the BLS chainload action that will be used
/// by the BLS generator to chainload entries.
const BLS_CHAINLOAD_ACTION_PREFIX: &str = "bls-chainload-";
/// Scan the specified `filesystem` for BLS configurations.
pub fn scan(
filesystem: &mut FileSystem,
root: &DevicePath,
config: &mut RootConfiguration,
) -> Result<bool> {
// BLS has a loader.conf file that can specify its own auto-entries mechanism.
let bls_loader_conf_path = Path::new(cstr16!("\\loader\\loader.conf"));
// BLS also has an entries directory that can specify explicit entries.
let bls_entries_path = Path::new(cstr16!("\\loader\\entries"));
// Convert the device path root to a string we can use in the configuration.
let mut root = root
.to_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 = unique_hash(&root);
// Whether we have a loader.conf file.
let has_loader_conf = filesystem
.try_exists(bls_loader_conf_path)
.context("unable to check for BLS loader.conf file")?;
// Whether we have an entries directory.
// We actually iterate the entries to see if there are any entry files, as the
// directory itself always has the "." and ".." items, even when it is empty.
let has_entries_dir = filesystem
.read_dir(bls_entries_path)
.map(|mut iterator| {
iterator.any(|entry| {
entry.is_ok_and(|entry| {
entry.is_regular_file()
&& entry
.file_name()
.to_string()
.to_lowercase()
.ends_with(".conf")
})
})
})
.unwrap_or(false);
// Detect if a BLS supported configuration is on this filesystem.
// We check both loader.conf and entries directory as only one of them is required.
if !(has_loader_conf || has_entries_dir) {
return Ok(false);
}
// Generate a unique name for the BLS chainload action.
let chainload_action_name = format!("{}{}", BLS_CHAINLOAD_ACTION_PREFIX, root_unique_hash,);
// BLS is now detected, generate a configuration for it.
let generator = BlsConfiguration {
entry: EntryDeclaration {
title: "$title".to_string(),
actions: vec![chainload_action_name.clone()],
..Default::default()
},
path: format!("{}\\loader", root),
};
// Generate a unique name for the BLS generator and insert the generator into the configuration.
config.generators.insert(
format!("auto-bls-{}", root_unique_hash),
GeneratorDeclaration {
bls: Some(generator),
..Default::default()
},
);
// Generate a chainload configuration for BLS.
// BLS will provide these values to us.
// Every initrd slot is chained in order. Unused slots stamp to the root of the
// filesystem, which the chainload action skips.
let chainload = ChainloadConfiguration {
path: format!("{}\\$chainload", root),
options: vec!["$options".to_string()],
linux_initrd: None,
linux_initrd_chain: (0..BLS_INITRD_SLOTS)
.map(|slot| format!("{}\\$initrd-{}", root, slot))
.collect(),
};
// Insert the chainload action into the configuration.
config.actions.insert(
chainload_action_name,
ActionDeclaration {
chainload: Some(chainload),
..Default::default()
},
);
// We had a BLS supported configuration, so return true.
Ok(true)
}
+253
View File
@@ -0,0 +1,253 @@
use alloc::collections::BTreeMap;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
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::list::ListConfiguration;
use edera_sprout_parsing::{
LINUX_INITRAMFS_PREFIXES, LINUX_KERNEL_PREFIXES, initramfs_candidates, match_kernel_prefix,
unique_hash,
};
use log::warn;
use uefi::CString16;
use uefi::fs::{FileSystem, Path, PathBuf};
use uefi::proto::device_path::DevicePath;
use uefi::proto::device_path::text::{AllowShortcuts, DisplayOnly};
/// The name prefix of the Linux chainload action that will be used to boot Linux.
const LINUX_CHAINLOAD_ACTION_PREFIX: &str = "linux-chainload-";
/// The locations to scan for kernel pairs.
/// A location that doesn't exist or can't be read is skipped.
/// The "\\" location represents the root of the filesystem.
const SCAN_LOCATIONS: &[&str] = &["\\boot", "\\"];
/// 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.
struct KernelPair {
/// The path to a kernel.
kernel: String,
/// The path to an initramfs, if any.
initramfs: Option<String>,
}
/// Scan the specified `filesystem` at `path` for [KernelPair] results.
fn scan_directory(filesystem: &mut FileSystem, path: &str) -> Result<Vec<KernelPair>> {
// All the discovered kernel pairs.
let mut pairs = Vec::new();
// We have to special-case the root directory due to path logic in the uefi crate.
let is_root = path.is_empty() || path == "\\";
// Construct a filesystem path from the path string.
let path = CString16::try_from(path).context("unable to convert path to CString16")?;
let path = Path::new(&path);
let path = path.to_path_buf();
// Check if the path exists and is a directory.
let exists = filesystem
.metadata(&path)
.ok()
.map(|metadata| metadata.is_directory())
.unwrap_or(false);
// If the path does not exist, return an empty list.
if !exists {
return Ok(pairs);
}
// Open a directory iterator on the path to scan.
// Ignore errors here as in some scenarios this might fail due to symlinks.
let Some(directory) = filesystem.read_dir(&path).ok() else {
return Ok(pairs);
};
// Create a new path used for joining file names below.
// All attempts to derive paths for the files in the directory should use this instead.
// The uefi crate does not handle push correctly for the root directory.
// It will add a second slash, which will cause our path logic to fail.
let path_for_join = if is_root {
PathBuf::new()
} else {
path.clone()
};
// For each item in the directory, find a kernel.
for item in directory {
let item = item.context("unable to read directory item")?;
// Skip over any items that are not regular files.
if !item.is_regular_file() {
continue;
}
// Convert the name from a CString16 to a String.
let name = item.file_name().to_string();
// Convert the name to lowercase to make all of this case-insensitive.
let name_for_match = name.to_lowercase();
// Find a kernel prefix that matches, if any.
// This is case-insensitive to ensure we pick up all possibilities.
let Some(prefix) = match_kernel_prefix(&name_for_match, LINUX_KERNEL_PREFIXES) else {
// Skip over anything that doesn't match a kernel prefix.
continue;
};
// Acquire the suffix of the name, this will be used to match an initramfs.
let suffix = &name[prefix.len()..];
// Find a matching initramfs by trying each candidate name, if any.
let mut candidates = initramfs_candidates(suffix, LINUX_INITRAMFS_PREFIXES);
let matched_initramfs_path = loop {
let Some(candidate) = candidates.next() else {
break None;
};
// Construct an initramfs path.
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));
// Check if the initramfs path is a file, if it is, break out of the loop.
// A path that can't be checked is treated as not existing.
if filesystem
.metadata(&initramfs_path)
.is_ok_and(|metadata| metadata.is_regular_file())
{
break Some(initramfs_path);
}
};
// Construct a kernel path from the kernel name.
let mut kernel = path_for_join.clone();
kernel.push(Path::new(&item.file_name()));
let kernel = kernel.to_string();
let initramfs = matched_initramfs_path.map(|initramfs_path| initramfs_path.to_string());
// Produce a kernel pair.
let pair = KernelPair { kernel, initramfs };
pairs.push(pair);
}
Ok(pairs)
}
/// Scan the specified `filesystem` for Linux kernels and matching initramfs.
pub fn scan(
filesystem: &mut FileSystem,
root: &DevicePath,
config: &mut RootConfiguration,
) -> Result<bool> {
let mut pairs = Vec::new();
// Convert the device path root to a string we can use in the configuration.
let mut root = root
.to_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 = unique_hash(&root);
// Scan all locations for kernel pairs, adding them to the list.
// A location that can't be scanned is skipped, so the other locations are still used.
for location in SCAN_LOCATIONS {
match scan_directory(filesystem, location) {
Ok(scanned) => pairs.extend(scanned),
Err(error) => warn!("unable to scan directory {}: {:#}", location, error),
}
}
// If no kernel pairs were found, return false.
if pairs.is_empty() {
return Ok(false);
}
// Generate a unique name for the linux chainload action.
let chainload_action_name = format!("{}{}", LINUX_CHAINLOAD_ACTION_PREFIX, root_unique_hash);
// Kernel pairs are detected, generate a list configuration for it.
let generator = ListConfiguration {
entry: EntryDeclaration {
title: "Boot Linux $name".to_string(),
actions: vec![chainload_action_name.clone()],
sort_key: Some("$kernel".to_string()),
..Default::default()
},
values: pairs
.into_iter()
.map(|pair| {
BTreeMap::from_iter(vec![
("name".to_string(), pair.kernel.clone()),
("kernel".to_string(), format!("{}{}", root, pair.kernel)),
(
"initrd".to_string(),
pair.initramfs
.map(|initramfs| format!("{}{}", root, initramfs))
.unwrap_or_default(),
),
])
})
.collect(),
};
// Generate a unique name for the Linux generator and insert the generator into the configuration.
config.generators.insert(
format!("auto-linux-{}", root_unique_hash),
GeneratorDeclaration {
list: Some(generator),
..Default::default()
},
);
// Insert a default value for the linux-options if it doesn't exist.
if !config.values.contains_key("linux-options") {
config.values.insert(
"linux-options".to_string(),
DEFAULT_LINUX_OPTIONS.to_string(),
);
}
// Generate a chainload configuration for the list generator.
// The list will provide these values to us.
// Note that we don't need an extra \\ in the paths here.
// The root already contains a trailing slash.
let chainload = ChainloadConfiguration {
path: "$kernel".to_string(),
options: vec!["$linux-options".to_string()],
linux_initrd: Some("$initrd".to_string()),
linux_initrd_chain: vec![],
};
// Insert the chainload action into the configuration.
config.actions.insert(
chainload_action_name,
ActionDeclaration {
chainload: Some(chainload),
..Default::default()
},
);
// We had a Linux kernel, so return true to indicate something was found.
Ok(true)
}
+83
View File
@@ -0,0 +1,83 @@
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;
use uefi::proto::device_path::text::{AllowShortcuts, DisplayOnly};
/// The name prefix of the Windows chainload action that will be used to boot Windows.
const WINDOWS_CHAINLOAD_ACTION_PREFIX: &str = "windows-chainload-";
/// Windows boot manager path.
const BOOTMGR_FW_PATH: &str = "\\EFI\\Microsoft\\Boot\\bootmgfw.efi";
/// Scan the specified `filesystem` for Windows configurations.
pub fn scan(
filesystem: &mut FileSystem,
root: &DevicePath,
config: &mut RootConfiguration,
) -> Result<bool> {
// Convert the boot manager firmware path to a path.
let bootmgr_fw_path =
CString16::try_from(BOOTMGR_FW_PATH).context("unable to convert path to CString16")?;
let bootmgr_fw_path = Path::new(&bootmgr_fw_path);
// Check if the boot manager firmware path exists, if it doesn't, return false.
if !filesystem
.try_exists(bootmgr_fw_path)
.context("unable to check if bootmgr firmware path exists")?
{
return Ok(false);
}
// Convert the device path root to a string we can use in the configuration.
let mut root = root
.to_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 = unique_hash(&root);
// Generate a unique name for the Windows chainload action.
let chainload_action_name = format!("{}{}", WINDOWS_CHAINLOAD_ACTION_PREFIX, root_unique_hash,);
// Generate an entry name for Windows.
let entry_name = format!("auto-windows-{}", root_unique_hash);
// Create an entry for Windows and insert it into the configuration.
let entry = EntryDeclaration {
title: "Boot Windows".to_string(),
actions: vec![chainload_action_name.clone()],
values: Default::default(),
sort_key: None, // Use the default sort key.
};
config.entries.insert(entry_name, entry);
// Generate a chainload configuration for Windows.
let chainload = ChainloadConfiguration {
path: format!("{}{}", root, bootmgr_fw_path),
options: vec![],
..Default::default()
};
// Insert the chainload action into the configuration.
config.actions.insert(
chainload_action_name,
ActionDeclaration {
chainload: Some(chainload),
..Default::default()
},
);
// We have a Windows boot entry, so return true to indicate something was found.
Ok(true)
}
+2
View File
@@ -0,0 +1,2 @@
/// The configuration loader mechanisms.
pub mod loader;
+131
View File
@@ -0,0 +1,131 @@
use crate::options::SproutOptions;
use alloc::format;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use anyhow::{Context, Result, bail};
use core::fmt::Display;
use core::ops::Deref;
use edera_sprout_config::{RootConfiguration, latest_version};
use eficore::platform::tpm::PlatformTpm;
use log::{info, warn};
use serde_ignored::Path;
use toml::Value;
use uefi::proto::device_path::LoadedImageDevicePath;
/// Loads the raw configuration from the sprout config file as data.
fn load_raw_config(options: &SproutOptions) -> Result<Vec<u8>> {
// Open the LoadedImageDevicePath protocol to get the path to the current image.
let current_image_device_path_protocol =
uefi::boot::open_protocol_exclusive::<LoadedImageDevicePath>(uefi::boot::image_handle())
.context("unable to get loaded image device path")?;
// Acquire the device path as a boxed device path.
let path = current_image_device_path_protocol.deref().to_boxed();
info!("configuration file: {}", options.config);
// Read the contents of the sprout config file.
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.
PlatformTpm::log_event(
PlatformTpm::PCR_BOOT_LOADER_CONFIG,
&content,
"sprout: configuration file",
)
.context("unable to measure the sprout.toml file into the TPM")?;
// Return the contents of the sprout config file.
Ok(content)
}
/// Format the configuration key `path` with dots, like "actions.linux.chainload.path".
fn key_path(path: &Path) -> String {
// Joins the `parent` path and the `key` with a dot, unless the parent is the root.
fn join(parent: &Path, key: impl Display) -> String {
match parent {
Path::Root => key.to_string(),
parent => format!("{}.{}", key_path(parent), key),
}
}
match path {
Path::Root => String::new(),
Path::Seq { parent, index } => join(parent, index),
Path::Map { parent, key } => join(parent, key),
// Optional values and wrapper types don't add a key of their own.
Path::Some { parent }
| Path::NewtypeStruct { parent }
| Path::NewtypeVariant { parent } => key_path(parent),
}
}
/// Warn about each declaration in `config` that configures more than one kind.
/// For example, an action with both chainload and print configured only runs chainload.
fn warn_multiple_kinds(config: &RootConfiguration) {
for (name, action) in &config.actions {
let kinds = [
action.chainload.is_some(),
action.print.is_some(),
action.edera.is_some(),
];
if kinds.iter().filter(|kind| **kind).count() > 1 {
warn!(
"action {} configures more than one action, only one is used",
name
);
}
}
for (name, generator) in &config.generators {
let kinds = [
generator.matrix.is_some(),
generator.bls.is_some(),
generator.list.is_some(),
];
if kinds.iter().filter(|kind| **kind).count() > 1 {
warn!(
"generator {} configures more than one generator, only one is used",
name
);
}
}
}
/// Loads the [RootConfiguration] for Sprout.
pub fn load(options: &SproutOptions) -> Result<RootConfiguration> {
// Load the raw configuration from the sprout config file.
let content = load_raw_config(options)?;
// Parse the raw configuration into a toml::Value which can represent any TOML file.
let value: Value = toml::from_slice(&content).context("unable to parse sprout config file")?;
// Check the version of the configuration without parsing the full configuration.
let version = value
.get("version")
.cloned()
.unwrap_or_else(|| Value::Integer(latest_version() as i64));
// Parse the version into an u32.
let version: u32 = version
.try_into()
.context("unable to get configuration version")?;
// Check if the version is supported.
if version != latest_version() {
bail!("unsupported configuration version: {}", version);
}
// If the version is supported, parse the full configuration.
// Keys that aren't part of the configuration, such as misspelled keys, are ignored.
// Warn about each one, as the configuration may not do what was intended.
let config: RootConfiguration = serde_ignored::deserialize(value, |path| {
warn!("ignoring unknown configuration key: {}", key_path(&path));
})
.context("unable to parse sprout.toml file")?;
// Warn about declarations that configure more than one kind, as only one is used.
warn_multiple_kinds(&config);
// Return the parsed configuration.
Ok(config)
}
+253
View File
@@ -0,0 +1,253 @@
use crate::options::SproutOptions;
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 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].
const CONTEXT_FINALIZE_ITERATION_LIMIT: usize = 100;
/// The maximum length of a value produced by [SproutContext::finalize].
/// A value that refers to itself more than once doubles in length on every iteration,
/// so this stops it from using up all memory before the iteration limit is reached.
const CONTEXT_FINALIZE_VALUE_LENGTH_LIMIT: usize = 64 * 1024;
/// Declares a root context for Sprout.
/// This contains data that needs to be shared across Sprout.
pub struct RootContext {
/// The actions that are available in Sprout.
actions: BTreeMap<String, ActionDeclaration>,
/// The device path of the loaded Sprout image.
loaded_image_path: Option<Box<DevicePath>>,
/// Platform timer started at the beginning of the boot process.
timer: PlatformTimer,
/// The global options of Sprout.
options: SproutOptions,
}
impl RootContext {
/// Creates a new root context with the `loaded_image_device_path` which will be stored
/// in the context for easy access. We also provide a `timer` which is used to measure elapsed
/// time for the bootloader.
pub fn new(
loaded_image_device_path: Box<DevicePath>,
timer: PlatformTimer,
options: SproutOptions,
) -> Self {
Self {
actions: BTreeMap::new(),
timer,
loaded_image_path: Some(loaded_image_device_path),
options,
}
}
/// Access the actions configured inside Sprout.
pub fn actions(&self) -> &BTreeMap<String, ActionDeclaration> {
&self.actions
}
/// Access the actions configured inside Sprout mutably for modification.
pub fn actions_mut(&mut self) -> &mut BTreeMap<String, ActionDeclaration> {
&mut self.actions
}
/// Access the platform timer that is started at the beginning of the boot process.
pub fn timer(&self) -> &PlatformTimer {
&self.timer
}
/// Access the device path of the loaded Sprout image.
pub fn loaded_image_path(&self) -> Result<&DevicePath> {
self.loaded_image_path
.as_deref()
.ok_or_else(|| anyhow!("no loaded image path"))
}
/// Access the global Sprout options.
pub fn options(&self) -> &SproutOptions {
&self.options
}
}
/// A context of Sprout. This is passed around different parts of Sprout and represents
/// a [RootContext] which is data that is shared globally, and [SproutContext] which works
/// sort of like a tree of values. You can cheaply clone a [SproutContext] and modify it with
/// new values, which override the values of contexts above it.
///
/// This is a core part of the value mechanism in Sprout which makes templating possible.
pub struct SproutContext {
root: Rc<RootContext>,
parent: Option<Rc<SproutContext>>,
values: BTreeMap<String, String>,
}
impl SproutContext {
/// Create a new [SproutContext] using `root` as the root context.
pub fn new(root: RootContext) -> Self {
Self {
root: Rc::new(root),
parent: None,
values: BTreeMap::new(),
}
}
/// Access the root context of this context.
pub fn root(&self) -> &RootContext {
self.root.as_ref()
}
/// Access the root context to modify it, if possible.
pub fn root_mut(&mut self) -> Option<&mut RootContext> {
Rc::get_mut(&mut self.root)
}
/// Retrieve the value specified by `key` from this context or its parents.
/// Returns `None` if the value is not found.
pub fn get(&self, key: impl AsRef<str>) -> Option<&String> {
self.values.get(key.as_ref()).or_else(|| {
self.parent
.as_ref()
.and_then(|parent| parent.get(key.as_ref()))
})
}
/// Collects all keys that are present in this context or its parents.
/// This is useful for iterating over all keys in a context.
pub fn all_keys(&self) -> Vec<String> {
let mut keys = BTreeSet::new();
for key in self.values.keys() {
keys.insert(key.clone());
}
if let Some(parent) = &self.parent {
keys.extend(parent.all_keys());
}
keys.into_iter().collect()
}
/// Collects all values that are present in this context or its parents.
/// This is useful for iterating over all values in a context.
pub fn all_values(&self) -> BTreeMap<String, String> {
let mut values = BTreeMap::new();
for key in self.all_keys() {
// Acquire the value from the context. Since retrieving all the keys will give us
// a full view of the context, we can be sure that the key exists.
let value = self.get(&key).cloned().unwrap_or_default();
values.insert(key.clone(), value);
}
values
}
/// Sets the value `key` to the value specified by `value` in this context.
/// If the parent context has this key, this will override that key.
pub fn set(&mut self, key: impl AsRef<str>, value: impl ToString) {
self.values
.insert(key.as_ref().to_string(), value.to_string());
}
/// Inserts all the specified `values` into this context.
/// These values will take precedence over its parent context.
pub fn insert(&mut self, values: &BTreeMap<String, String>) {
for (key, value) in values {
self.values.insert(key.clone(), value.clone());
}
}
/// Forks this context as an owned [SproutContext]. This makes it possible
/// to cheaply modify a context without cloning the parent context map.
/// The parent of the returned context is [self].
pub fn fork(self: &Rc<SproutContext>) -> Self {
Self {
root: self.root.clone(),
parent: Some(self.clone()),
values: BTreeMap::new(),
}
}
/// Freezes this context into a [Rc] which makes it possible to cheaply clone
/// and makes it less easy to modify a context. This can be used to pass the context
/// to various other parts of Sprout and ensure it won't be modified. Instead, once
/// a context is frozen, it should be [self.fork]'d to be modified.
pub fn freeze(self) -> Rc<SproutContext> {
Rc::new(self)
}
/// Finalizes a context by producing a context with no parent that contains all the values
/// of all parent contexts merged. This makes it possible to ensure [SproutContext] has no
/// inheritance with other [SproutContext]s. It will still contain a [RootContext] however.
pub fn finalize(&self) -> Result<SproutContext> {
// Collect all the values from the context and its parents.
let mut current_values = self.all_values();
// To ensure that there is no possible infinite loop, we need to check
// the number of iterations. If it exceeds CONTEXT_FINALIZE_ITERATION_LIMIT, we bail.
let mut iterations: usize = 0;
loop {
iterations += 1;
if iterations > CONTEXT_FINALIZE_ITERATION_LIMIT {
bail!("maximum number of replacement iterations reached while finalizing context");
}
let mut did_change = false;
let mut values = BTreeMap::new();
for (key, value) in &current_values {
let (changed, result) = stamp_values(&current_values, value);
if changed {
// If the value changed, we need to re-stamp it.
did_change = true;
}
// Ensure the value has not grown without bound.
if result.len() > CONTEXT_FINALIZE_VALUE_LENGTH_LIMIT {
bail!("value {} is too long while finalizing context", key);
}
// Insert the new value into the value map.
values.insert(key.clone(), result);
}
current_values = values;
// If the values did not change, we can stop.
if !did_change {
break;
}
}
// Produce the final context.
Ok(Self {
root: self.root.clone(),
parent: None,
values: current_values,
})
}
/// 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 {
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
/// may not succeed if something else is holding onto the value.
pub fn unload(self: Rc<SproutContext>) -> Option<SproutContext> {
Rc::into_inner(self)
}
}
+83
View File
@@ -0,0 +1,83 @@
use crate::context::SproutContext;
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 uefi::boot::SearchType;
/// 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 = eficore::path::resolve_path(
Some(context.root().loaded_image_path()?),
context.stamp(&driver.path),
)
.context("unable to resolve path to driver")?;
// 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 = 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.handle()).context("unable to start driver image")?;
Ok(())
}
/// Reconnects all handles to their controllers.
/// This is effectively a UEFI stack reload in a sense.
/// After we load all the drivers, we need to reconnect all of their handles
/// so that filesystems are recognized again.
fn reconnect() -> Result<()> {
// Locate all of the handles in the UEFI stack.
let handles = uefi::boot::locate_handle_buffer(SearchType::AllHandles)
.context("unable to locate handles buffer")?;
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, true);
}
Ok(())
}
/// Load all the drivers specified in `drivers`.
/// There is no driver order currently. This will reconnect all the controllers
/// to all handles if at least one driver was loaded.
pub fn load(
context: Rc<SproutContext>,
drivers: &BTreeMap<String, DriverDeclaration>,
) -> Result<()> {
// If there are no drivers, we don't need to do anything.
if drivers.is_empty() {
return Ok(());
}
info!("loading drivers");
// Load all the drivers in no particular order.
for (name, driver) in drivers {
load_driver(context.clone(), driver).context(format!("unable to load driver: {}", name))?;
}
// Reconnect all the controllers to all handles.
reconnect().context("unable to reconnect drivers")?;
info!("loaded drivers");
// We've now loaded all the drivers, so we can return.
Ok(())
}
+149
View File
@@ -0,0 +1,149 @@
use crate::context::SproutContext;
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)]
pub struct BootableEntry {
name: String,
title: String,
context: Rc<SproutContext>,
declaration: EntryDeclaration,
default: bool,
pin_name: bool,
sort_key: Option<String>,
}
impl BootableEntry {
/// Create a new bootable entry to represent the full context of an entry.
pub fn new(
name: String,
title: String,
context: Rc<SproutContext>,
declaration: EntryDeclaration,
) -> Self {
Self {
name,
title,
context,
declaration,
default: false,
pin_name: false,
sort_key: None,
}
}
/// Fetch the name of the entry. This is usually a machine-identifiable key.
pub fn name(&self) -> &str {
&self.name
}
/// Fetch the title of the entry. This is usually a human-readable key.
pub fn title(&self) -> &str {
&self.title
}
/// Fetch the full context of the entry.
pub fn context(&self) -> Rc<SproutContext> {
Rc::clone(&self.context)
}
/// Fetch the declaration of the entry.
pub fn declaration(&self) -> &EntryDeclaration {
&self.declaration
}
/// Fetch whether the entry is the default entry.
pub fn is_default(&self) -> bool {
self.default
}
/// Fetch whether the entry is pinned, which prevents prefixing.
pub fn is_pin_name(&self) -> bool {
self.pin_name
}
/// Swap out the context of the entry.
pub fn swap_context(&mut self, context: Rc<SproutContext>) {
self.context = context;
}
/// Restamp the title with the current context.
pub fn restamp_title(&mut self) {
self.title = self.context.stamp(&self.title);
}
/// Restamp the declared sort key with the current context.
/// A sort key that was set on this entry directly is left as is.
pub fn restamp_sort_key(&mut self) {
if self.sort_key.is_none()
&& let Some(ref sort_key) = self.declaration.sort_key
{
self.sort_key = Some(self.context.stamp(sort_key));
}
}
/// Mark this entry as the default entry.
pub fn mark_default(&mut self) {
self.default = true;
}
// Unmark this entry as the default entry.
pub fn unmark_default(&mut self) {
self.default = false;
}
/// Mark this entry as being pinned, which prevents prefixing.
pub fn mark_pin_name(&mut self) {
self.pin_name = true;
}
/// Prepend the name of the entry with `prefix`.
pub fn prepend_name_prefix(&mut self, prefix: &str) {
self.name.insert_str(0, prefix);
}
/// 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>(
needle: &str,
haystack: impl Iterator<Item = &'a BootableEntry>,
) -> Option<&'a BootableEntry> {
haystack
.enumerate()
.find(|(index, entry)| entry.is_match(needle) || index.to_string() == needle)
.map(|(_index, entry)| entry)
}
}
@@ -1,17 +1,15 @@
use crate::context::SproutContext;
use crate::extractors::filesystem_device_match::FilesystemDeviceMatchExtractor;
use alloc::rc::Rc;
use alloc::string::String;
use anyhow::{Result, bail};
use serde::{Deserialize, Serialize};
use std::rc::Rc;
use edera_sprout_config::extractors::ExtractorDeclaration;
/// The filesystem device match extractor.
pub mod filesystem_device_match;
#[derive(Serialize, Deserialize, Default, Clone)]
pub struct ExtractorDeclaration {
#[serde(default, rename = "filesystem-device-match")]
pub filesystem_device_match: Option<FilesystemDeviceMatchExtractor>,
}
/// Extracts the value using the specified `extractor` under the provided `context`.
/// The extractor must return a value, and if a value cannot be determined, an error
/// should be returned.
pub fn extract(context: Rc<SproutContext>, extractor: &ExtractorDeclaration) -> Result<String> {
if let Some(filesystem) = &extractor.filesystem_device_match {
filesystem_device_match::extract(context, filesystem)
@@ -0,0 +1,178 @@
use crate::context::SproutContext;
use alloc::rc::Rc;
use alloc::string::String;
use anyhow::{Context, Result, anyhow, bail};
use core::ops::Deref;
use core::str::FromStr;
use edera_sprout_config::extractors::filesystem_device_match::FilesystemDeviceMatchExtractor;
use eficore::partition::PartitionGuidForm;
use log::warn;
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, Handle};
/// Check whether the filesystem at `handle` matches all the criteria of the `extractor`
/// configuration inside the `context`. The partition UUIDs are parsed ahead of time into
/// `partition_uuid` and `partition_type_uuid`.
fn is_match(
context: &Rc<SproutContext>,
extractor: &FilesystemDeviceMatchExtractor,
handle: Handle,
partition_uuid: Option<Guid>,
partition_type_uuid: Option<Guid>,
) -> Result<bool> {
// Check if the partition info matches partition uuid criteria.
if let Some(parsed_uuid) = partition_uuid {
// Fetch the root of the device.
let root = uefi::boot::open_protocol_exclusive::<DevicePath>(handle)
.context("unable to fetch the device path of the filesystem")?
.deref()
.to_boxed();
// Fetch the partition uuid for this filesystem.
let partition_uuid =
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 partition_uuid != Some(parsed_uuid) {
return Ok(false);
}
}
// Check if the partition info matches partition type uuid criteria.
if let Some(parsed_uuid) = partition_type_uuid {
// Fetch the root of the device.
let root = uefi::boot::open_protocol_exclusive::<DevicePath>(handle)
.context("unable to fetch the device path of the filesystem")?
.deref()
.to_boxed();
// Fetch the partition type uuid for this filesystem.
let partition_type_uuid =
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 partition_type_uuid != Some(parsed_uuid) {
return Ok(false);
}
}
// The remaining criteria need the filesystem itself.
if extractor.has_label.is_none() && extractor.has_item.is_none() {
return Ok(true);
}
// Open the filesystem protocol for this handle.
let mut filesystem = uefi::boot::open_protocol_exclusive::<SimpleFileSystem>(handle)
.context("unable to open filesystem protocol")?;
// Check if the filesystem matches label criteria.
if let Some(ref label) = extractor.has_label {
let want_label = CString16::try_from(context.stamp(label).as_str())
.context("unable to convert label to CString16")?;
let mut root = filesystem
.open_volume()
.context("unable to open filesystem volume")?;
let label = root
.get_boxed_info::<FileSystemVolumeLabel>()
.context("unable to get filesystem volume label")?;
if label.volume_label() != want_label {
return Ok(false);
}
}
// Check if the filesystem matches item criteria.
if let Some(ref item) = extractor.has_item {
let want_item = CString16::try_from(context.stamp(item).as_str())
.context("unable to convert item to CString16")?;
let mut filesystem = FileSystem::new(filesystem);
// Check the metadata of the item.
// Ignore filesystem errors as we can't do anything useful with the error.
let Some(metadata) = filesystem.metadata(Path::new(&want_item)).ok() else {
return Ok(false);
};
// Only check directories and files.
if !(metadata.is_directory() || metadata.is_regular_file()) {
return Ok(false);
}
}
Ok(true)
}
/// Extract a filesystem device path using the specified `context` and `extractor` configuration.
pub fn extract(
context: Rc<SproutContext>,
extractor: &FilesystemDeviceMatchExtractor,
) -> Result<String> {
// If no criteria are provided, bail with an error.
if extractor.has_label.is_none()
&& extractor.has_item.is_none()
&& extractor.has_partition_uuid.is_none()
&& extractor.has_partition_type_uuid.is_none()
{
bail!("at least one criteria is required for filesystem-device-match");
}
// Parse the partition uuid from the extractor.
let partition_uuid = extractor
.has_partition_uuid
.as_deref()
.map(Guid::from_str)
.transpose()
.map_err(|e| anyhow!("unable to parse has-partition-uuid: {}", e))?;
// Parse the partition type uuid from the extractor.
let partition_type_uuid = extractor
.has_partition_type_uuid
.as_deref()
.map(Guid::from_str)
.transpose()
.map_err(|e| anyhow!("unable to parse has-partition-type-uuid: {}", e))?;
// Find all the filesystems inside the UEFI stack.
let handles = uefi::boot::find_handles::<SimpleFileSystem>()
.context("unable to find filesystem handles")?;
// Iterate over all the filesystems and check if they match the criteria.
for handle in handles {
// A filesystem that can't be checked is skipped, as it should not prevent
// finding a match on the other filesystems or using the fallback value.
match is_match(
&context,
extractor,
handle,
partition_uuid,
partition_type_uuid,
) {
Ok(true) => {}
Ok(false) => continue,
Err(error) => {
warn!("skipping filesystem, unable to check it: {:#}", error);
continue;
}
}
// If we have a match, return the device root path.
let path = uefi::boot::open_protocol_exclusive::<DevicePath>(handle)
.context("unable to open filesystem device path")?;
let path = path.deref();
// Acquire the device path root as a string.
return eficore::path::device_path_root(path).context("unable to get device path root");
}
// If there is a fallback value, use it at this point.
if let Some(fallback) = &extractor.fallback {
return Ok(fallback.clone());
}
// Without a fallback, we can't continue, so bail.
bail!("unable to find matching filesystem")
}
+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");
}
}
+230
View File
@@ -0,0 +1,230 @@
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,
};
/// The number of initrd slots set on each BLS entry.
/// Entries set `initrd-0` through `initrd-7`, and slots without an initrd are empty.
pub const BLS_INITRD_SLOTS: usize = 8;
// TODO(azenla): remove this once variable substitution is implemented.
/// This function is used to remove the `tuned_initrd` variable from the initrd paths.
/// Fedora uses tuned which adds an initrd that shouldn't be used.
fn quirk_initrd_remove_tuned(paths: Vec<String>) -> Vec<String> {
paths
.into_iter()
.filter(|path| path != "$tuned_initrd")
.collect()
}
/// 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;
}
// Put the initrds through a quirk modifier to support Fedora.
let initrds = quirk_initrd_remove_tuned(entry.initrd_paths());
// Skip entries with more initrds than there are slots, as they can't be fully loaded.
if initrds.len() > BLS_INITRD_SLOTS {
warn!(
"bls entry {} has more than {} initrds, skipping",
name, BLS_INITRD_SLOTS
);
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();
// 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);
// The initrd value keeps the last initrd, which is what it held before
// multiple initrds were supported.
context.set("initrd", initrds.last().cloned().unwrap_or_default());
// Set every initrd slot, even the unused ones. An unset slot would let "$initrd-1"
// match "$initrd" instead, producing the initrd value followed by "-1".
for slot in 0..BLS_INITRD_SLOTS {
context.set(
format!("initrd-{}", slot),
initrds.get(slot).cloned().unwrap_or_default(),
);
}
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())
}
+39
View File
@@ -0,0 +1,39 @@
use crate::context::SproutContext;
use crate::entries::BootableEntry;
use alloc::rc::Rc;
use alloc::string::ToString;
use alloc::vec::Vec;
use anyhow::Result;
use edera_sprout_config::generators::list::ListConfiguration;
/// Generates a set of entries using the specified `list` configuration in the `context`.
pub fn generate(
context: Rc<SproutContext>,
list: &ListConfiguration,
) -> Result<Vec<BootableEntry>> {
let mut entries = Vec::new();
// For each combination, create a new context and entry.
for (index, combination) in list.values.iter().enumerate() {
let mut context = context.fork();
// Insert the combination into the context.
context.insert(combination);
let context = context.freeze();
// Stamp the entry title and actions from the template.
let mut entry = list.entry.clone();
// 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(),
entry.title.clone(),
context,
entry,
));
}
Ok(entries)
}
+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,
},
)
}
+501
View File
@@ -0,0 +1,501 @@
#![doc = include_str!("../README.md")]
#![no_std]
#![no_main]
extern crate alloc;
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 uefi::{entry, proto::device_path::LoadedImageDevicePath};
use uefi_raw::Status;
/// actions: Code that can be configured and executed by Sprout.
pub mod actions;
/// autoconfigure: Autoconfigure Sprout based on the detected environment.
pub mod autoconfigure;
/// config: Sprout configuration mechanism.
pub mod config;
/// context: Stored values that can be cheaply forked and cloned.
pub mod context;
/// drivers: EFI drivers to load and provide extra functionality.
pub mod drivers;
/// entries: Boot menu entries that have a title and can execute actions.
pub mod entries;
/// extractors: Runtime code that can extract values into the Sprout context.
pub mod extractors;
/// generators: Runtime code that can generate entries with specific values.
pub mod generators;
/// menu: Display a boot menu to select an entry to boot.
pub mod menu;
/// options: Parse the options of the Sprout executable.
pub mod options;
/// phases: Hooks into specific parts of the boot process.
pub mod phases;
/// sbat: Secure Boot Attestation section.
pub mod sbat;
/// The delay to wait for when an error occurs in Sprout.
const DELAY_ON_ERROR: Duration = Duration::from_secs(10);
/// Unwraps the `result` of a bootloader interface operation, logging the error if one occurs.
/// The bootloader interface only exchanges information with the operating system, so a
/// firmware variable that can't be read or written should not prevent booting.
/// If an error occurs, the default value is provided instead.
fn advisory<T: Default>(result: Result<T>) -> T {
result.unwrap_or_else(|error| {
warn!("{:#}", error);
T::default()
})
}
/// 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!("Sprout Secure Boot is in beta. Some functionality may not work as expected.");
}
// Start the platform timer.
let timer = PlatformTimer::start();
// Mark the initialization of Sprout in the bootloader interface.
advisory(
BootloaderInterface::mark_init(&timer)
.context("unable to mark initialization in bootloader interface"),
);
// Tell the bootloader interface what firmware we are running on.
advisory(
BootloaderInterface::set_firmware_info()
.context("unable to set firmware info in bootloader interface"),
);
// Tell the bootloader interface what loader is being used.
advisory(
BootloaderInterface::set_loader_info()
.context("unable to set loader info in bootloader interface"),
);
// Acquire the number of active PCR banks on the TPM.
// If no TPM is available, this will return zero.
// This is only reported to the bootloader interface, so a TPM that fails to report its
// PCR banks is treated as having none, rather than preventing boot.
let active_pcr_banks = PlatformTpm::active_pcr_banks().unwrap_or_else(|error| {
warn!("unable to determine the active TPM PCR banks: {}", error);
0
});
// Tell the bootloader interface what the number of active PCR banks is.
advisory(
BootloaderInterface::set_tpm2_active_pcr_banks(active_pcr_banks)
.context("unable to set tpm2 active PCR banks in bootloader interface"),
);
// Parse the options to the sprout executable.
let options = SproutOptions::parse().context("unable to parse options")?;
// If --autoconfigure is specified, we use a stub configuration.
let mut config = if options.autoconfigure {
info!("autoconfiguration enabled, configuration file will be ignored");
RootConfiguration::default()
} else {
// Load the configuration of sprout.
// At this point, the configuration has been validated and the specified
// version is checked to ensure compatibility.
config::loader::load(&options)?
};
// Grab the sprout.efi loaded image path.
// This is done in a block to ensure the release of the LoadedImageDevicePath protocol.
let loaded_image_path = {
let current_image_device_path_protocol = uefi::boot::open_protocol_exclusive::<
LoadedImageDevicePath,
>(uefi::boot::image_handle())
.context("unable to get loaded image device path")?;
current_image_device_path_protocol.deref().to_boxed()
};
// Grab the partition GUID of the ESP that sprout was loaded from.
// This is only reported to the bootloader interface.
let loaded_image_partition_guid = advisory(
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.
if let Some(loaded_image_partition_guid) = loaded_image_partition_guid {
// Tell the system about the partition GUID.
advisory(
BootloaderInterface::set_partition_guid(&loaded_image_partition_guid)
.context("unable to set partition guid in bootloader interface"),
);
}
// Tell the bootloader interface what the loaded image path is.
advisory(
BootloaderInterface::set_loader_path(&loaded_image_path)
.context("unable to set loader path in bootloader interface"),
);
// Create the root context.
let mut root = RootContext::new(loaded_image_path, timer, options);
// Insert the configuration actions into the root context.
root.actions_mut().extend(config.actions.clone());
// Create a new sprout context with the root context.
let mut context = SproutContext::new(root);
// Insert the configuration values into the sprout context.
context.insert(&config.values);
// Freeze the sprout context so it can be shared and cheaply cloned.
let context = context.freeze();
// Execute the early phase.
phase(context.clone(), &config.phases.early).context("unable to execute early phase")?;
// Load all configured drivers.
drivers::load(context.clone(), &config.drivers).context("unable to load drivers")?;
// If --autoconfigure is specified or the loaded configuration has autoconfigure enabled,
// trigger the autoconfiguration mechanism.
if context.root().options().autoconfigure || config.options.autoconfigure {
autoconfigure::autoconfigure(&mut config).context("unable to autoconfigure")?;
}
// Unload the context so that it can be modified.
let Some(mut context) = context.unload() else {
bail!("context safety violation while trying to unload context");
};
// Perform root context modification in a block to release the modification when complete.
{
// Modify the root context to include the autoconfigured actions.
let Some(root) = context.root_mut() else {
bail!("context safety violation while trying to modify root context");
};
// Extend the root context with the autoconfigured actions.
root.actions_mut().extend(config.actions);
// Insert any modified root values.
context.insert(&config.values);
}
// Refreeze the context to ensure that further operations can share the context.
let context = context.freeze();
// Run all the extractors declared in the configuration.
let mut extracted = BTreeMap::new();
for (name, extractor) in &config.extractors {
let value = extractors::extract(context.clone(), extractor)
.context(format!("unable to extract value {}", name))?;
info!("extracted value {}: {}", name, value);
extracted.insert(name.clone(), value);
}
let mut context = context.fork();
// Insert the extracted values into the sprout context.
context.insert(&extracted);
let context = context.freeze();
// Execute the startup phase.
phase(context.clone(), &config.phases.startup).context("unable to execute startup phase")?;
let mut entries = Vec::new();
// Insert all the static entries from the configuration into the entry list.
for (name, entry) in config.entries {
// Associate the main context with the static entry.
entries.push(BootableEntry::new(
name,
entry.title.clone(),
context.clone(),
entry,
));
}
// Run all the generators declared in the configuration.
for (name, generator) in config.generators {
let context = context.fork().freeze();
// We will prefix all entries with [name]-, provided the name is not pinned.
let prefix = format!("{}-", name);
// Add all the entries generated by the generator to the entry list.
// The generator specifies the context associated with the entry.
for mut entry in generators::generate(context.clone(), &generator)? {
// If the entry name is not pinned, prepend the name prefix.
if !entry.is_pin_name() {
entry.prepend_name_prefix(&prefix);
}
entries.push(entry);
}
}
// Entries whose context can't be finalized are skipped, as one broken entry
// should not prevent booting any of the other entries.
entries.retain_mut(|entry| {
let mut context = entry.context().fork();
// Insert the values from the entry configuration into the
// sprout context to use with the entry itself.
context.insert(&entry.declaration().values);
let context = match context.finalize() {
Ok(context) => context.freeze(),
Err(error) => {
warn!(
"skipping entry {}, unable to finalize context: {:#}",
entry.name(),
error
);
return false;
}
};
// Provide the new context to the bootable entry.
entry.swap_context(context);
// Restamp the title and sort key with any values.
entry.restamp_title();
entry.restamp_sort_key();
// Mark this entry as the default entry if it is declared as such.
if let Some(ref default_entry) = config.options.default_entry {
// If the entry matches the default entry, mark it as the default entry.
if entry.is_match(default_entry) {
entry.mark_default();
}
}
true
});
// 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.
advisory(
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 = advisory(
BootloaderInterface::get_timeout().context("unable to get bootloader interface timeout"),
);
// Acquire the default entry from the bootloader interface.
let bootloader_interface_default_entry = advisory(
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 = advisory(
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);
// Whether the boot menu should wait for the user, instead of counting down.
let mut wait_for_user = false;
// Apply bootloader interface timeout settings.
match bootloader_interface_timeout {
BootloaderInterfaceTimeout::MenuForce => {
// Force the boot menu, and wait for the user to choose an entry.
force_boot_menu = true;
wait_for_user = true;
}
BootloaderInterfaceTimeout::MenuForceTimeout(timeout) => {
// Force the boot menu with the specified timeout.
force_boot_menu = true;
menu_timeout = timeout;
}
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 {
if entries
.iter()
.any(|entry| entry.is_match(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();
}
}
} else {
// If no entry matches, such as when the entry was removed, the setting is ignored
// so that the configured default entry is still used.
warn!(
"ignoring bootloader interface default entry '{}': no matching entry",
bootloader_interface_default_entry
);
}
}
// 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()
{
entry.mark_default();
}
// Convert the menu timeout to a duration.
// A zero timeout boots without showing the menu, so a forced menu with a zero timeout
// waits for the user instead.
let menu_timeout = if wait_for_user || (force_boot_menu && menu_timeout == 0) {
None
} else {
Some(Duration::from_secs(menu_timeout))
};
// Determine the menu style based on the options or configuration.
// We prefer the options over the configuration to allow for overriding.
let menu_style = context
.root()
.options()
.menu_style
.unwrap_or(config.options.menu_style);
// Find the forced boot entry, unless the boot menu is forced.
// If the forced boot entry can't be found, such as when it was removed,
// the boot menu is used instead.
let forced_entry = force_boot_entry
.as_ref()
.filter(|_| !force_boot_menu)
.and_then(|force_boot_entry| {
let entry = BootableEntry::find(force_boot_entry, entries.iter());
if entry.is_none() {
warn!(
"unable to find entry '{}', using the boot menu",
force_boot_entry
);
}
entry
});
// Use the forced boot entry if possible, otherwise pick an entry using a boot menu.
let entry = match forced_entry {
Some(entry) => entry,
// Delegate to the menu to select an entry to boot.
None => menu::select(&timer, menu_timeout, menu_style, &entries)
.context("unable to select entry via boot menu")?,
};
// Tell the bootloader interface what the selected entry is.
advisory(
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);
actions::execute(entry.context().clone(), &action)
.context(format!("unable to execute action '{}'", action))?;
}
Ok(())
}
/// The main entrypoint of sprout.
/// It is possible this function will not return if actions that are executed
/// exit boot services or do not return control to sprout.
#[entry]
fn efi_main() -> Status {
// Initialize the basic UEFI environment.
// 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);
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.
Status::SUCCESS
}
+142
View File
@@ -0,0 +1,142 @@
use crate::entries::BootableEntry;
use crate::menu::basic::BasicMenu;
use crate::menu::simple::SimpleMenu;
use alloc::vec;
use anyhow::{Context, Result};
use core::time::Duration;
use edera_sprout_config::MenuStyle;
use eficore::bootloader_interface::BootloaderInterface;
use eficore::platform::timer::PlatformTimer;
use log::warn;
use uefi::ResultExt;
use uefi::boot::TimerTrigger;
use uefi::proto::console::text::{Input, Key};
use uefi_raw::table::boot::{EventType, Tpl};
/// basic: A menu that prints the entries and selects them by number.
pub mod basic;
/// simple: A full-screen menu that selects entries with the arrow keys.
pub mod simple;
/// The longest duration a single timer is set for. This is well within the range of
/// the 100ns units that timers are set in, and still over a century long.
const MAX_TIMER_DURATION: Duration = Duration::from_secs(u32::MAX as u64);
/// A boot menu that can be shown to select an entry to boot.
pub trait BootMenu {
/// Select an entry from `entries` to boot. If no entry is chosen before `timeout` passes,
/// the default entry is selected. If `timeout` is zero, the default entry is selected
/// without showing the menu. If `timeout` is [None], the menu waits for the user.
fn select<'a>(
&self,
timeout: Option<Duration>,
entries: &'a [BootableEntry],
) -> Result<&'a BootableEntry>;
}
/// Read a key from `input`, giving up once `timeout` passes.
/// If `timeout` is [None], this waits for a key indefinitely.
/// Returns the key that was pressed, or [None] if the timeout passed.
pub fn read_key(input: &mut Input, timeout: Option<Duration>) -> Result<Option<Key>> {
// The event to wait for a key press.
let key_event = input
.wait_for_key_event()
.context("unable to acquire key event")?;
// Timer event for timeout.
// SAFETY: The timer event creation allocated a timer pointer on the UEFI heap.
// This is validated safe as long as we are in boot services.
let timer_event = unsafe {
uefi::boot::create_event_ex(EventType::TIMER, Tpl::CALLBACK, None, None, None)
.context("unable to create timer event")?
};
let events = vec![timer_event, key_event];
// Wait for a key until the timer triggers.
// Store the result so that we can free the timer event.
let result = (|| {
// Set a timer to trigger after the specified duration.
// The timer is limited to what the firmware can represent, as a longer timeout
// can't be converted into a timer trigger.
// Without a timeout, the timer is never set, so it never triggers.
if let Some(timeout) = timeout {
let trigger = TimerTrigger::Relative(timeout.min(MAX_TIMER_DURATION));
uefi::boot::set_timer(&events[0], trigger).context("unable to set timeout timer")?;
}
loop {
// Wait for either the timer event or the key event to trigger.
let event = uefi::boot::wait_for_event(&events)
.discard_errdata()
.context("unable to wait for event")?;
// The first event is the timer event, so the timeout has passed.
if event == 0 {
return Ok(None);
}
// Some firmware signals the key event without a key being available.
// In that case, keep waiting on the same timer so the timeout is not extended.
if let Some(key) = input.read_key().context("unable to read key")? {
return Ok(Some(key));
}
}
})();
// 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() {
// 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 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 error from reading the key.
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 error from reading the key.
close_event_result?;
}
}
result
}
/// Shows a boot menu of the specified `style` to select a bootable entry to boot.
/// See [BootMenu::select] for how `timeout` is handled.
pub fn select<'live>(
timer: &'live PlatformTimer,
timeout: Option<Duration>,
style: MenuStyle,
entries: &'live [BootableEntry],
) -> Result<&'live BootableEntry> {
// Notify the bootloader interface that we are about to display the menu.
// This is only informational, so it should not prevent booting.
if let Err(error) = BootloaderInterface::mark_menu(timer) {
warn!(
"unable to mark menu display in bootloader interface: {:#}",
error
);
}
// Pick the menu that implements the requested style.
match style {
MenuStyle::Basic => BasicMenu.select(timeout, entries),
// The simple menu needs a console that can move the cursor and set colors, which
// not every console supports. If it fails, the basic menu is used instead.
MenuStyle::Simple => SimpleMenu.select(timeout, entries).or_else(|error| {
warn!(
"unable to show the simple boot menu, using the basic menu: {:#}",
error
);
BasicMenu.select(timeout, entries)
}),
}
}
+184
View File
@@ -0,0 +1,184 @@
use crate::entries::BootableEntry;
use crate::menu::{BootMenu, read_key};
use anyhow::{Context, Result};
use core::time::Duration;
use log::info;
use uefi::proto::console::text::{Input, Key, ScanCode};
/// The characters that can be used to select an entry from keys.
const ENTRY_NUMBER_TABLE: &[char] = &['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
/// A boot menu that prints the entries and selects them by number.
pub struct BasicMenu;
/// Represents the operation that can be performed by the boot menu.
#[derive(PartialEq, Eq)]
enum MenuOperation {
/// The user typed a digit of an entry number.
Digit(usize),
/// The user selected the escape key to exit the boot menu.
Exit,
/// The user selected the enter key to boot the typed entry number,
/// or to display the entries again if no number was typed.
Enter,
/// The user selected some other key to display the entries again.
Continue,
/// Timeout occurred.
Timeout,
/// No operation should be performed.
Nop,
}
/// Read a key from the input device with an optional timeout, returning the [MenuOperation]
/// that was performed. Without a timeout, this waits for a key indefinitely.
fn read(input: &mut Input, timeout: Option<Duration>) -> Result<MenuOperation> {
// If the timer triggered, the user did not select a numbered entry.
let Some(key) = read_key(input, timeout)? else {
return Ok(MenuOperation::Timeout);
};
match key {
Key::Printable(c) => {
// If the key is not ascii, we can't process it.
if !c.is_ascii() {
return Ok(MenuOperation::Continue);
}
// Convert the key to a char.
let c: char = c.into();
// Serial consoles may send a line feed instead of a carriage return.
if matches!(c, '\r' | '\n') {
return Ok(MenuOperation::Enter);
}
// Find the key pressed in the entry number table or continue.
Ok(ENTRY_NUMBER_TABLE
.iter()
.position(|&x| x == c)
.map(MenuOperation::Digit)
.unwrap_or(MenuOperation::Continue))
}
// The escape key is used to exit the boot menu.
Key::Special(ScanCode::ESCAPE) => Ok(MenuOperation::Exit),
// If the special key is unknown, do nothing.
Key::Special(_) => Ok(MenuOperation::Nop),
}
}
/// Find the default entry in `entries`.
fn default_entry(entries: &[BootableEntry]) -> Result<&BootableEntry> {
entries
.iter()
.find(|item| item.is_default())
.context("no default entry available")
}
/// Selects an entry from the list of entries using the boot menu.
fn select_with_input<'a>(
input: &mut Input,
timeout: Option<Duration>,
entries: &'a [BootableEntry],
) -> Result<&'a BootableEntry> {
// If the timeout is zero, boot the default entry without showing the menu.
if timeout.is_some_and(|timeout| timeout.is_zero()) {
return default_entry(entries);
}
// The time to wait for a key, or None once a key stops the countdown.
let mut countdown = timeout;
// The entry number typed so far, while more digits could still select another entry.
let mut typed: Option<usize> = None;
// Whether to print the entries before reading the next key.
let mut display = true;
loop {
if display {
// Print all the entries with the number used to select them.
info!("Boot Menu:");
for (index, entry) in entries.iter().enumerate() {
let title = entry.context().stamp(&entry.declaration().title);
info!(" [{}] {}", index, title);
}
info!("Select a boot entry using the number keys.");
if entries.len() > ENTRY_NUMBER_TABLE.len() {
info!("Press enter after the number if the entry does not boot right away.");
}
info!("Press Escape to exit and enter to display the entries again.");
display = false;
}
let operation = read(input, countdown)?;
// Any key stops the countdown, so the menu waits for the user from now on.
if operation != MenuOperation::Timeout {
countdown = None;
}
match operation {
// A digit of the entry number was typed.
MenuOperation::Digit(digit) => {
let number = typed
.take()
.unwrap_or(0)
.saturating_mul(10)
.saturating_add(digit);
// If another digit could select a different entry, wait for it or for enter.
if number != 0 && number.saturating_mul(10) < entries.len() {
typed = Some(number);
continue;
}
// Otherwise, boot the entry. If the number is invalid, we continue.
let Some(entry) = entries.get(number) else {
info!("invalid entry number");
continue;
};
return Ok(entry);
}
// Enter boots the typed entry number, or displays the entries again.
MenuOperation::Enter => {
let Some(number) = typed.take() else {
display = true;
continue;
};
let Some(entry) = entries.get(number) else {
info!("invalid entry number");
continue;
};
return Ok(entry);
}
// When the user exits the boot menu or a timeout occurs, we should
// boot the default entry, if any.
MenuOperation::Exit | MenuOperation::Timeout => {
return default_entry(entries);
}
// Any other key clears the typed number and displays the entries again.
MenuOperation::Continue => {
typed = None;
display = true;
}
// If the operation is nop, there is nothing to do.
MenuOperation::Nop => {}
}
}
}
impl BootMenu for BasicMenu {
/// Selects an entry using the basic menu.
/// The actual work is done internally in [select_with_input] which is called
/// within the context of the standard input device.
fn select<'a>(
&self,
timeout: Option<Duration>,
entries: &'a [BootableEntry],
) -> Result<&'a BootableEntry> {
uefi::system::with_stdin(move |input| select_with_input(input, timeout, entries))
}
}
+280
View File
@@ -0,0 +1,280 @@
use crate::entries::BootableEntry;
use crate::menu::{BootMenu, read_key};
use alloc::format;
use alloc::string::String;
use anyhow::{Context, Result, anyhow};
use core::fmt::Write;
use core::iter::repeat;
use core::time::Duration;
use uefi::proto::console::text::{Color, Input, Key, Output, ScanCode};
/// The number of columns to assume if the console does not report its mode.
const FALLBACK_COLUMNS: usize = 80;
/// The number of rows to assume if the console does not report its mode.
const FALLBACK_ROWS: usize = 25;
/// How often the countdown is updated.
const TICK: Duration = Duration::from_secs(1);
/// The number of spaces on either side of an entry title.
const ENTRY_PADDING: usize = 2;
/// The hint shown once the countdown has been stopped.
const HINT: &str = "Use the arrow keys to select an entry and press enter to boot.";
/// A full-screen boot menu that selects entries with the arrow keys, similar to systemd-boot.
pub struct SimpleMenu;
/// The position and size of the menu on the console.
struct Layout {
/// The number of rows on the console.
rows: usize,
/// The number of columns that can be drawn to.
columns: usize,
/// The column that entries start at.
x: usize,
/// The row that entries start at.
y: usize,
/// The width of each entry, including padding.
width: usize,
/// The number of entries that fit on the console.
visible: usize,
}
impl Layout {
/// Calculate the layout of `entries` on the console `output`.
fn new(output: &Output, entries: &[BootableEntry]) -> Self {
// Some consoles do not report a mode, so we assume a standard size for them.
let (columns, rows) = output
.current_mode()
.ok()
.flatten()
.map(|mode| (mode.columns(), mode.rows()))
.unwrap_or((FALLBACK_COLUMNS, FALLBACK_ROWS));
// Leave the last column empty, as writing to it makes some consoles scroll.
let columns = columns.saturating_sub(1);
// All entries are the width of the longest title so the highlight lines up.
let longest = entries
.iter()
.map(|entry| entry.title().chars().count())
.max()
.unwrap_or(0);
let width = (longest + ENTRY_PADDING * 2).min(columns);
// Keep the last two rows for the status line and a blank row above it.
let space = rows.saturating_sub(2).max(1);
let visible = entries.len().min(space);
Self {
rows,
columns,
x: (columns - width) / 2,
y: (space - visible) / 2,
width,
visible,
}
}
}
/// Draw `text` at `column` and `row`, padded with spaces or cut to fit `width`.
fn draw(
output: &mut Output,
column: usize,
row: usize,
width: usize,
text: &str,
colors: (Color, Color),
) -> Result<()> {
// Replace anything that isn't printable ASCII, as not all consoles can draw it.
let line: String = text
.chars()
.map(|c| {
if c == ' ' || c.is_ascii_graphic() {
c
} else {
'?'
}
})
.chain(repeat(' '))
.take(width)
.collect();
output
.set_color(colors.0, colors.1)
.context("unable to set console color")?;
output
.set_cursor_position(column, row)
.context("unable to set console cursor position")?;
output
.write_str(&line)
.map_err(|_| anyhow!("unable to write to console"))?;
Ok(())
}
/// Draw the visible `entries` starting at `offset` with `selected` highlighted,
/// and the `status` line at the bottom of the console.
fn render(
output: &mut Output,
layout: &Layout,
entries: &[BootableEntry],
selected: usize,
offset: usize,
status: &str,
) -> Result<()> {
for (row, (index, entry)) in entries
.iter()
.enumerate()
.skip(offset)
.take(layout.visible)
.enumerate()
{
// The selected entry is drawn in inverted colors.
let colors = if index == selected {
(Color::Black, Color::LightGray)
} else {
(Color::LightGray, Color::Black)
};
let text = format!("{:padding$}{}", "", entry.title(), padding = ENTRY_PADDING);
draw(
output,
layout.x,
layout.y + row,
layout.width,
&text,
colors,
)?;
}
// Center the status line across the console.
let padding = layout.columns.saturating_sub(status.len()) / 2;
let text = format!("{:padding$}{}", "", status);
draw(
output,
0,
layout.rows.saturating_sub(1),
layout.columns,
&text,
(Color::LightGray, Color::Black),
)
}
/// Run the menu until an entry is chosen, returning the index of that entry.
/// The `selected` entry is booted if no key is pressed before `timeout` passes.
/// Without a timeout, the menu waits for the user.
fn run(
input: &mut Input,
output: &mut Output,
layout: &Layout,
timeout: Option<Duration>,
entries: &[BootableEntry],
mut selected: usize,
) -> Result<usize> {
let last = entries.len() - 1;
// The first visible entry, which changes when scrolling through a long list.
let mut offset = 0;
// The time left before booting, or None once a key stops the countdown.
let mut remaining = timeout;
loop {
// Scroll so that the selected entry is visible.
if selected < offset {
offset = selected;
} else if selected >= offset + layout.visible {
offset = selected + 1 - layout.visible;
}
let status = match remaining {
Some(remaining) => format!("Boot in {} s.", remaining.as_secs()),
None => String::from(HINT),
};
render(output, layout, entries, selected, offset, &status)?;
// Wait for a key, waking up every tick to update the countdown.
let tick = remaining.map_or(TICK, |remaining| remaining.min(TICK));
let Some(key) = read_key(input, Some(tick))? else {
// Without a countdown there's nothing to do until a key is pressed.
let Some(left) = remaining else {
continue;
};
let left = left.saturating_sub(tick);
if left.is_zero() {
return Ok(selected);
}
remaining = Some(left);
continue;
};
// Any key stops the countdown, which leaves escape with nothing else to do.
remaining = None;
match key {
Key::Special(ScanCode::UP) => selected = selected.saturating_sub(1),
Key::Special(ScanCode::DOWN) => selected = (selected + 1).min(last),
Key::Special(ScanCode::HOME) => selected = 0,
Key::Special(ScanCode::END) => selected = last,
Key::Special(ScanCode::PAGE_UP) => selected = selected.saturating_sub(layout.visible),
Key::Special(ScanCode::PAGE_DOWN) => selected = (selected + layout.visible).min(last),
// Serial consoles may send a line feed instead of a carriage return.
Key::Printable(c) if matches!(char::from(c), '\r' | '\n') => return Ok(selected),
_ => {}
}
}
}
/// Selects an entry from `entries` using the console `input` and `output`.
fn select_with_console<'a>(
input: &mut Input,
output: &mut Output,
timeout: Option<Duration>,
entries: &'a [BootableEntry],
) -> Result<&'a BootableEntry> {
let default = entries
.iter()
.position(|entry| entry.is_default())
.context("no default entry available")?;
// If the timeout is zero, boot the default entry without showing the menu.
if timeout.is_some_and(|timeout| timeout.is_zero()) {
return Ok(&entries[default]);
}
let layout = Layout::new(output, entries);
let cursor_visible = output.cursor_visible();
output
.set_color(Color::LightGray, Color::Black)
.context("unable to set console color")?;
output.clear().context("unable to clear console")?;
// Not all consoles can hide the cursor, so this is allowed to fail.
let _ = output.enable_cursor(false);
let result = run(input, output, &layout, timeout, entries, default);
// Put the console back so that anything printed after the menu is readable.
let _ = output.set_color(Color::LightGray, Color::Black);
let _ = output.clear();
let _ = output.enable_cursor(cursor_visible);
Ok(&entries[result?])
}
impl BootMenu for SimpleMenu {
/// Selects an entry using the simple menu.
/// The actual work is done internally in [select_with_console] which is called
/// within the context of the standard input and output devices.
fn select<'a>(
&self,
timeout: Option<Duration>,
entries: &'a [BootableEntry],
) -> Result<&'a BootableEntry> {
uefi::system::with_stdin(|input| {
uefi::system::with_stdout(|output| select_with_console(input, output, timeout, entries))
})
}
}
+185
View File
@@ -0,0 +1,185 @@
use alloc::string::{String, ToString};
use anyhow::{Context, Result, anyhow, bail};
use core::cell::Cell;
use core::ptr::null_mut;
use edera_sprout_config::MenuStyle;
use jaarg::{
HelpWriter, HelpWriterContext, Opt, Opts, ParseControl, ParseResult, StandardFullHelpWriter,
};
use log::{info, warn};
use uefi_raw::Status;
/// Default configuration file path.
const DEFAULT_CONFIG_PATH: &str = "\\sprout.toml";
/// The parsed options of sprout.
#[derive(Debug)]
pub struct SproutOptions {
/// Configures Sprout automatically based on the environment.
pub autoconfigure: bool,
/// Path to a configuration file to load.
pub config: String,
/// Entry to boot without showing the boot menu.
pub boot: Option<String>,
/// Force display of the boot menu.
pub force_menu: bool,
/// The timeout for the boot menu in seconds.
pub menu_timeout: Option<u64>,
/// The style of boot menu to display.
pub menu_style: Option<MenuStyle>,
/// 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,
menu_style: 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,
MenuStyle,
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::value(ArgID::MenuStyle, &["--menu-style"], "STYLE")
.help_text("Boot menu style, basic or simple"),
Opt::flag(ArgID::RetainBootConsole, &["--retain-boot-console"])
.help_text("Retain boot console before boot"),
]);
// Acquire the arguments as determined by the UEFI core.
let mut args = eficore::env::args()?;
// Parse the arguments, dropping any argument that can't be parsed and parsing again.
// A typo in a boot entry or junk from the firmware should not prevent booting.
loop {
// Use the default value of sprout options and have the raw options be parsed into it.
let mut result = Self::default();
// The number of arguments handed to the parser, used to find the one it rejected.
let consumed = Cell::new(0usize);
// The reason the parser rejected an argument, if it did.
let mut rejection = None;
// Parse the OPTIONS into a map using jaarg.
let parsed = OPTIONS.parse(
"sprout",
args.iter().inspect(|_| consumed.set(consumed.get() + 1)),
|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.
match value.parse::<u64>() {
Ok(timeout) => result.menu_timeout = Some(timeout),
Err(error) => {
warn!("ignoring invalid {} value '{}': {}", name, value, error)
}
}
}
ArgID::MenuStyle => {
// The style of boot menu to display.
match value {
"basic" => result.menu_style = Some(MenuStyle::Basic),
"simple" => result.menu_style = Some(MenuStyle::Simple),
_ => warn!(
"ignoring invalid {} value '{}': expected basic or simple",
name, value
),
}
}
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| {
rejection = Some(error.to_string());
},
);
match parsed {
ParseResult::ContinueSuccess => return Ok(result),
ParseResult::ExitSuccess => unsafe {
uefi::boot::exit(uefi::boot::image_handle(), Status::SUCCESS, 0, null_mut())
.context("unable to exit")?;
return Err(anyhow!("unable to exit"));
},
ParseResult::ExitError => {
// The parser stops at the argument it rejected, which makes it the last
// argument it was handed. This is also true of an option missing its value,
// which is only detected once the arguments run out.
let Some(index) = consumed.get().checked_sub(1).filter(|i| *i < args.len())
else {
bail!("unable to parse options: {}", rejection.unwrap_or_default());
};
let arg = args.remove(index);
warn!(
"ignoring argument '{}': {}",
arg,
rejection.unwrap_or_default()
);
}
}
}
}
}
+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(())
}
+2
View File
@@ -0,0 +1,2 @@
sbat,1,SBAT Version,sbat,1,https://github.com/rhboot/shim/blob/main/SBAT.md
sprout,1,Edera,sprout,{version},https://sprout.edera.dev
1 sbat 1 SBAT Version sbat 1 https://github.com/rhboot/shim/blob/main/SBAT.md
2 sprout 1 Edera sprout {version} https://sprout.edera.dev
+2
View File
@@ -0,0 +1,2 @@
// Include the generated sbat section in this file.
include!(concat!(env!("OUT_DIR"), "/sbat.generated.rs"));
+12
View File
@@ -0,0 +1,12 @@
[package]
name = "edera-sprout-build"
description = "Sprout Build Tools"
license.workspace = true
version.workspace = true
homepage.workspace = true
repository.workspace = true
edition.workspace = true
[lib]
name = "edera_sprout_build"
path = "src/lib.rs"
+76
View File
@@ -0,0 +1,76 @@
use std::path::PathBuf;
use std::{env, fs};
/// Block size of the sbat section.
const SBAT_BLOCK_SIZE: usize = 512;
/// Template contents for the sbat.generated.rs file.
const SBAT_RS_TEMPLATE: &str = include_str!("sbat.template.rs");
/// Pad with zeros the given `data` to a multiple of `block_size`.
fn block_pad(data: &mut Vec<u8>, block_size: usize) {
let needed = data.len().div_ceil(block_size).max(1) * block_size;
if needed != data.len() {
data.resize(needed, 0);
}
}
/// Generate an .sbat link section module. This should be coupled with including the sbat module in
/// the crate that intends to embed the sbat section.
/// We intake a sbat.template.csv file in the calling crate and output a sbat.dat
/// which is included by a generated sbat.generated.rs file.
pub fn generate_sbat_module() {
// Notify Cargo that if the version changes, we need to regenerate the sbat.out file.
println!("cargo:rerun-if-env-changed=CARGO_PKG_VERSION");
// The version of the package.
let version = env::var("CARGO_PKG_VERSION").expect("CARGO_PKG_VERSION not set");
// The output directory to place the sbat.csv into.
let output_dir = PathBuf::from(env::var("OUT_DIR").expect("OUT_DIR not set"));
// The output path to the sbat.out file.
let out_file = output_dir.join("sbat.out");
// The output path to the sbat.generated.rs file.
let rs_file = output_dir.join("sbat.generated.rs");
// The path to the root of the crate.
let crate_root =
PathBuf::from(env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR not set"));
// The path to the sbat.template.tsv file is in the source directory of the crate.
let sbat_template_file = crate_root.join("src/sbat.csv");
// Notify Cargo that if sbat.csv changes, we need to regenerate the sbat.out file.
println!(
"cargo:rerun-if-changed={}",
sbat_template_file
.to_str()
.expect("unable to convert sbat template path file to a string")
);
// Read the sbat.csv template file.
let sbat_template =
fs::read_to_string(&sbat_template_file).expect("unable to read sbat.csv file");
// Replace the version placeholder in the template with the actual version.
let sbat = sbat_template.replace("{version}", &version);
// Encode the sbat.csv as bytes.
let mut encoded = sbat.as_bytes().to_vec();
// Pad the sbat.csv to the required block size.
block_pad(&mut encoded, SBAT_BLOCK_SIZE);
// Write the sbat.out file to the output directory.
fs::write(&out_file, &encoded).expect("unable to write sbat.out");
// Generate the contents of the sbat.generated.rs file.
// The size must tbe size of the encoded sbat.out file.
let sbat_rs = SBAT_RS_TEMPLATE.replace("{size}", &encoded.len().to_string());
// Write the sbat.generated.rs file to the output directory.
fs::write(&rs_file, sbat_rs).expect("unable to write sbat.generated.rs");
}
+6
View File
@@ -0,0 +1,6 @@
/// Define the SBAT attestation by including the sbat.csv file.
/// See this document for more details: <https://github.com/rhboot/shim/blob/main/SBAT.md>
/// NOTE: This data must be aligned by 512 bytes.
#[used]
#[unsafe(link_section = ".sbat")]
static SBAT: [u8; {size}] = *include_bytes!(concat!(env!("OUT_DIR"), "/sbat.out"));
+16
View File
@@ -0,0 +1,16 @@
[package]
name = "edera-sprout-config"
description = "Sprout Configuration"
license.workspace = true
version.workspace = true
homepage.workspace = true
repository.workspace = true
edition.workspace = true
[dependencies.serde]
workspace = true
default-features = false
[lib]
name = "edera_sprout_config"
path = "src/lib.rs"
+32
View File
@@ -0,0 +1,32 @@
use serde::{Deserialize, Serialize};
/// Configuration for the chainload action.
pub mod chainload;
/// Configuration for the edera action.
pub mod edera;
/// Configuration for the print action.
pub mod print;
/// Declares an action that sprout can execute.
/// Actions allow configuring sprout's internal runtime mechanisms with values
/// that you can specify via other concepts.
///
/// Actions are the main work that Sprout gets done, like booting Linux.
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
pub struct ActionDeclaration {
/// Chainload to another EFI application.
/// This allows you to load any EFI application, either to boot an operating system
/// or to perform more EFI actions and return to sprout.
#[serde(default)]
pub chainload: Option<chainload::ChainloadConfiguration>,
/// Print a string to the EFI console.
#[serde(default)]
pub print: Option<print::PrintConfiguration>,
/// Boot the Edera hypervisor and the root operating system.
/// This action is an extension on top of the Xen EFI stub that
/// is specific to Edera.
#[serde(default, rename = "edera")]
pub edera: Option<edera::EderaConfiguration>,
}
+26
View File
@@ -0,0 +1,26 @@
use alloc::string::String;
use alloc::vec::Vec;
use serde::{Deserialize, Serialize};
/// The configuration of the chainload action.
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
pub struct ChainloadConfiguration {
/// The path to the image to chainload.
/// This can be a Linux EFI stub (vmlinuz usually) or a standard EFI executable.
pub path: String,
/// The options to pass to the image.
/// The options are concatenated by a space and then passed to the EFI application.
#[serde(default)]
pub options: Vec<String>,
/// An optional path to a Linux initrd.
/// This uses the `LINUX_EFI_INITRD_MEDIA_GUID` mechanism to load the initrd into the EFI stack.
/// For Linux, you can also use initrd=\path\to\initrd as an option, but this option is
/// generally better and safer as it can support additional load options in the future.
#[serde(default, rename = "linux-initrd")]
pub linux_initrd: Option<String>,
/// An optional list of paths to Linux initrds to chain together.
/// The contents of each initrd are concatenated in order and loaded as a single initrd
/// using the same mechanism as `linux-initrd`. This cannot be used with `linux-initrd`.
#[serde(default, rename = "linux-initrd-chain")]
pub linux_initrd_chain: Vec<String>,
}
+28
View File
@@ -0,0 +1,28 @@
use alloc::string::String;
use alloc::vec::Vec;
use serde::{Deserialize, Serialize};
/// The configuration of the edera action which boots the Edera hypervisor.
/// Edera is based on Xen but modified significantly with a Rust stack.
/// Sprout is a component of the Edera stack and provides the boot functionality of Xen.
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
pub struct EderaConfiguration {
/// The path to the Xen hypervisor EFI image.
pub xen: String,
/// The path to the kernel to boot for dom0.
pub kernel: String,
/// The path to the initrd to load for dom0.
#[serde(default)]
pub initrd: Option<String>,
/// The paths to the initrds to chain together for dom0.
/// The contents of each initrd are concatenated in order and loaded as a single initrd.
/// This cannot be used with `initrd`.
#[serde(default, rename = "initrd-chain")]
pub initrd_chain: Vec<String>,
/// The options to pass to the kernel.
#[serde(default, rename = "kernel-options")]
pub kernel_options: Vec<String>,
/// The options to pass to the Xen hypervisor.
#[serde(default, rename = "xen-options")]
pub xen_options: Vec<String>,
}
+10
View File
@@ -0,0 +1,10 @@
use alloc::string::String;
use serde::{Deserialize, Serialize};
/// The configuration of the print action.
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
pub struct PrintConfiguration {
/// The text to print to the console.
#[serde(default)]
pub text: String,
}
+13
View File
@@ -0,0 +1,13 @@
use alloc::string::String;
use serde::{Deserialize, Serialize};
/// Declares a driver configuration.
/// Drivers allow extending the functionality of Sprout.
/// Drivers are loaded at runtime and can provide extra functionality like filesystem support.
/// Drivers are loaded by their name, which is used to reference them in other concepts.
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
pub struct DriverDeclaration {
/// The filesystem path to the driver.
/// This file should be an EFI executable that can be located and executed.
pub path: String,
}
@@ -1,13 +1,16 @@
use alloc::collections::BTreeMap;
use alloc::string::String;
use alloc::vec::Vec;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
/// 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, Default, Clone)]
#[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)]
@@ -15,4 +18,7 @@ pub struct EntryDeclaration {
/// The values to insert into the context when the entry is selected.
#[serde(default)]
pub values: BTreeMap<String, String>,
/// The key to sort entries, via version comparison.
#[serde(default, rename = "sort-key")]
pub sort_key: Option<String>,
}
+18
View File
@@ -0,0 +1,18 @@
use crate::extractors::filesystem_device_match::FilesystemDeviceMatchExtractor;
use serde::{Deserialize, Serialize};
/// Configuration for the filesystem-device-match extractor.
pub mod filesystem_device_match;
/// Declares an extractor configuration.
/// Extractors allow calculating values at runtime
/// using built-in sprout modules.
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
pub struct ExtractorDeclaration {
/// 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.
#[serde(default, rename = "filesystem-device-match")]
pub filesystem_device_match: Option<FilesystemDeviceMatchExtractor>,
}
@@ -0,0 +1,30 @@
use alloc::string::String;
use serde::{Deserialize, Serialize};
/// The filesystem device match extractor.
/// This extractor finds a filesystem using some search criteria and returns
/// the device root path that can concatenated with subpaths to access files
/// on a particular filesystem.
/// The fallback value can be used to provide a value if no match is found.
///
/// This extractor requires all the criteria to match. If no criteria is provided,
/// an error is returned.
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
pub struct FilesystemDeviceMatchExtractor {
/// Matches a filesystem that has the specified label.
#[serde(default, rename = "has-label")]
pub has_label: Option<String>,
/// Matches a filesystem that has the specified item.
/// An item is either a directory or file.
#[serde(default, rename = "has-item")]
pub has_item: Option<String>,
/// Matches a filesystem that has the specified partition UUID.
#[serde(default, rename = "has-partition-uuid")]
pub has_partition_uuid: Option<String>,
/// Matches a filesystem that has the specified partition type UUID.
#[serde(default, rename = "has-partition-type-uuid")]
pub has_partition_type_uuid: Option<String>,
/// The fallback value to use if no filesystem matches the criteria.
#[serde(default)]
pub fallback: Option<String>,
}
+40
View File
@@ -0,0 +1,40 @@
use crate::generators::bls::BlsConfiguration;
use crate::generators::list::ListConfiguration;
use crate::generators::matrix::MatrixConfiguration;
use serde::{Deserialize, Serialize};
/// Configuration for the BLS generator.
pub mod bls;
/// Configuration for the list generator.
pub mod list;
/// Configuration for the matrix generator.
pub mod matrix;
/// Declares a generator configuration.
/// Generators allow generating entries at runtime based on a set of data.
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
pub struct GeneratorDeclaration {
/// Matrix generator configuration.
/// Matrix allows you to specify multiple value-key values as arrays.
/// This allows multiplying the number of entries by any number of possible
/// configuration options. For example,
/// data.x = ["a", "b"]
/// data.y = ["c", "d"]
/// would generate an entry for each of these combinations:
/// x = a, y = c
/// x = a, y = d
/// x = b, y = c
/// x = b, y = d
#[serde(default)]
pub matrix: Option<MatrixConfiguration>,
/// BLS generator configuration.
/// BLS allows you to pass a filesystem path that contains a set of BLS entries.
/// It will generate a sprout entry for every supported BLS entry.
#[serde(default)]
pub bls: Option<BlsConfiguration>,
/// List generator configuration.
/// Allows you to specify a list of values to generate an entry from.
pub list: Option<ListConfiguration>,
}
+22
View File
@@ -0,0 +1,22 @@
use crate::entries::EntryDeclaration;
use alloc::string::{String, ToString};
use serde::{Deserialize, Serialize};
/// The default path to the BLS directory.
const BLS_TEMPLATE_PATH: &str = "\\loader";
/// The configuration of the BLS generator.
/// The BLS uses the Bootloader Specification to produce
/// entries from an input template.
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
pub struct BlsConfiguration {
/// The entry to use for as a template.
pub entry: EntryDeclaration,
/// The path to the BLS directory.
#[serde(default = "default_bls_path")]
pub path: String,
}
fn default_bls_path() -> String {
BLS_TEMPLATE_PATH.to_string()
}
+18
View File
@@ -0,0 +1,18 @@
use crate::entries::EntryDeclaration;
use alloc::collections::BTreeMap;
use alloc::string::String;
use alloc::vec::Vec;
use serde::{Deserialize, Serialize};
/// List generator configuration.
/// The list generator produces multiple entries based
/// on a set of input maps.
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
pub struct ListConfiguration {
/// The template entry to use for each generated entry.
#[serde(default)]
pub entry: EntryDeclaration,
/// The values to use as the input for the matrix.
#[serde(default)]
pub values: Vec<BTreeMap<String, String>>,
}
+18
View File
@@ -0,0 +1,18 @@
use crate::entries::EntryDeclaration;
use alloc::collections::BTreeMap;
use alloc::string::String;
use alloc::vec::Vec;
use serde::{Deserialize, Serialize};
/// Matrix generator configuration.
/// The matrix generator produces multiple entries based
/// on input values multiplicatively.
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
pub struct MatrixConfiguration {
/// The template entry to use for each generated entry.
#[serde(default)]
pub entry: EntryDeclaration,
/// The values to use as the input for the matrix.
#[serde(default)]
pub values: BTreeMap<String, Vec<String>>,
}
+174
View File
@@ -0,0 +1,174 @@
//! Sprout configuration descriptions.
//! This crate provides all the configuration structures for Sprout.
#![no_std]
extern crate alloc;
use crate::actions::ActionDeclaration;
use crate::drivers::DriverDeclaration;
use crate::entries::EntryDeclaration;
use crate::extractors::ExtractorDeclaration;
use crate::generators::GeneratorDeclaration;
use crate::phases::PhasesConfiguration;
use alloc::collections::BTreeMap;
use alloc::string::String;
use serde::{Deserialize, Serialize};
pub mod actions;
pub mod drivers;
pub mod entries;
pub mod extractors;
pub mod generators;
pub mod phases;
/// This is the latest version of the sprout configuration format.
/// This must be incremented when the configuration breaks compatibility.
pub const LATEST_VERSION: u32 = 1;
/// The default timeout for the boot menu in seconds.
pub const DEFAULT_MENU_TIMEOUT_SECONDS: u64 = 10;
/// The Sprout configuration format.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct RootConfiguration {
/// The version of the configuration. This should always be declared
/// and be the latest version that is supported. If not specified, it is assumed
/// the configuration is the latest version.
#[serde(default = "latest_version")]
pub version: u32,
/// Default options for Sprout.
#[serde(default)]
pub options: OptionsConfiguration,
/// Values to be inserted into the root sprout context.
#[serde(default)]
pub values: BTreeMap<String, String>,
/// Drivers to load.
/// These drivers provide extra functionality like filesystem support to Sprout.
/// Each driver has a name which uniquely identifies it inside Sprout.
#[serde(default)]
pub drivers: BTreeMap<String, DriverDeclaration>,
/// Declares the extractors that add values to the sprout context that are calculated
/// at runtime. Each extractor has a name which corresponds to the value it will set
/// inside the sprout context.
#[serde(default)]
pub extractors: BTreeMap<String, ExtractorDeclaration>,
/// Declares the actions that can execute operations for sprout.
/// Actions are executable modules in sprout that take in specific structured values.
/// Actions are responsible for ensuring that passed strings are stamped to replace values
/// at runtime.
/// Each action has a name that can be referenced by other base concepts like entries.
#[serde(default)]
pub actions: BTreeMap<String, ActionDeclaration>,
/// Declares the entries that are displayed on the boot menu. These entries are static
/// but can still use values from the sprout context.
#[serde(default)]
pub entries: BTreeMap<String, EntryDeclaration>,
/// Declares the generators that are used to generate entries at runtime.
/// Each generator has its own logic for generating entries, but generally they intake
/// a template entry and stamp that template entry over some values determined at runtime.
/// Each generator has an associated name used to differentiate it across sprout.
#[serde(default)]
pub generators: BTreeMap<String, GeneratorDeclaration>,
/// Configures the various phases of sprout. This allows you to hook into specific parts
/// of the boot process to execute actions, for example, you can show a boot splash during
/// the early phase.
#[serde(default)]
pub phases: PhasesConfiguration,
}
/// Options configuration for Sprout, used when the corresponding options are not specified.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct OptionsConfiguration {
/// The entry to mark as the default entry, instead of the first entry.
#[serde(rename = "default-entry", default)]
pub default_entry: Option<String>,
/// The timeout of the boot menu.
#[serde(rename = "menu-timeout", default = "default_menu_timeout")]
pub menu_timeout: u64,
/// The style of boot menu to display.
#[serde(rename = "menu-style", default)]
pub menu_style: MenuStyle,
/// Enables autoconfiguration of Sprout based on the environment.
#[serde(default)]
pub autoconfigure: bool,
}
/// The default configuration, which matches an empty configuration file.
impl Default for RootConfiguration {
fn default() -> Self {
Self {
version: latest_version(),
options: Default::default(),
values: Default::default(),
drivers: Default::default(),
extractors: Default::default(),
actions: Default::default(),
entries: Default::default(),
generators: Default::default(),
phases: Default::default(),
}
}
}
/// The default options, which match an empty options section.
impl Default for OptionsConfiguration {
fn default() -> Self {
Self {
default_entry: None,
menu_timeout: default_menu_timeout(),
menu_style: Default::default(),
autoconfigure: false,
}
}
}
/// The style of boot menu to display.
#[derive(Serialize, Deserialize, Debug, Default, Clone, Copy, PartialEq, Eq)]
pub enum MenuStyle {
/// A basic menu that prints the entries and selects them by number.
#[serde(rename = "basic")]
Basic,
/// A simple full-screen menu that selects entries with the arrow keys.
#[default]
#[serde(rename = "simple")]
Simple,
}
/// Get the latest version of the Sprout configuration format.
pub fn latest_version() -> u32 {
LATEST_VERSION
}
fn default_menu_timeout() -> u64 {
DEFAULT_MENU_TIMEOUT_SECONDS
}
#[cfg(test)]
mod tests {
use super::*;
use serde::de::value::{Error, MapDeserializer};
/// Deserialize a configuration from an empty map, like an empty configuration file.
fn empty() -> RootConfiguration {
let map = MapDeserializer::<_, Error>::new(core::iter::empty::<(&str, &str)>());
RootConfiguration::deserialize(map).expect("empty configuration should deserialize")
}
#[test]
fn default_matches_empty_configuration() {
let default = RootConfiguration::default();
let empty = empty();
assert_eq!(default.version, empty.version);
assert_eq!(default.options.menu_timeout, empty.options.menu_timeout);
assert_eq!(default.options.menu_style, empty.options.menu_style);
assert_eq!(default.options.default_entry, empty.options.default_entry);
assert_eq!(default.options.autoconfigure, empty.options.autoconfigure);
}
#[test]
fn default_menu_timeout_is_not_zero() {
assert_eq!(
RootConfiguration::default().options.menu_timeout,
DEFAULT_MENU_TIMEOUT_SECONDS
);
}
}
+32
View File
@@ -0,0 +1,32 @@
use alloc::collections::BTreeMap;
use alloc::string::String;
use alloc::vec::Vec;
use serde::{Deserialize, Serialize};
/// Configures the various phases of the boot process.
/// This allows hooking various phases to run actions.
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
pub struct PhasesConfiguration {
/// The early phase is run before drivers are loaded.
#[serde(default)]
pub early: Vec<PhaseConfiguration>,
/// The startup phase is run after drivers are loaded, but before entries are displayed.
#[serde(default)]
pub startup: Vec<PhaseConfiguration>,
/// The late phase is run after the entry is chosen, but before the actions are executed.
#[serde(default)]
pub late: Vec<PhaseConfiguration>,
}
/// Configures a single phase of the boot process.
/// There can be multiple phase configurations that are
/// executed sequentially.
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
pub struct PhaseConfiguration {
/// The actions to run when the phase is executed.
#[serde(default)]
pub actions: Vec<String>,
/// The values to insert into the context when the phase is executed.
#[serde(default)]
pub values: BTreeMap<String, String>,
}
+22
View File
@@ -0,0 +1,22 @@
[package]
name = "edera-sprout-eficore"
description = "Sprout EFI Core"
license.workspace = true
version.workspace = true
homepage.workspace = true
repository.workspace = true
edition.workspace = true
[dependencies]
anyhow.workspace = true
bitflags.workspace = true
log.workspace = true
shlex.workspace = true
spin.workspace = true
uefi.workspace = true
uefi-raw.workspace = true
[lib]
name = "eficore"
path = "src/lib.rs"
test = false
+338
View File
@@ -0,0 +1,338 @@
use crate::bootloader_interface::bitflags::LoaderFeatures;
use crate::platform::timer::PlatformTimer;
use crate::variables::{VariableClass, VariableController};
use alloc::format;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use anyhow::{Context, Result};
use log::warn;
use uefi::proto::device_path::DevicePath;
use uefi::{Guid, guid};
use uefi_raw::table::runtime::VariableVendor;
/// bitflags: LoaderFeatures bitflags.
mod bitflags;
/// The name of the bootloader to tell the system.
const LOADER_NAME: &str = "Sprout";
/// Represents the configured timeout for the bootloader interface.
#[derive(Default)]
pub enum BootloaderInterfaceTimeout {
/// Force the menu to be shown, and wait for the user.
MenuForce,
/// Force the menu to be shown, with a timeout.
MenuForceTimeout(u64),
/// Hide the menu.
MenuHidden,
/// Disable the menu.
MenuDisabled,
/// Set a timeout for the menu.
Timeout(u64),
/// Timeout is unspecified.
#[default]
Unspecified,
}
/// Bootloader Interface support.
pub struct BootloaderInterface;
impl BootloaderInterface {
/// Bootloader Interface GUID from https://systemd.io/BOOT_LOADER_INTERFACE
const VENDOR: VariableController = VariableController::new(VariableVendor(guid!(
"4a67b082-0a4c-41cf-b6c7-440b29bb8c4f"
)));
/// The feature we support in Sprout.
fn features() -> LoaderFeatures {
LoaderFeatures::Xbootldr
| LoaderFeatures::LoadDriver
| LoaderFeatures::Tpm2ActivePcrBanks
| LoaderFeatures::RetainShim
| LoaderFeatures::ConfigTimeout
| LoaderFeatures::ConfigTimeoutOneShot
| LoaderFeatures::MenuDisable
| LoaderFeatures::EntryDefault
| LoaderFeatures::EntryOneShot
}
/// Tell the system that Sprout was initialized at the current time.
pub fn mark_init(timer: &PlatformTimer) -> Result<()> {
Self::mark_time("LoaderTimeInitUSec", timer)
}
/// Tell the system that Sprout is about to execute the boot entry.
pub fn mark_exec(timer: &PlatformTimer) -> Result<()> {
Self::mark_time("LoaderTimeExecUSec", timer)
}
/// Tell the system that Sprout is about to display the menu.
pub fn mark_menu(timer: &PlatformTimer) -> Result<()> {
Self::mark_time("LoaderTimeMenuUSec", timer)
}
/// Tell the system about the current time as measured by the platform timer.
/// Sets the variable specified by `key` to the number of microseconds.
fn mark_time(key: &str, timer: &PlatformTimer) -> Result<()> {
// Measure the elapsed time since the hardware timer was started.
let elapsed = timer.elapsed_since_lifetime();
Self::VENDOR.set_cstr16(
key,
&elapsed.as_micros().to_string(),
VariableClass::BootAndRuntimeTemporary,
)
}
/// Tell the system what loader is being used and our features.
pub fn set_loader_info() -> Result<()> {
// Set the LoaderInfo variable with the name of the loader.
Self::VENDOR
.set_cstr16(
"LoaderInfo",
LOADER_NAME,
VariableClass::BootAndRuntimeTemporary,
)
.context("unable to set loader info variable")?;
// Set the LoaderFeatures variable with the features we support.
Self::VENDOR
.set_u64le(
"LoaderFeatures",
Self::features().bits(),
VariableClass::BootAndRuntimeTemporary,
)
.context("unable to set loader features variable")?;
Ok(())
}
/// Tell the system the relative path to the partition root of the current bootloader.
pub fn set_loader_path(path: &DevicePath) -> Result<()> {
let subpath =
crate::path::device_path_subpath(path).context("unable to get loader path subpath")?;
Self::VENDOR.set_cstr16(
"LoaderImageIdentifier",
&subpath,
VariableClass::BootAndRuntimeTemporary,
)
}
/// Tell the system what the partition GUID of the ESP Sprout was booted from is.
pub fn set_partition_guid(guid: &Guid) -> Result<()> {
Self::VENDOR.set_cstr16(
"LoaderDevicePartUUID",
&guid.to_string(),
VariableClass::BootAndRuntimeTemporary,
)
}
/// Tell the system what boot entries are available.
pub fn set_entries<N: AsRef<str>>(entries: impl Iterator<Item = N>) -> Result<()> {
// Entries are stored as a null-terminated list of CString16 strings back to back.
// Iterate over the entries and convert them to CString16 placing them into data.
let mut data = Vec::new();
for entry in entries {
// Convert the entry to CString16 little endian.
let encoded = entry
.as_ref()
.encode_utf16()
.flat_map(|c| c.to_le_bytes())
.collect::<Vec<u8>>();
// Write the bytes into the data buffer.
data.extend_from_slice(&encoded);
// Add a null terminator to the end of the entry.
data.extend_from_slice(&[0, 0]);
}
// If no data was generated, we will do nothing.
if data.is_empty() {
return Ok(());
}
Self::VENDOR.set(
"LoaderEntries",
&data,
VariableClass::BootAndRuntimeTemporary,
)
}
/// Tell the system what the selected boot entry is.
pub fn set_selected_entry(entry: String) -> Result<()> {
Self::VENDOR.set_cstr16(
"LoaderEntrySelected",
&entry,
VariableClass::BootAndRuntimeTemporary,
)
}
/// Tell the system about the UEFI firmware we are running on.
pub fn set_firmware_info() -> Result<()> {
// Access the firmware revision.
let firmware_revision = uefi::system::firmware_revision();
// Access the UEFI revision.
let uefi_revision = uefi::system::uefi_revision();
// Format the firmware information string into something human-readable.
let firmware_info = format!(
"{} {}.{:02}",
uefi::system::firmware_vendor(),
firmware_revision >> 16,
firmware_revision & 0xffff,
);
Self::VENDOR.set_cstr16(
"LoaderFirmwareInfo",
&firmware_info,
VariableClass::BootAndRuntimeTemporary,
)?;
// Format the firmware revision into something human-readable.
let firmware_type = format!(
"UEFI {}.{:02}",
uefi_revision.major(),
uefi_revision.minor()
);
Self::VENDOR.set_cstr16(
"LoaderFirmwareType",
&firmware_type,
VariableClass::BootAndRuntimeTemporary,
)
}
/// Tell the system what the number of active PCR banks is.
/// If this is zero, that is okay.
pub fn set_tpm2_active_pcr_banks(value: u32) -> Result<()> {
// Format the value into the specification format.
let value = format!("0x{:08x}", value);
Self::VENDOR.set_cstr16(
"LoaderTpm2ActivePcrBanks",
&value,
VariableClass::BootAndRuntimeTemporary,
)
}
/// Retrieve the timeout value from the bootloader interface, using the specified `key`.
/// `remove` indicates whether, when found, we remove the variable.
fn get_timeout_value(key: &str, remove: bool) -> Result<Option<BootloaderInterfaceTimeout>> {
// Retrieve the timeout value from the bootloader interface.
let Some(value) = Self::VENDOR
.get_cstr16(key)
.context("unable to get timeout value")?
else {
return Ok(None);
};
// If we reach here, we know the value was specified.
// If `remove` is true, remove the variable.
// If removal fails, the value is still used, as it was meant for this boot.
if remove && let Err(error) = Self::VENDOR.remove(key) {
warn!("unable to remove {} variable: {:#}", key, error);
}
// If the value is empty, return Unspecified.
if value.is_empty() {
return Ok(Some(BootloaderInterfaceTimeout::Unspecified));
}
// If the value is "menu-force", return MenuForce.
if value == "menu-force" {
return Ok(Some(BootloaderInterfaceTimeout::MenuForce));
}
// If the value is "menu-hidden", return MenuHidden.
if value == "menu-hidden" {
return Ok(Some(BootloaderInterfaceTimeout::MenuHidden));
}
// If the value is "menu-disabled", return MenuDisabled.
if value == "menu-disabled" {
return Ok(Some(BootloaderInterfaceTimeout::MenuDisabled));
}
// Parse the value as a u64 to decode an numeric value.
// An invalid value is ignored, as a bad variable should not prevent booting.
let value = match value.parse::<u64>() {
Ok(value) => value,
Err(error) => {
warn!("ignoring invalid {} value '{}': {}", key, value, error);
return Ok(None);
}
};
// If we reach here, we know it must be a real timeout value.
// A value of 0 is left to the caller, as its meaning depends on the variable.
Ok(Some(BootloaderInterfaceTimeout::Timeout(value)))
}
/// Get the timeout from the bootloader interface.
/// This indicates how the menu should behave.
/// If no values are set, Unspecified is returned.
pub fn get_timeout() -> Result<BootloaderInterfaceTimeout> {
// Attempt to acquire the value of the LoaderConfigTimeoutOneShot variable.
// This should take precedence over the LoaderConfigTimeout variable.
let oneshot = Self::get_timeout_value("LoaderConfigTimeoutOneShot", true)
.context("unable to check for LoaderConfigTimeoutOneShot variable")?;
// If oneshot was found, return it.
// The specification says the one-shot timeout shows the menu on this boot,
// and a value of 0 means the menu waits for the user.
if let Some(oneshot) = oneshot {
return Ok(match oneshot {
BootloaderInterfaceTimeout::Timeout(0) => BootloaderInterfaceTimeout::MenuForce,
BootloaderInterfaceTimeout::Timeout(value) => {
BootloaderInterfaceTimeout::MenuForceTimeout(value)
}
other => other,
});
}
// Attempt to acquire the value of the LoaderConfigTimeout variable.
// This will be used if the LoaderConfigTimeoutOneShot variable is not set.
let direct = Self::get_timeout_value("LoaderConfigTimeout", false)
.context("unable to check for LoaderConfigTimeout variable")?;
// If direct was found, return it.
// The specification says that a value of 0 means that the menu should be hidden.
if let Some(direct) = direct {
return Ok(match direct {
BootloaderInterfaceTimeout::Timeout(0) => BootloaderInterfaceTimeout::MenuHidden,
other => other,
});
}
// If we reach here, we know that neither variable was set.
// We provide the unspecified value instead.
Ok(BootloaderInterfaceTimeout::Unspecified)
}
/// Get the default entry set by the bootloader interface.
pub fn get_default_entry() -> Result<Option<String>> {
// An empty value is treated as if the default entry was not set.
Ok(Self::VENDOR
.get_cstr16("LoaderEntryDefault")
.context("unable to get default entry from bootloader interface")?
.filter(|value| !value.is_empty()))
}
/// Get the oneshot entry set by the bootloader interface.
/// This should be the entry we boot.
pub fn get_oneshot_entry() -> Result<Option<String>> {
// Acquire the value of the LoaderEntryOneShot variable.
// If it is not set, return None.
let Some(value) = Self::VENDOR
.get_cstr16("LoaderEntryOneShot")
.context("unable to get oneshot entry from bootloader interface")?
else {
return Ok(None);
};
// Remove the oneshot entry from the bootloader interface.
// If removal fails, the value is still used, as it was meant for this boot.
if let Err(error) = Self::VENDOR.remove("LoaderEntryOneShot") {
warn!("unable to remove LoaderEntryOneShot variable: {:#}", error);
}
// Return the oneshot value.
// An empty value is treated as if the oneshot entry was not set.
Ok(Some(value).filter(|value| !value.is_empty()))
}
}
@@ -0,0 +1,46 @@
use bitflags::bitflags;
bitflags! {
/// Feature bitflags for the bootloader interface.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct LoaderFeatures: u64 {
/// Bootloader supports LoaderConfigTimeout.
const ConfigTimeout = 1 << 0;
/// Bootloader supports LoaderConfigTimeoutOneShot.
const ConfigTimeoutOneShot = 1 << 1;
/// Bootloader supports LoaderEntryDefault.
const EntryDefault = 1 << 2;
/// Bootloader supports LoaderEntryOneShot.
const EntryOneShot = 1 << 3;
/// Bootloader supports boot counting.
const BootCounting = 1 << 4;
/// Bootloader supports detection from XBOOTLDR partitions.
const Xbootldr = 1 << 5;
/// Bootloader supports the handling of random seeds.
const RandomSeed = 1 << 6;
/// Bootloader supports loading drivers.
const LoadDriver = 1 << 7;
/// Bootloader supports sort keys.
const SortKey = 1 << 8;
/// Bootloader supports saved entries.
const SavedEntry = 1 << 9;
/// Bootloader supports device trees.
const DeviceTree = 1 << 10;
/// Bootloader supports secure boot enroll.
const SecureBootEnroll = 1 << 11;
/// Bootloader retains the shim.
const RetainShim = 1 << 12;
/// Bootloader supports disabling the menu via the menu timeout variable.
const MenuDisable = 1 << 13;
/// Bootloader supports multi-profile UKI.
const MultiProfileUki = 1 << 14;
/// Bootloader reports URLs.
const ReportUrl = 1 << 15;
/// Bootloader supports type-1 UKIs.
const Type1Uki = 1 << 16;
/// Bootloader supports type-1 UKI urls.
const Type1UkiUrl = 1 << 17;
/// Bootloader indicates TPM2 active PCR banks.
const Tpm2ActivePcrBanks = 1 << 18;
}
}
+68
View File
@@ -0,0 +1,68 @@
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use anyhow::{Context, Result};
use uefi::proto::loaded_image::LoadedImage;
/// Loads the command-line arguments passed to the current image.
pub fn args() -> Result<Vec<String>> {
// Acquire the current image handle.
let handle = uefi::boot::image_handle();
// Open the LoadedImage protocol for the current image.
let loaded_image = uefi::boot::open_protocol_exclusive::<LoadedImage>(handle)
.context("unable to open loaded image protocol for current image")?;
// Load the command-line argument string.
// Load options are usually a null-terminated UCS-2 string, but firmware is not consistent
// about this. Some firmware passes an odd number of bytes, leaves off the null terminator,
// or pads the string with extra nulls. We decode everything up to the first null and
// replace anything that isn't valid UCS-2, instead of refusing to boot.
let Some(options) = loaded_image.load_options_as_bytes() else {
// No load options were passed. We will return an empty vector.
return Ok(Vec::new());
};
// Decode the options as little-endian UTF-16, stopping at the first null.
// A trailing odd byte can't be part of a character, so it is dropped.
let options = options
.as_chunks::<2>()
.0
.iter()
.map(|pair| u16::from_le_bytes(*pair))
.take_while(|c| *c != 0)
.collect::<Vec<u16>>();
let options = String::from_utf16_lossy(&options);
// Use shlex to parse the options.
// If shlex fails, we will perform a simple whitespace split.
let mut args = shlex::split(&options).unwrap_or_else(|| {
options
.split_ascii_whitespace()
.map(|string| string.to_string())
.collect::<Vec<_>>()
});
// Correct firmware that may add invalid arguments at the start.
// Witnessed this on a Dell Precision 5690 when direct booting.
args = args
.into_iter()
.skip_while(|arg| {
arg.chars()
.next()
// Filter out unprintable characters and backticks.
// Both of which have been observed in the wild.
.map(|c| c.is_ascii_control() || c == '`')
.unwrap_or(false)
})
.collect();
// If there is a first argument, check if it is not an option.
// If it is not, we will assume it is the path to the executable and remove it.
if let Some(arg) = args.first()
&& !arg.starts_with('-')
{
args.remove(0);
}
Ok(args)
}
+58
View File
@@ -0,0 +1,58 @@
use alloc::vec;
use alloc::vec::Vec;
use anyhow::{Context, Result};
use uefi::proto::console::gop::{BltOp, BltPixel, BltRegion, GraphicsOutput};
/// Represents the EFI framebuffer.
pub struct Framebuffer {
/// The width of the framebuffer in pixels.
width: usize,
/// The height of the framebuffer in pixels.
height: usize,
/// The pixels of the framebuffer.
pixels: Vec<BltPixel>,
}
impl Framebuffer {
/// Creates a new framebuffer of the specified `width` and `height`.
pub fn new(width: usize, height: usize) -> Result<Self> {
// Verify that the size is valid during multiplication.
let size = width
.checked_mul(height)
.context("framebuffer size overflow")?;
// Initialize the pixel buffer with black pixels, with the verified size.
let pixels = vec![BltPixel::new(0, 0, 0); size];
Ok(Framebuffer {
width,
height,
pixels,
})
}
/// Mutably acquires a pixel of the framebuffer at the specified `x` and `y` coordinate.
pub fn pixel(&mut self, x: usize, y: usize) -> Option<&mut BltPixel> {
// Verify that the coordinates are within the bounds of the framebuffer.
if x >= self.width || y >= self.height {
return None;
}
// Calculate the index of the pixel safely, returning None if it overflows.
let index = y.checked_mul(self.width)?.checked_add(x)?;
// Return the pixel at the index. If the index is out of bounds, this will return None.
self.pixels.get_mut(index)
}
/// Blit the framebuffer to the specified `gop` [GraphicsOutput].
pub fn blit(&self, gop: &mut GraphicsOutput) -> Result<()> {
gop.blt(BltOp::BufferToVideo {
buffer: &self.pixels,
src: BltRegion::Full,
dest: (0, 0),
dims: (self.width, self.height),
})
.context("unable to blit framebuffer")?;
Ok(())
}
}
+26
View File
@@ -0,0 +1,26 @@
use anyhow::{Context, Result};
use uefi::boot::SearchType;
use uefi::{Guid, Handle};
use uefi_raw::Status;
/// Find a handle that provides the specified `protocol`.
pub fn find_handle(protocol: &Guid) -> Result<Option<Handle>> {
// Locate the requested protocol handle.
match uefi::boot::locate_handle_buffer(SearchType::ByProtocol(protocol)) {
// If a handle is found, the protocol is available.
Ok(handles) => Ok(if handles.is_empty() {
None
} else {
Some(handles[0])
}),
// If an error occurs, check if it is because the protocol is not available.
// If so, return false. Otherwise, return the error.
Err(error) => {
if error.status() == Status::NOT_FOUND {
Ok(None)
} else {
Err(error).context("unable to determine if the protocol is available")
}
}
}
}
+47
View File
@@ -0,0 +1,47 @@
//! Sprout EFI core.
//! This crate provides core EFI functionality for Sprout.
// For some reason this triggers, and I can't figure out why.
#![allow(rustdoc::bare_urls)]
#![no_std]
extern crate alloc;
/// EFI handle helpers.
pub mod handle;
/// Load and start EFI images.
pub mod loader;
/// Logging support for EFI applications.
pub mod logger;
/// Disk partitioning support infrastructure.
pub mod partition;
/// Path handling for UEFI.
pub mod path;
/// platform: Integration or support code for specific hardware platforms.
pub mod platform;
/// Secure Boot support.
pub mod secure;
/// Support for the shim loader application that enables Secure Boot.
pub mod shim;
/// String utilities.
pub mod strings;
/// Implements support for the bootloader interface specification.
pub mod bootloader_interface;
/// Acquire arguments from UEFI environment.
pub mod env;
/// Support code for the EFI framebuffer.
pub mod framebuffer;
/// Support code for the media loader protocol.
pub mod media_loader;
/// setup: Code that initializes the UEFI environment for Sprout.
pub mod setup;
/// Support code for EFI variables.
pub mod variables;
+143
View File
@@ -0,0 +1,143 @@
use crate::loader::source::ImageSource;
use crate::secure::SecureBoot;
use crate::shim::hook::SecurityHook;
use crate::shim::{ShimInput, ShimSupport};
use anyhow::{Context, Result, bail};
use log::warn;
use uefi::Handle;
use uefi::boot::LoadImageSource;
/// Represents EFI image sources generically.
pub mod source;
/// Handle to a loaded EFI image.
pub struct ImageHandle {
/// Handle to the loaded image.
handle: Handle,
}
impl ImageHandle {
/// Create a new image handle based on a handle from the UEFI stack.
pub fn new(handle: Handle) -> Self {
Self { handle }
}
/// Retrieve the underlying handle.
pub fn handle(&self) -> &Handle {
&self.handle
}
}
/// Request to load an image from a source, with support for additional validation features.
pub struct ImageLoadRequest<'source> {
/// Handle to the current image.
current_image: Handle,
/// Source of the image to load.
source: ImageSource<'source>,
}
impl<'source> ImageLoadRequest<'source> {
/// Create a new image load request with a current image and a source.
pub fn new(current_image: Handle, source: ImageSource<'source>) -> Self {
Self {
current_image,
source,
}
}
/// Retrieve the current image.
pub fn current_image(&self) -> &Handle {
&self.current_image
}
/// Retrieve the source of the image to load.
pub fn source(&'source self) -> &'source ImageSource<'source> {
&self.source
}
/// Convert the request into a source.
pub fn into_source(self) -> ImageSource<'source> {
self.source
}
}
/// EFI image loader.
pub struct ImageLoader;
impl ImageLoader {
/// Load an image using the image `request` which allows
pub fn load(request: ImageLoadRequest) -> Result<ImageHandle> {
// Determine whether Secure Boot is enabled.
let secure_boot =
SecureBoot::enabled().context("unable to determine if secure boot is enabled")?;
// Determine whether the shim is loaded.
let shim_loaded = ShimSupport::loaded().context("unable to determine if shim is loaded")?;
// Determine whether the shim loader is available.
let shim_loader_available = ShimSupport::loader_available()
.context("unable to determine if shim loader is available")?;
// Determines whether LoadImage in Boot Services must be patched.
// Version 16 of the shim doesn't require extra effort to load Secure Boot binaries.
// If the image loader is installed, we can skip over the security hook.
let requires_security_hook = secure_boot && shim_loaded && !shim_loader_available;
// If the shim is loaded, we will need to retain the shim protocol to allow
// loading multiple images.
if shim_loaded {
// Retain the shim protocol after loading the image.
ShimSupport::retain()?;
}
// Clone the current image handle to use for loading the image.
let current_image = *request.current_image();
// Converts the source to a shim input with an owned data buffer.
let input = ShimInput::from(request.into_source())
.into_owned_data_buffer()
.context("unable to convert input to loaded data buffer")?;
// Constructs a LoadImageSource from the input.
let source = LoadImageSource::FromBuffer {
buffer: input.buffer().context("unable to get buffer from input")?,
file_path: input.file_path(),
};
// If the security hook is required, install it right before loading the image.
// Installing it here ensures that an error while preparing the image can't
// return without uninstalling the hook.
if requires_security_hook {
// Install the security hook, if possible. If it's not, this is necessary to continue,
// so we should bail.
let installed = SecurityHook::install().context("unable to install security hook")?;
if !installed {
bail!("unable to install security hook required for this platform");
}
}
// Loads the image using Boot Services LoadImage function.
let result = uefi::boot::load_image(current_image, source).context("unable to load image");
// If the security override is required, we will uninstall the security hook.
if requires_security_hook {
let uninstall_result = crate::shim::hook::SecurityHook::uninstall();
// Ensure we don't mask load image errors if uninstalling fails.
if result.is_err()
&& let Err(uninstall_error) = &uninstall_result
{
// Warn on the error since the load image error is more important.
warn!("unable to uninstall security hook: {}", uninstall_error);
} else {
// Otherwise, ensure we handle the original uninstallation result.
uninstall_result?;
}
}
// Assert the result and grab the handle.
let handle = result?;
// Retrieve the handle from the result and make a new image handle.
Ok(ImageHandle::new(handle))
}
}
+25
View File
@@ -0,0 +1,25 @@
use crate::path::ResolvedPath;
use crate::shim::ShimInput;
/// Represents a source of an EFI image.
pub enum ImageSource<'source> {
/// The image is located at the specified path that has been resolved.
ResolvedPath(&'source ResolvedPath),
/// The image is located in a buffer.
DataBuffer {
/// Optional path to the image.
path: Option<&'source ResolvedPath>,
/// Buffer containing the image.
buffer: &'source [u8],
},
}
/// Implement conversion from `ImageSource` to `ShimInput`, which is used by the shim support code.
impl<'source> From<ImageSource<'source>> for ShimInput<'source> {
fn from(value: ImageSource<'source>) -> Self {
match value {
ImageSource::ResolvedPath(path) => ShimInput::ResolvedPath(path),
ImageSource::DataBuffer { path, buffer } => ShimInput::DataBuffer(path, buffer),
}
}
}
+94
View File
@@ -0,0 +1,94 @@
//! Based on: https://github.com/rust-osdev/uefi-rs/blob/main/uefi/src/helpers/logger.rs
use alloc::format;
use core::fmt::Write;
use core::ptr;
use core::sync::atomic::{AtomicPtr, Ordering};
use log::{Log, Record};
use uefi::proto::console::text::Output;
/// The global logger object.
static LOGGER: Logger = Logger::new();
/// Logging mechanism for Sprout.
/// Must be initialized to be used, as we use atomic pointers to store the output to write to.
pub struct Logger {
writer: AtomicPtr<Output>,
}
impl Default for Logger {
/// Creates a default logger, which is uninitialized with an output.
fn default() -> Self {
Self::new()
}
}
impl Logger {
/// Create a new logger with an output not specified.
/// This will cause the logger to not print anything until it is configured.
pub const fn new() -> Self {
Self {
writer: AtomicPtr::new(ptr::null_mut()),
}
}
/// Retrieves the pointer to the output.
/// SAFETY: This pointer might be null, it should be checked before use.
#[must_use]
fn output(&self) -> *mut Output {
self.writer.load(Ordering::Acquire)
}
/// Sets the output to write to.
///
/// # Safety
/// This function is unsafe because the output is technically leaked and unmanaged.
pub unsafe fn set_output(&self, output: *mut Output) {
self.writer.store(output, Ordering::Release);
}
}
impl Log for Logger {
/// Enable the logger always.
fn enabled(&self, _metadata: &log::Metadata<'_>) -> bool {
true
}
/// Log the specified `record` to the output if one is set.
fn log(&self, record: &Record) {
// Acquire the output. If one is not set, we do nothing.
let Some(output) = (unsafe { self.output().as_mut() }) else {
return;
};
// Format the log message.
let message = format!("{}", record.args());
// Iterate over every line, formatting the message and writing it to the output.
for line in message.lines() {
// The format writes the log level in front of every line of text.
let _ = writeln!(output, "[{:>5}] {}", record.level(), line);
}
}
/// This log is not buffered, so flushing isn't required.
fn flush(&self) {}
}
/// Initialize the logging environment, calling panic if something goes wrong.
pub fn init() {
// Retrieve the stdout handle and set it as the output for the global logger.
uefi::system::with_stdout(|stdout| unsafe {
// SAFETY: We are using the stdout handle to create a pointer to the output.
// The handle is global and is guaranteed to be valid for the lifetime of the program.
LOGGER.set_output(stdout);
});
// Set the logger to the global logger.
if let Err(error) = log::set_logger(&LOGGER) {
panic!("unable to set logger: {}", error);
}
// Set the max level to the level specified by the log features.
log::set_max_level(log::STATIC_MAX_LEVEL);
}
+306
View File
@@ -0,0 +1,306 @@
use alloc::boxed::Box;
use alloc::vec::Vec;
use anyhow::{Context, Result, bail};
use core::ffi::c_void;
use core::ptr;
use log::error;
use uefi::proto::device_path::DevicePath;
use uefi::proto::device_path::build::DevicePathBuilder;
use uefi::proto::device_path::build::media::Vendor;
use uefi::proto::media::load_file::LoadFile2;
use uefi::{Guid, Handle};
use uefi_raw::protocol::device_path::DevicePathProtocol;
use uefi_raw::protocol::media::LoadFile2Protocol;
use uefi_raw::{Boolean, Status};
pub mod constants;
/// The media loader protocol.
#[derive(Debug)]
#[repr(C)]
struct MediaLoaderProtocol {
/// This is the standard EFI LoadFile2 protocol.
pub load_file: unsafe extern "efiapi" fn(
this: *mut MediaLoaderProtocol,
file_path: *const DevicePathProtocol,
boot_policy: Boolean,
buffer_size: *mut usize,
buffer: *mut c_void,
) -> Status,
/// A pointer to a Box<[u8]> containing the data to load.
pub address: *mut c_void,
/// The length of the data to load.
pub length: usize,
}
/// Represents a media loader which has been registered in the UEFI stack.
/// Calling `drop` on this handle will unregister the media loader.
pub struct MediaLoaderHandle {
/// The handle of the media loader in the UEFI stack.
handle: Handle,
/// The protocol interface pointer.
protocol: *mut MediaLoaderProtocol,
/// The device path pointer.
path: *mut DevicePath,
}
impl MediaLoaderHandle {
/// The behavior of this function is derived from how Linux calls it.
///
/// Linux calls this function by first passing a NULL `buffer`.
/// We must set the size of the buffer it should allocate in `buffer_size`.
/// The next call will pass a buffer of the right size, and we should copy
/// data into that buffer, checking whether it is safe to copy based on
/// the buffer size.
///
/// SAFETY: `this.address` and `this.length` are set by leaking a Box<[u8]>, so we can
/// be sure their pointers are valid when this is called. The caller must call this function
/// while inside UEFI boot services to ensure pointers are valid. Copying to `buffer` is
/// assumed valid because the caller must ensure `buffer` is valid by function contract.
unsafe extern "efiapi" fn load_file(
this: *mut MediaLoaderProtocol,
file_path: *const DevicePathProtocol,
boot_policy: Boolean,
buffer_size: *mut usize,
buffer: *mut c_void,
) -> Status {
// Check if the pointers are non-null first.
if this.is_null() || buffer_size.is_null() || file_path.is_null() {
return Status::INVALID_PARAMETER;
}
// Boot policy must not be true, and if it is, that is special behavior that is irrelevant
// for the media loader concept. Any non-zero value is true.
if bool::from(boot_policy) {
return Status::UNSUPPORTED;
}
// SAFETY: Validated as safe because this is checked to be non-null. It is the caller's
// responsibility to ensure that the right pointer is passed for [this].
unsafe {
// Check if the length and address are valid.
if (*this).length == 0 || (*this).address.is_null() {
return Status::NOT_FOUND;
}
// Check if the buffer is large enough.
// If it is not, we need to set the buffer size to the length of the data.
// This is the way that Linux calls this function, to check the size to allocate
// for the buffer that holds the data.
if buffer.is_null() || *buffer_size < (*this).length {
*buffer_size = (*this).length;
return Status::BUFFER_TOO_SMALL;
}
// Copy the data into the buffer.
buffer.copy_from((*this).address, (*this).length);
// Set the buffer size to the length of the data.
*buffer_size = (*this).length;
}
// We've successfully loaded the data.
Status::SUCCESS
}
/// Creates a new device path for the media loader based on a vendor `guid`.
fn device_path(guid: Guid) -> Result<Box<DevicePath>> {
// The buffer for the device path.
let mut path = Vec::new();
// Build a device path for the media loader with a vendor-specific guid.
let path = DevicePathBuilder::with_vec(&mut path)
.push(&Vendor {
vendor_guid: guid,
vendor_defined_data: &[],
})
.context("unable to produce device path")?
.finalize()
.context("unable to produce device path")?;
// Convert the device path to a boxed device path.
// This is safer than dealing with a pooled device path.
Ok(path.to_boxed())
}
/// Checks if the media loader is already registered with the UEFI stack.
fn already_registered(guid: Guid) -> Result<bool> {
// Acquire the device path for the media loader.
let path = Self::device_path(guid)?;
let mut existing_path = path.as_ref();
// Locate the LoadFile2 protocol for the media loader based on the device path.
let result = uefi::boot::locate_device_path::<LoadFile2>(&mut existing_path);
// If the result is okay, the media loader is already registered.
if result.is_ok() {
return Ok(true);
} else if let Err(error) = result
&& error.status() != Status::NOT_FOUND
// If the error is not found, that means it's not registered.
{
bail!("unable to locate media loader device path: {}", error);
}
// The media loader is not registered.
Ok(false)
}
/// Registers the provided `data` with the UEFI stack as media loader.
/// This uses a special device path that other EFI programs will look at
/// to load the data from.
pub fn register(guid: Guid, data: Box<[u8]>) -> Result<MediaLoaderHandle> {
// Acquire the vendor device path for the media loader.
let path = Self::device_path(guid)?;
// Check if the media loader is already registered.
// If it is, we can't register it again safely.
if Self::already_registered(guid)? {
bail!("media loader already registered");
}
// Leak the device path to pass it to the UEFI stack.
let path = Box::leak(path);
// Install a protocol interface for the device path.
// This ensures it can be located by other EFI programs.
let primary_handle = match unsafe {
uefi::boot::install_protocol_interface(
None,
&DevicePathProtocol::GUID,
path.as_ffi_ptr() as *mut c_void,
)
}
.context("unable to install media loader device path handle")
{
// Acquiring the primary handle succeeded, so we can return the handle.
Ok(handle) => handle,
// If acquiring the primary handle failed, we free the device path and return the error.
Err(error) => {
// SAFETY: We know that the device path is leaked,
// so we can safely take a reference to it again.
// The UEFI stack failed to install the protocol interface
// if we reach here, so the path is no longer in use.
let path = unsafe { Box::from_raw(path) };
// Explicitly drop the path to clarify the lifetime.
drop(path);
// Return the original error.
return Err(error);
}
};
// Leak the data we need to pass to the UEFI stack.
let data = Box::leak(data);
// Allocate a new box for the protocol interface.
let protocol = Box::new(MediaLoaderProtocol {
load_file: Self::load_file,
address: data.as_ptr() as *mut _,
length: data.len(),
});
// Leak the protocol interface to pass it to the UEFI stack.
let protocol = Box::leak(protocol);
// Install a protocol interface for the load file protocol for the media loader protocol.
let secondary_handle = unsafe {
uefi::boot::install_protocol_interface(
Some(primary_handle),
&LoadFile2Protocol::GUID,
// The UEFI API expects an opaque pointer here.
protocol as *mut MediaLoaderProtocol as *mut c_void,
)
};
// If installing the second protocol interface failed, we need to clean up after ourselves.
if secondary_handle.is_err() {
// Uninstall the protocol interface for the device path protocol.
// SAFETY: If we have reached this point, we know that the protocol is registered.
// If this fails, we have no choice but to leak memory. The error will be shown
// to the user, so at least they can see it. In most cases, catching this error
// will exit, so leaking is safe.
unsafe {
uefi::boot::uninstall_protocol_interface(
primary_handle,
&DevicePathProtocol::GUID,
path.as_ffi_ptr() as *mut c_void,
)
.context(
"unable to uninstall media loader device path handle, this will leak memory",
)?;
}
// SAFETY: We know that the protocol is leaked, so we can safely take a reference to it.
let protocol = unsafe { Box::from_raw(protocol) };
// SAFETY: We know that the data is leaked, so we can safely take a reference to it.
let data = unsafe { Box::from_raw(data) };
// SAFETY: We know that the path is leaked, so we can safely take a reference to it.
let path = unsafe { Box::from_raw(path) };
// Drop all the allocations explicitly to clarify the lifetime.
drop(protocol);
drop(data);
drop(path);
}
// If installing the second protocol interface failed, this will return the error.
// We should have already cleaned up after ourselves, so this is safe.
secondary_handle.context("unable to install media loader load file handle")?;
// Return a handle to the media loader.
Ok(Self {
handle: primary_handle,
protocol,
path,
})
}
/// Unregisters a media loader from the UEFI stack.
/// This will free the memory allocated by the passed data.
fn unregister(&self) -> Result<()> {
// SAFETY: We know that the media loader is registered if the handle is valid,
// so we can safely uninstall it.
// We should have allocated the pointers involved, so we can safely free them.
unsafe {
// Uninstall the protocol interface for the device path protocol.
uefi::boot::uninstall_protocol_interface(
self.handle,
&DevicePathProtocol::GUID,
self.path as *mut c_void,
)
.context("unable to uninstall media loader device path handle")?;
// Uninstall the protocol interface for the load file protocol.
uefi::boot::uninstall_protocol_interface(
self.handle,
&LoadFile2Protocol::GUID,
self.protocol as *mut _ as *mut c_void,
)
.context("unable to uninstall media loader load file handle")?;
// Retrieve a box for the device path and protocols.
let path = Box::from_raw(self.path);
let protocol = Box::from_raw(self.protocol);
// Retrieve a box for the data we passed in.
let slice = ptr::slice_from_raw_parts_mut(protocol.address as *mut u8, protocol.length);
let data = Box::from_raw(slice);
// Drop all the allocations explicitly, as we don't want to leak them.
drop(path);
drop(protocol);
drop(data);
}
Ok(())
}
}
/// Implement drop for the handle to automatically unregister the media loader.
impl Drop for MediaLoaderHandle {
fn drop(&mut self) {
// If unregister fails, print an error to the log.
// This may leak stuff, but the only other option is to panic.
if let Err(error) = self.unregister() {
error!("unable to unregister media loader: {}", error);
}
}
}
@@ -13,7 +13,8 @@ pub mod xen {
/// The device path GUID for the Xen EFI config.
pub const XEN_EFI_CONFIG_MEDIA_GUID: Guid = guid!("bf61f458-a28e-46cd-93d7-07dac5e8cd66");
/// The device path GUID for the Xen EFI config.
/// The device path GUID for the Xen EFI kernel.
pub const XEN_EFI_KERNEL_MEDIA_GUID: Guid = guid!("4010c8bf-6ced-40f5-a53f-e820aee8f34b");
/// The device path GUID for the Xen EFI ramdisk.
pub const XEN_EFI_RAMDISK_MEDIA_GUID: Guid = guid!("5db1fd01-c3cb-4812-b2ba-8791e52d4a89");
}
+55
View File
@@ -0,0 +1,55 @@
use anyhow::{Context, Result};
use uefi::Guid;
use uefi::proto::device_path::DevicePath;
use uefi::proto::media::partition::PartitionInfo;
use uefi_raw::Status;
/// Represents the type of partition GUID that can be retrieved.
#[derive(PartialEq, Eq)]
pub enum PartitionGuidForm {
/// The partition GUID is the unique partition GUID.
Partition,
/// The partition GUID is the partition type GUID.
PartitionType,
}
/// Retrieve the partition / partition type GUID of the device root `path`.
/// This only works on GPT partitions. If the root is not a GPT partition, None is returned.
/// If the GUID is all zeros, this will return None.
pub fn partition_guid(path: &DevicePath, form: PartitionGuidForm) -> Result<Option<Guid>> {
// Clone the path so we can pass it to the UEFI stack.
let path = path.to_boxed();
let result = uefi::boot::locate_device_path::<PartitionInfo>(&mut &*path);
let handle = match result {
Ok(handle) => Ok(Some(handle)),
Err(error) => {
// If the error is NOT_FOUND or UNSUPPORTED, we can return None.
// These are non-fatal errors.
if error.status() == Status::NOT_FOUND || error.status() == Status::UNSUPPORTED {
Ok(None)
} else {
Err(error)
}
}
}
.context("unable to locate device path")?;
// If we have the handle, we can try to open the partition info protocol.
if let Some(handle) = handle {
// Open the partition info protocol.
let partition_info = uefi::boot::open_protocol_exclusive::<PartitionInfo>(handle)
.context("unable to open partition info protocol")?;
// Find the unique partition GUID.
// If this is not a GPT partition, this will produce None.
Ok(partition_info
.gpt_partition_entry()
.map(|entry| match form {
// Match the form of the partition GUID.
PartitionGuidForm::Partition => entry.unique_partition_guid,
PartitionGuidForm::PartitionType => entry.partition_type_guid.0,
})
.filter(|guid| !guid.is_zero()))
} else {
Ok(None)
}
}
+102 -94
View File
@@ -1,78 +1,17 @@
use alloc::borrow::ToOwned;
use alloc::boxed::Box;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use anyhow::{Context, Result};
use std::ops::Deref;
use core::ops::Deref;
use uefi::fs::{FileSystem, Path};
use uefi::proto::device_path::text::{AllowShortcuts, DevicePathFromText, DisplayOnly};
use uefi::proto::device_path::{DevicePath, PoolDevicePath};
use uefi::proto::device_path::{
DevicePath, DevicePathNode, DeviceSubType, DeviceType, PoolDevicePath,
};
use uefi::proto::media::fs::SimpleFileSystem;
use uefi::{CString16, Handle};
pub mod framebuffer;
pub mod media_loader;
/// Parses the input [path] as a [DevicePath].
/// Uses the [DevicePathFromText] protocol exclusively, and will fail if it cannot acquire the protocol.
pub fn text_to_device_path(path: &str) -> Result<PoolDevicePath> {
let path = CString16::try_from(path).context("unable to convert path to CString16")?;
let device_path_from_text = uefi::boot::open_protocol_exclusive::<DevicePathFromText>(
uefi::boot::get_handle_for_protocol::<DevicePathFromText>()
.context("no device path from text protocol")?,
)
.context("unable to open device path from text protocol")?;
device_path_from_text
.convert_text_to_device_path(&path)
.context("unable to convert text to device path")
}
/// Grabs the root part of the [path].
/// For example, given "PciRoot(0x0)/Pci(0x4,0x0)/NVMe(0x1,00-00-00-00-00-00-00-00)/HD(1,MBR,0xBE1AFDFA,0x3F,0xFBFC1)/\EFI\BOOT\BOOTX64.efi"
/// it will give "PciRoot(0x0)/Pci(0x4,0x0)/NVMe(0x1,00-00-00-00-00-00-00-00)/HD(1,MBR,0xBE1AFDFA,0x3F,0xFBFC1)"
pub fn device_path_root(path: &DevicePath) -> Result<String> {
let mut path = path
.node_iter()
.filter_map(|item| {
let item = item.to_string(DisplayOnly(false), AllowShortcuts(false));
if item
.as_ref()
.map(|item| item.to_string().contains("("))
.unwrap_or(false)
{
Some(item.unwrap_or_default())
} else {
None
}
})
.map(|item| item.to_string())
.collect::<Vec<_>>()
.join("/");
path.push('/');
Ok(path)
}
/// Grabs the part of the [path] after the root.
/// For example, given "PciRoot(0x0)/Pci(0x4,0x0)/NVMe(0x1,00-00-00-00-00-00-00-00)/HD(1,MBR,0xBE1AFDFA,0x3F,0xFBFC1)/\EFI\BOOT\BOOTX64.efi"
/// it will give "\EFI\BOOT\BOOTX64.efi"
pub fn device_path_subpath(path: &DevicePath) -> Result<String> {
let path = path
.node_iter()
.filter_map(|item| {
let item = item.to_string(DisplayOnly(false), AllowShortcuts(false));
if item
.as_ref()
.map(|item| item.to_string().contains("("))
.unwrap_or(false)
{
None
} else {
Some(item.unwrap_or_default())
}
})
.map(|item| item.to_string())
.collect::<Vec<_>>()
.join("\\");
Ok(path)
}
/// Represents the components of a resolved path.
pub struct ResolvedPath {
/// The root path of the resolved path. This is the device itself.
@@ -89,25 +28,98 @@ pub struct ResolvedPath {
pub filesystem_handle: Handle,
}
/// Resolve a path specified by [input] to its various components.
/// Uses [default_root_path] as the base root if one is not specified in the path.
impl ResolvedPath {
/// Read the file specified by this path into a buffer and return it.
pub fn read_file(&self) -> Result<Vec<u8>> {
let fs = uefi::boot::open_protocol_exclusive::<SimpleFileSystem>(self.filesystem_handle)
.context("unable to open filesystem protocol")?;
let mut fs = FileSystem::new(fs);
let path = self
.sub_path
.to_string16(DisplayOnly(false), AllowShortcuts(false))?;
let content = fs.read(Path::new(&path));
content.context("unable to read file contents")
}
}
/// Checks if the device path `node` is a file path node, rather than a device node.
fn is_file_path_node(node: &DevicePathNode) -> bool {
node.device_type() == DeviceType::MEDIA && node.sub_type() == DeviceSubType::MEDIA_FILE_PATH
}
/// Converts the device path `node` to its text form.
fn node_to_string(node: &DevicePathNode) -> Result<String> {
Ok(node
.to_string16(DisplayOnly(false), AllowShortcuts(false))
.context("unable to convert device path node to string")?
.to_string())
}
/// Parses the input `path` as a [DevicePath].
/// Uses the [DevicePathFromText] protocol exclusively, and will fail if it cannot acquire the protocol.
pub fn text_to_device_path(path: impl AsRef<str>) -> Result<PoolDevicePath> {
let path = CString16::try_from(path.as_ref()).context("unable to convert path to CString16")?;
let device_path_from_text = uefi::boot::open_protocol_exclusive::<DevicePathFromText>(
uefi::boot::get_handle_for_protocol::<DevicePathFromText>()
.context("no device path from text protocol")?,
)
.context("unable to open device path from text protocol")?;
device_path_from_text
.convert_text_to_device_path(&path)
.context("unable to convert text to device path")
}
/// Grabs the root part of the `path`.
/// For example, given "PciRoot(0x0)/Pci(0x4,0x0)/NVMe(0x1,00-00-00-00-00-00-00-00)/HD(1,MBR,0xBE1AFDFA,0x3F,0xFBFC1)/\EFI\BOOT\BOOTX64.efi"
/// it will give "PciRoot(0x0)/Pci(0x4,0x0)/NVMe(0x1,00-00-00-00-00-00-00-00)/HD(1,MBR,0xBE1AFDFA,0x3F,0xFBFC1)"
pub fn device_path_root(path: &DevicePath) -> Result<String> {
let mut path = path
.node_iter()
.filter(|node| !is_file_path_node(node))
.map(node_to_string)
.collect::<Result<Vec<_>>>()?
.join("/");
path.push('/');
Ok(path)
}
/// Grabs the part of the `path` after the root.
/// For example, given "PciRoot(0x0)/Pci(0x4,0x0)/NVMe(0x1,00-00-00-00-00-00-00-00)/HD(1,MBR,0xBE1AFDFA,0x3F,0xFBFC1)/\EFI\BOOT\BOOTX64.efi"
/// it will give "\EFI\BOOT\BOOTX64.efi"
pub fn device_path_subpath(path: &DevicePath) -> Result<String> {
let path = path
.node_iter()
.filter(|node| is_file_path_node(node))
.map(node_to_string)
.collect::<Result<Vec<_>>>()?
.join("\\");
Ok(path)
}
/// Resolve a path specified by `input` to its various components.
/// Uses `default_root_path` as the base root if one is not specified in the path.
/// Returns [ResolvedPath] which contains the resolved components.
pub fn resolve_path(default_root_path: &DevicePath, input: &str) -> Result<ResolvedPath> {
let mut path = text_to_device_path(input).context("unable to convert text to path")?;
pub fn resolve_path(
default_root_path: Option<&DevicePath>,
input: impl ToString,
) -> Result<ResolvedPath> {
let mut input = input.to_string();
let mut path = text_to_device_path(&input).context("unable to convert text to path")?;
// A path that starts with a device node has a device, while a path that starts
// with a file path node is relative to the default root path.
let path_has_device = path
.node_iter()
.next()
.map(|it| {
it.to_string(DisplayOnly(false), AllowShortcuts(false))
.unwrap_or_default()
})
.map(|it| it.to_string().contains("("))
.unwrap_or(false);
.is_some_and(|node| !is_file_path_node(node));
if !path_has_device {
let mut input = input.to_string();
if !input.starts_with("\\") {
if !input.starts_with('\\') {
input.insert(0, '\\');
}
let default_root_path = default_root_path.context("unable to get default root path")?;
input.insert_str(
0,
device_path_root(default_root_path)
@@ -122,8 +134,11 @@ pub fn resolve_path(default_root_path: &DevicePath, input: &str) -> Result<Resol
let root_path = text_to_device_path(root.as_str())
.context("unable to convert root to path")?
.to_boxed();
let mut root_path = root_path.as_ref();
let handle = uefi::boot::locate_device_path::<SimpleFileSystem>(&mut root_path)
let root_path = root_path.as_ref();
// locate_device_path modifies the path, so we need to clone it.
let root_path_modifiable = root_path.to_owned();
let handle = uefi::boot::locate_device_path::<SimpleFileSystem>(&mut &*root_path_modifiable)
.context("unable to locate filesystem device path")?;
let subpath = device_path_subpath(path.deref()).context("unable to get device subpath")?;
Ok(ResolvedPath {
@@ -134,21 +149,14 @@ pub fn resolve_path(default_root_path: &DevicePath, input: &str) -> Result<Resol
})
}
/// Read the contents of a file at the location specified with the [input] path.
/// Read the contents of a file at the location specified with the `input` path.
/// Internally, this uses [resolve_path] to resolve the path to its various components.
/// [resolve_path] is passed the [default_root_path] which should specify a base root.
/// [resolve_path] is passed the `default_root_path` which should specify a base root.
///
/// This acquires exclusive protocol access to the [SimpleFileSystem] protocol of the resolved
/// filesystem handle, so care must be taken to call this function outside a scope with
/// the filesystem handle protocol acquired.
pub fn read_file_contents(default_root_path: &DevicePath, input: &str) -> Result<Vec<u8>> {
pub fn read_file_contents(default_root_path: Option<&DevicePath>, input: &str) -> Result<Vec<u8>> {
let resolved = resolve_path(default_root_path, input)?;
let fs = uefi::boot::open_protocol_exclusive::<SimpleFileSystem>(resolved.filesystem_handle)
.context("unable to open filesystem protocol")?;
let mut fs = FileSystem::new(fs);
let path = resolved
.sub_path
.to_string(DisplayOnly(false), AllowShortcuts(false))?;
let content = fs.read(Path::new(&path));
content.context("unable to read file contents")
resolved.read_file()
}
+4
View File
@@ -0,0 +1,4 @@
/// Timer support.
pub mod timer;
/// TPM support.
pub mod tpm;
+100
View File
@@ -0,0 +1,100 @@
// Referenced https://github.com/sheroz/tick_counter (MIT license) as a baseline.
// Architecturally modified to support UEFI and remove x86 (32-bit) support.
use core::time::Duration;
use log::warn;
/// Support for aarch64 timers.
#[cfg(target_arch = "aarch64")]
pub mod aarch64;
/// Support for x86_64 timers.
#[cfg(target_arch = "x86_64")]
pub mod x86_64;
/// The tick frequency of the platform.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum TickFrequency {
/// The platform provides the tick frequency.
Hardware(u64),
/// The tick frequency is measured internally.
Measured(u64),
}
impl TickFrequency {
/// Acquire the tick frequency reported by the platform.
fn ticks(&self) -> u64 {
match self {
TickFrequency::Hardware(frequency) => *frequency,
TickFrequency::Measured(frequency) => *frequency,
}
}
/// Calculate the nanoseconds represented by a tick.
fn nanos(&self) -> f64 {
1.0e9_f64 / (self.ticks() as f64)
}
/// Produce a duration from the provided elapsed `ticks` value.
/// If the frequency is unknown, the duration is zero.
fn duration(&self, ticks: u64) -> Duration {
if self.ticks() == 0 {
return Duration::ZERO;
}
let accuracy = self.nanos();
let nanos = ticks as f64 * accuracy;
Duration::from_nanos(nanos as u64)
}
}
/// Acquire the tick value reported by the platform.
fn arch_ticks() -> u64 {
#[cfg(target_arch = "aarch64")]
return aarch64::ticks();
#[cfg(target_arch = "x86_64")]
return x86_64::ticks();
}
/// Acquire the tick frequency reported by the platform.
fn arch_frequency() -> TickFrequency {
#[cfg(target_arch = "aarch64")]
let frequency = aarch64::frequency();
#[cfg(target_arch = "x86_64")]
let frequency = x86_64::frequency();
// Some firmware does not program the timer frequency. The timer is only used to report
// timestamps to the bootloader interface, so this should not prevent booting.
if frequency.ticks() == 0 {
warn!("timer frequency is zero, timestamps will not be reported");
}
frequency
}
/// Platform timer that allows measurement of the elapsed time.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct PlatformTimer {
/// The start tick value.
start: u64,
/// The tick frequency of the platform.
frequency: TickFrequency,
}
impl PlatformTimer {
/// Start a platform timer at the current instant.
pub fn start() -> Self {
Self {
start: arch_ticks(),
frequency: arch_frequency(),
}
}
/// Measure the elapsed duration since the hardware started ticking upwards.
pub fn elapsed_since_lifetime(&self) -> Duration {
self.frequency.duration(arch_ticks())
}
/// Measure the elapsed duration since the timer was started.
pub fn elapsed_since_start(&self) -> Duration {
let duration = arch_ticks().wrapping_sub(self.start);
self.frequency.duration(duration)
}
}
@@ -0,0 +1,23 @@
use crate::platform::timer::TickFrequency;
use core::arch::asm;
/// Reads the cntvct_el0 counter and returns the value.
pub fn ticks() -> u64 {
let counter: u64;
unsafe {
asm!("mrs x0, cntvct_el0", out("x0") counter);
}
counter
}
/// Our frequency is provided by cntfrq_el0 on the platform.
pub fn frequency() -> TickFrequency {
let frequency: u64;
unsafe {
asm!(
"mrs x0, cntfrq_el0",
out("x0") frequency
);
}
TickFrequency::Hardware(frequency)
}
@@ -0,0 +1,29 @@
use crate::platform::timer::TickFrequency;
use core::time::Duration;
/// We will measure the frequency of the timer based on 1000 microseconds.
/// This will result in a call to BS->Stall(1000) in the end.
const MEASURE_FREQUENCY_DURATION: Duration = Duration::from_micros(1000);
/// Read the number of ticks from the platform timer.
pub fn ticks() -> u64 {
// SAFETY: Reads the platform timer, which is safe in any context.
unsafe { core::arch::x86_64::_rdtsc() }
}
/// Measure the frequency of the platform timer.
/// NOTE: Intentionally, we do not synchronize rdtsc during measurement to match systemd behavior.
fn measure_frequency() -> u64 {
let start = ticks();
uefi::boot::stall(MEASURE_FREQUENCY_DURATION);
let stop = ticks();
let elapsed = stop.wrapping_sub(start) as f64;
(elapsed / MEASURE_FREQUENCY_DURATION.as_secs_f64()) as u64
}
/// Acquire the platform timer frequency.
/// On x86_64, this is slightly expensive, so it should be done once.
pub fn frequency() -> TickFrequency {
let frequency = measure_frequency();
TickFrequency::Measured(frequency)
}
+155
View File
@@ -0,0 +1,155 @@
use anyhow::{Context, Result};
use log::warn;
use uefi::ResultExt;
use uefi::boot::ScopedProtocol;
use uefi::proto::tcg::PcrIndex;
use uefi::proto::tcg::v2::{PcrEventInputs, Tcg};
use uefi_raw::Status;
use uefi_raw::protocol::tcg::EventType;
use uefi_raw::protocol::tcg::v2::{Tcg2HashLogExtendEventFlags, Tcg2Protocol, Tcg2Version};
/// Represents the platform TPM.
pub struct PlatformTpm;
/// Represents an open TPM handle.
pub struct TpmProtocolHandle {
/// The version of the TPM protocol.
version: Tcg2Version,
/// The protocol itself.
protocol: ScopedProtocol<Tcg>,
}
impl TpmProtocolHandle {
/// Construct a new [TpmProtocolHandle] from the `version` and `protocol`.
pub fn new(version: Tcg2Version, protocol: ScopedProtocol<Tcg>) -> Self {
Self { version, protocol }
}
/// Access the version provided by the tcg2 protocol.
pub fn version(&self) -> Tcg2Version {
self.version
}
/// Access the protocol interface for tcg2.
pub fn protocol(&mut self) -> &mut ScopedProtocol<Tcg> {
&mut self.protocol
}
}
impl PlatformTpm {
/// The PCR for measuring the bootloader configuration into.
pub const PCR_BOOT_LOADER_CONFIG: PcrIndex = PcrIndex(5);
/// The PCR for measuring the dom0 initrd payload into.
pub const PCR_INITRD: PcrIndex = PcrIndex(9);
/// The PCR for measuring the dom0 kernel payload into.
pub const PCR_KERNEL: PcrIndex = PcrIndex(11);
/// The PCR for measuring kernel command line and xen options into.
pub const PCR_KERNEL_PARAMETERS: PcrIndex = PcrIndex(12);
/// Acquire access to the TPM protocol handle, if possible.
/// Returns None if TPM is not available.
fn protocol() -> Result<Option<TpmProtocolHandle>> {
// Attempt to acquire the TCG2 protocol handle. If it's not available, return None.
let Some(handle) = crate::handle::find_handle(&Tcg2Protocol::GUID)
.context("unable to determine tpm presence")?
else {
return Ok(None);
};
// If we reach here, we've already validated that the handle
// implements the TCG2 protocol.
let mut protocol = uefi::boot::open_protocol_exclusive::<Tcg>(handle)
.context("unable to open tcg2 protocol")?;
// Acquire the capabilities of the TPM.
let capability = protocol
.get_capability()
.context("unable to get tcg2 boot service capability")?;
// If the TPM is not present, return None.
if !capability.tpm_present() {
return Ok(None);
}
// If the TPM is present, we need to determine the version of the TPM.
let version = capability.protocol_version;
// We have a TPM, so return the protocol version and the protocol handle.
Ok(Some(TpmProtocolHandle::new(version, protocol)))
}
/// Determines whether the platform TPM is present.
pub fn present() -> Result<bool> {
Ok(PlatformTpm::protocol()?.is_some())
}
/// Determine the number of active PCR banks on the TPM.
/// If no TPM is available, this will return zero.
pub fn active_pcr_banks() -> Result<u32> {
// Acquire access to the TPM protocol handle.
let Some(mut handle) = PlatformTpm::protocol()? else {
return Ok(0);
};
// Check if the TPM supports `GetActivePcrBanks`, and if it doesn't return zero.
if (handle.version().major < 1)
|| (handle.version().major == 1 && (handle.version().minor < 1))
{
return Ok(0);
}
// The safe wrapper for this function will decode the bitmap.
// Strictly speaking, it's not future-proof to re-encode that, but in practice it will work.
let banks = handle
.protocol()
.get_active_pcr_banks()
.context("unable to get active pcr banks")?;
// Return the number of active PCR banks.
Ok(banks.bits())
}
/// Log an event into the TPM pcr `pcr_index` with `buffer` as data. The `description`
/// is used to describe what the event is.
///
/// If a TPM is not available, this will do nothing.
pub fn log_event(pcr_index: PcrIndex, buffer: &[u8], description: &str) -> Result<()> {
// Acquire access to the TPM protocol handle.
let Some(mut handle) = PlatformTpm::protocol()? else {
return Ok(());
};
// Encode the description as UTF-8.
let encoded_description = description.as_bytes().to_vec();
// Construct an event input for the TPM.
let event = PcrEventInputs::new_in_box(pcr_index, EventType::IPL, &encoded_description)
.discard_errdata()
.context("unable to construct pcr event inputs")?;
// Log the event into the TPM.
let result = handle.protocol().hash_log_extend_event(
Tcg2HashLogExtendEventFlags::empty(),
buffer,
&event,
);
// A full event log means the PCR was extended, but the event could not be logged.
// The measurement still happened, so this should not prevent booting.
if let Err(ref error) = result
&& error.status() == Status::VOLUME_FULL
{
warn!(
"tpm event log is full, unable to log event: {}",
description
);
return Ok(());
}
result.context("unable to log event to tpm")?;
Ok(())
}
}
+14
View File
@@ -0,0 +1,14 @@
use crate::variables::VariableController;
use anyhow::Result;
/// Secure boot services.
pub struct SecureBoot;
impl SecureBoot {
/// Checks if Secure Boot is enabled on the system.
/// This might fail if retrieving the variable fails in an irrecoverable way.
pub fn enabled() -> Result<bool> {
// The SecureBoot variable will tell us whether Secure Boot is enabled at all.
VariableController::GLOBAL.get_bool("SecureBoot")
}
}
+14
View File
@@ -0,0 +1,14 @@
use crate::logger;
use anyhow::{Context, Result};
/// Initializes the UEFI environment.
pub fn init() -> Result<()> {
// Initialize the logger for Sprout.
// NOTE: This cannot use a result type as errors need to be printed
// using the logger, which is not initialized until this returns.
logger::init();
// Initialize further UEFI internals.
uefi::helpers::init().context("unable to initialize uefi environment")?;
Ok(())
}
+247
View File
@@ -0,0 +1,247 @@
use crate::path::ResolvedPath;
use crate::variables::{VariableClass, VariableController};
use alloc::boxed::Box;
use alloc::string::ToString;
use alloc::vec::Vec;
use anyhow::{Context, Result, anyhow, bail};
use core::ffi::c_void;
use core::pin::Pin;
use uefi::proto::device_path::text::{AllowShortcuts, DisplayOnly};
use uefi::proto::device_path::{DevicePath, FfiDevicePath};
use uefi::proto::unsafe_protocol;
use uefi_raw::table::runtime::VariableVendor;
use uefi_raw::{Guid, Status, guid};
/// Security hook support.
pub mod hook;
/// Support for the shim loader application for Secure Boot.
pub struct ShimSupport;
/// Input to the shim mechanisms.
pub enum ShimInput<'a> {
/// Data loaded into a buffer and ready to be verified, owned.
OwnedDataBuffer(Option<&'a ResolvedPath>, Pin<Box<[u8]>>),
/// Data loaded into a buffer and ready to be verified.
DataBuffer(Option<&'a ResolvedPath>, &'a [u8]),
/// Low-level data buffer provided by the security hook.
SecurityHookBuffer(Option<*const FfiDevicePath>, &'a [u8]),
/// Low-level owned data buffer provided by the security hook.
SecurityHookOwnedBuffer(Option<*const FfiDevicePath>, Pin<Box<[u8]>>),
/// Low-level path provided by the security hook.
SecurityHookPath(*const FfiDevicePath),
/// Data is provided as a resolved path. We will need to load the data to verify it.
/// The output will them return the loaded data.
ResolvedPath(&'a ResolvedPath),
}
impl<'a> ShimInput<'a> {
/// Accesses the buffer behind the shim input, if available.
pub fn buffer(&self) -> Option<&[u8]> {
match self {
ShimInput::OwnedDataBuffer(_, data) => Some(data),
ShimInput::SecurityHookOwnedBuffer(_, data) => Some(data),
ShimInput::SecurityHookBuffer(_, data) => Some(data),
ShimInput::SecurityHookPath(_) => None,
ShimInput::DataBuffer(_, data) => Some(data),
ShimInput::ResolvedPath(_) => None,
}
}
/// Accesses the full device path to the input.
pub fn file_path(&self) -> Option<&DevicePath> {
match self {
ShimInput::OwnedDataBuffer(path, _) => path.as_ref().map(|it| it.full_path.as_ref()),
ShimInput::DataBuffer(path, _) => path.as_ref().map(|it| it.full_path.as_ref()),
ShimInput::SecurityHookBuffer(path, _) => {
path.map(|it| unsafe { DevicePath::from_ffi_ptr(it) })
}
ShimInput::SecurityHookPath(path) => unsafe { Some(DevicePath::from_ffi_ptr(*path)) },
ShimInput::ResolvedPath(path) => Some(path.full_path.as_ref()),
ShimInput::SecurityHookOwnedBuffer(path, _) => {
path.map(|it| unsafe { DevicePath::from_ffi_ptr(it) })
}
}
}
/// Converts this input into an owned data buffer, where the data is loaded.
/// For ResolvedPath, this will read the file.
pub fn into_owned_data_buffer(self) -> Result<ShimInput<'a>> {
match self {
ShimInput::OwnedDataBuffer(root, data) => Ok(ShimInput::OwnedDataBuffer(root, data)),
ShimInput::DataBuffer(root, data) => Ok(ShimInput::OwnedDataBuffer(
root,
Box::into_pin(data.to_vec().into_boxed_slice()),
)),
ShimInput::SecurityHookPath(ffi_path) => {
// Acquire the file path.
let Some(path) = self.file_path() else {
bail!("unable to convert security hook path to device path");
};
// Convert the underlying path to a string.
let path = path
.to_string16(DisplayOnly(false), AllowShortcuts(false))
.context("unable to convert device path to string")?;
let path = crate::path::resolve_path(None, path.to_string())
.context("unable to resolve path")?;
// Read the file path.
let data = path.read_file()?;
Ok(ShimInput::SecurityHookOwnedBuffer(
Some(ffi_path),
Box::into_pin(data.to_vec().into_boxed_slice()),
))
}
ShimInput::SecurityHookBuffer(_, _) => {
bail!("unable to convert security hook buffer to owned data buffer")
}
ShimInput::ResolvedPath(path) => {
// Read the file path.
let data = path.read_file()?;
Ok(ShimInput::OwnedDataBuffer(
Some(path),
Box::into_pin(data.to_vec().into_boxed_slice()),
))
}
ShimInput::SecurityHookOwnedBuffer(path, data) => {
Ok(ShimInput::SecurityHookOwnedBuffer(path, data))
}
}
}
}
/// Output of the shim verification function.
/// Since the shim needs to load the data from disk, we will optimize by using that as the data
/// to actually boot.
pub enum ShimVerificationOutput {
/// The verification failed.
VerificationFailed(Status),
/// The data provided to the verifier was already a buffer.
VerifiedDataNotLoaded,
/// Verifying the data resulted in loading the data from the source.
/// This contains the data that was loaded, so it won't need to be loaded again.
VerifiedDataBuffer(Vec<u8>),
}
/// The shim lock protocol as defined by the shim loader application.
#[unsafe_protocol(ShimSupport::SHIM_LOCK_GUID)]
struct ShimLockProtocol {
/// Verify the data in `buffer` with the size `buffer_size` to determine if it is valid.
/// NOTE: On x86_64, this function uses SYSV calling conventions. On aarch64 it uses the
/// efiapi calling convention. This is truly wild, but you can verify it yourself by
/// looking at: https://github.com/rhboot/shim/blob/15.8/shim.h#L207-L212
/// There is no calling convention declared like there should be.
#[cfg(target_arch = "x86_64")]
pub shim_verify: unsafe extern "sysv64" fn(buffer: *const c_void, buffer_size: u32) -> Status,
#[cfg(target_arch = "aarch64")]
pub shim_verify: unsafe extern "efiapi" fn(buffer: *const c_void, buffer_size: u32) -> Status,
/// Unused function that is defined by the shim.
_generate_header: *mut c_void,
/// Unused function that is defined by the shim.
_read_header: *mut c_void,
}
impl ShimSupport {
/// Variable controller for the shim lock.
const SHIM_LOCK_VARIABLES: VariableController =
VariableController::new(VariableVendor(Self::SHIM_LOCK_GUID));
/// GUID for the shim lock protocol.
const SHIM_LOCK_GUID: Guid = guid!("605dab50-e046-4300-abb6-3dd810dd8b23");
/// GUID for the shim image loader protocol.
const SHIM_IMAGE_LOADER_GUID: Guid = guid!("1f492041-fadb-4e59-9e57-7cafe73a55ab");
/// Determines whether the shim is loaded.
pub fn loaded() -> Result<bool> {
Ok(crate::handle::find_handle(&Self::SHIM_LOCK_GUID)
.context("unable to find shim lock protocol")?
.is_some())
}
/// Determines whether the shim loader is available.
pub fn loader_available() -> Result<bool> {
Ok(crate::handle::find_handle(&Self::SHIM_IMAGE_LOADER_GUID)
.context("unable to find shim image loader protocol")?
.is_some())
}
/// Use the shim to validate the `input`, returning [ShimVerificationOutput] when complete.
pub fn verify(input: ShimInput) -> Result<ShimVerificationOutput> {
// Acquire the handle to the shim lock protocol.
let handle = crate::handle::find_handle(&Self::SHIM_LOCK_GUID)
.context("unable to find shim lock protocol")?
.ok_or_else(|| anyhow!("unable to find shim lock protocol"))?;
// Acquire the protocol exclusively to the shim lock.
let protocol = uefi::boot::open_protocol_exclusive::<ShimLockProtocol>(handle)
.context("unable to open shim lock protocol")?;
// If the input type is a device path, we need to load the data.
let maybe_loaded_data = match input {
ShimInput::OwnedDataBuffer(_, _data) => {
bail!("owned data buffer is not supported in the verification function");
}
ShimInput::SecurityHookBuffer(_, _) => None,
ShimInput::SecurityHookOwnedBuffer(_, _) => None,
ShimInput::DataBuffer(_, _) => None,
ShimInput::ResolvedPath(path) => Some(path.read_file()?),
ShimInput::SecurityHookPath(_) => None,
};
// Convert the input to a buffer.
// If the input provides the data buffer, we will use that.
// Otherwise, we will use the data loaded by this function.
let buffer = match &input {
ShimInput::OwnedDataBuffer(_root, data) => data,
ShimInput::DataBuffer(_root, data) => *data,
ShimInput::ResolvedPath(_path) => maybe_loaded_data
.as_deref()
.context("expected data buffer to be loaded already")?,
ShimInput::SecurityHookBuffer(_, data) => data,
ShimInput::SecurityHookOwnedBuffer(_, data) => data,
ShimInput::SecurityHookPath(_) => {
bail!("security hook path input not supported in the verification function")
}
};
// Check if the buffer is too large to verify.
if buffer.len() > u32::MAX as usize {
bail!("buffer is too large to verify with shim lock protocol");
}
// Call the shim verify function.
// SAFETY: The shim verify function is specified by the shim lock protocol.
// Calling this function is considered safe because the shim verify function is
// guaranteed to be defined by the environment if we are able to acquire the protocol.
let status = unsafe {
(protocol.shim_verify)(buffer.as_ptr() as *const c_void, buffer.len() as u32)
};
// If the verification failed, return the verification failure output.
if !status.is_success() {
return Ok(ShimVerificationOutput::VerificationFailed(status));
}
// If verification succeeded, return the validation output,
// which might include the loaded data.
Ok(maybe_loaded_data
.map(ShimVerificationOutput::VerifiedDataBuffer)
.unwrap_or(ShimVerificationOutput::VerifiedDataNotLoaded))
}
/// Set the ShimRetainProtocol variable to indicate that shim should retain the protocols
/// for the full lifetime of boot services.
pub fn retain() -> Result<()> {
Self::SHIM_LOCK_VARIABLES
.set_bool(
"ShimRetainProtocol",
true,
VariableClass::BootAndRuntimeTemporary,
)
.context("unable to retain shim protocol")?;
Ok(())
}
}
+305
View File
@@ -0,0 +1,305 @@
use crate::shim::{ShimInput, ShimSupport, ShimVerificationOutput};
use anyhow::{Context, Result};
use core::slice;
use log::warn;
use spin::{LazyLock, Mutex};
use uefi::proto::device_path::FfiDevicePath;
use uefi::proto::unsafe_protocol;
use uefi::{Guid, guid};
use uefi_raw::{Boolean, Status};
/// GUID for the EFI_SECURITY_ARCH protocol.
const SECURITY_ARCH_GUID: Guid = guid!("a46423e3-4617-49f1-b9ff-d1bfa9115839");
/// GUID for the EFI_SECURITY_ARCH2 protocol.
const SECURITY_ARCH2_GUID: Guid = guid!("94ab2f58-1438-4ef1-9152-18941a3a0e68");
/// EFI_SECURITY_ARCH protocol definition.
#[unsafe_protocol(SECURITY_ARCH_GUID)]
pub struct SecurityArchProtocol {
/// Determines the file authentication state.
pub file_authentication_state: unsafe extern "efiapi" fn(
this: *const SecurityArchProtocol,
status: u32,
path: *const FfiDevicePath,
) -> Status,
}
/// EFI_SECURITY_ARCH2 protocol definition.
#[unsafe_protocol(SECURITY_ARCH2_GUID)]
pub struct SecurityArch2Protocol {
/// Determines the file authentication.
pub file_authentication: unsafe extern "efiapi" fn(
this: *const SecurityArch2Protocol,
path: *const FfiDevicePath,
file_buffer: *const u8,
file_size: usize,
boot_policy: Boolean,
) -> Status,
}
/// Global state for the security hook.
struct SecurityHookState {
original_hook: SecurityArchProtocol,
original_hook2: SecurityArch2Protocol,
}
/// Global state for the security hook.
/// This is messy, but it is safe given the mutex.
static GLOBAL_HOOK_STATE: LazyLock<Mutex<Option<SecurityHookState>>> =
LazyLock::new(|| Mutex::new(None));
/// Security hook helper.
pub struct SecurityHook;
impl SecurityHook {
/// Shared verifier logic for both hook types.
#[must_use]
fn verify(input: ShimInput) -> bool {
// Verify the input and convert the result to a status.
let status = match ShimSupport::verify(input) {
Ok(output) => match output {
// If the verification failed, return the access-denied status.
ShimVerificationOutput::VerificationFailed(status) => status,
// If the verification succeeded, return the success status.
ShimVerificationOutput::VerifiedDataNotLoaded => Status::SUCCESS,
ShimVerificationOutput::VerifiedDataBuffer(_) => Status::SUCCESS,
},
// If an error occurs, log the error since we can't return a better error.
// Then return the access-denied status.
Err(error) => {
warn!("unable to verify image: {}", error);
Status::ACCESS_DENIED
}
};
// If the status is not a success, log the status.
if !status.is_success() {
warn!("shim verification failed: {}", status);
}
// Return whether the status is a success.
// If it's not a success, the original hook should be called.
status.is_success()
}
/// Call the original EFI_SECURITY_ARCH hook with `this`, `status`, and `path`.
/// This lets the firmware decide on images that the shim can't verify.
unsafe fn original_file_authentication_state(
this: *const SecurityArchProtocol,
status: u32,
path: *const FfiDevicePath,
) -> Status {
// Acquire the global hook state to grab the original hook.
let function = match GLOBAL_HOOK_STATE.lock().as_ref() {
// The hook state is available, so we can acquire the original hook.
Some(state) => state.original_hook.file_authentication_state,
// The hook state is not available, so we can't call the original hook.
None => {
warn!("global hook state is not available, unable to call original hook");
return Status::LOAD_ERROR;
}
};
// Call the original hook function to see what it reports.
// SAFETY: This function is safe to call as it is stored by us and is required
// in the UEFI specification.
unsafe { function(this, status, path) }
}
/// Call the original EFI_SECURITY_ARCH2 hook with `this`, `path`, `file_buffer`,
/// `file_size`, and `boot_policy`.
/// This lets the firmware decide on images that the shim can't verify.
unsafe fn original_file_authentication(
this: *const SecurityArch2Protocol,
path: *const FfiDevicePath,
file_buffer: *const u8,
file_size: usize,
boot_policy: Boolean,
) -> Status {
// Acquire the global hook state to grab the original hook.
let function = match GLOBAL_HOOK_STATE.lock().as_ref() {
// The hook state is available, so we can acquire the original hook.
Some(state) => state.original_hook2.file_authentication,
// The hook state is not available, so we can't call the original hook.
None => {
warn!("global hook state is not available, unable to call original hook");
return Status::LOAD_ERROR;
}
};
// Call the original hook function to see what it reports.
// SAFETY: This function is safe to call as it is stored by us and is required
// in the UEFI specification.
unsafe { function(this, path, file_buffer, file_size, boot_policy) }
}
/// File authentication state verifier for the EFI_SECURITY_ARCH protocol.
/// Takes the `path` and determines the verification.
/// Anything the shim can't verify is passed to the original hook.
unsafe extern "efiapi" fn arch_file_authentication_state(
this: *const SecurityArchProtocol,
status: u32,
path: *const FfiDevicePath,
) -> Status {
// Without a path, there is nothing for the shim to read and verify.
if path.is_null() {
// SAFETY: The arguments are passed through unchanged from the firmware.
return unsafe { Self::original_file_authentication_state(this, status, path) };
}
// Construct a shim input from the path.
let input = ShimInput::SecurityHookPath(path);
// Convert the input to an owned data buffer.
let input = match input.into_owned_data_buffer() {
Ok(input) => input,
// If the data can't be read, such as for a path the shim can't access,
// let the original hook decide.
Err(error) => {
warn!("unable to read data to be authenticated: {}", error);
// SAFETY: The arguments are passed through unchanged from the firmware.
return unsafe { Self::original_file_authentication_state(this, status, path) };
}
};
// Verify the input, if it fails, call the original hook.
if !Self::verify(input) {
// SAFETY: The arguments are passed through unchanged from the firmware.
unsafe { Self::original_file_authentication_state(this, status, path) }
} else {
Status::SUCCESS
}
}
/// File authentication verifier for the EFI_SECURITY_ARCH2 protocol.
/// Takes the `path` and a file buffer to determine the verification.
/// Anything the shim can't verify is passed to the original hook.
unsafe extern "efiapi" fn arch2_file_authentication(
this: *const SecurityArch2Protocol,
path: *const FfiDevicePath,
file_buffer: *const u8,
file_size: usize,
boot_policy: Boolean,
) -> Status {
// The path and file buffer are optional, and the shim can't verify without them.
// The shim also doesn't support the boot policy.
if path.is_null() || file_buffer.is_null() || bool::from(boot_policy) {
// SAFETY: The arguments are passed through unchanged from the firmware.
return unsafe {
Self::original_file_authentication(this, path, file_buffer, file_size, boot_policy)
};
}
// Construct a slice out of the file buffer and size.
let buffer = unsafe { slice::from_raw_parts(file_buffer, file_size) };
// Construct a shim input from the path.
let input = ShimInput::SecurityHookBuffer(Some(path), buffer);
// Verify the input, if it fails, call the original hook.
if !Self::verify(input) {
// SAFETY: The arguments are passed through unchanged from the firmware.
unsafe {
Self::original_file_authentication(this, path, file_buffer, file_size, boot_policy)
}
} else {
Status::SUCCESS
}
}
/// Install the security hook if needed.
pub fn install() -> Result<bool> {
// Find the security arch protocol. If we can't find it, we will return false.
let Some(hook_arch) = crate::handle::find_handle(&SECURITY_ARCH_GUID)
.context("unable to check security arch existence")?
else {
return Ok(false);
};
// Find the security arch2 protocol. If we can't find it, we will return false.
let Some(hook_arch2) = crate::handle::find_handle(&SECURITY_ARCH2_GUID)
.context("unable to check security arch2 existence")?
else {
return Ok(false);
};
// Open the security arch protocol.
let mut arch_protocol =
uefi::boot::open_protocol_exclusive::<SecurityArchProtocol>(hook_arch)
.context("unable to open security arch protocol")?;
// Open the security arch2 protocol.
let mut arch_protocol2 =
uefi::boot::open_protocol_exclusive::<SecurityArch2Protocol>(hook_arch2)
.context("unable to open security arch2 protocol")?;
// Construct the global state to store.
let state = SecurityHookState {
original_hook: SecurityArchProtocol {
file_authentication_state: arch_protocol.file_authentication_state,
},
original_hook2: SecurityArch2Protocol {
file_authentication: arch_protocol2.file_authentication,
},
};
// Acquire the lock to the global state and replace it.
let mut global_state = GLOBAL_HOOK_STATE.lock();
// If the hook is already installed, the protocols contain our hooks.
// Storing them as the original hooks would cause infinite recursion on
// verification failure, so keep the existing state instead.
if global_state.is_some() {
return Ok(true);
}
global_state.replace(state);
// Install the hooks into the UEFI stack.
arch_protocol.file_authentication_state = Self::arch_file_authentication_state;
arch_protocol2.file_authentication = Self::arch2_file_authentication;
Ok(true)
}
/// Uninstalls the global security hook, if installed.
pub fn uninstall() -> Result<()> {
// Find the security arch protocol. If we can't find it, we will do nothing.
let Some(hook_arch) = crate::handle::find_handle(&SECURITY_ARCH_GUID)
.context("unable to check security arch existence")?
else {
return Ok(());
};
// Find the security arch2 protocol. If we can't find it, we will do nothing.
let Some(hook_arch2) = crate::handle::find_handle(&SECURITY_ARCH2_GUID)
.context("unable to check security arch2 existence")?
else {
return Ok(());
};
// Open the security arch protocol.
let mut arch_protocol =
uefi::boot::open_protocol_exclusive::<SecurityArchProtocol>(hook_arch)
.context("unable to open security arch protocol")?;
// Open the security arch2 protocol.
let mut arch_protocol2 =
uefi::boot::open_protocol_exclusive::<SecurityArch2Protocol>(hook_arch2)
.context("unable to open security arch2 protocol")?;
// Acquire the lock to the global state.
let mut global_state = GLOBAL_HOOK_STATE.lock();
// Take the state and replace the original functions.
let Some(state) = global_state.take() else {
return Ok(());
};
// Reinstall the original functions.
arch_protocol.file_authentication_state = state.original_hook.file_authentication_state;
arch_protocol2.file_authentication = state.original_hook2.file_authentication;
Ok(())
}
}
+30
View File
@@ -0,0 +1,30 @@
use alloc::vec::Vec;
use anyhow::{Context, Result, bail};
use uefi::CString16;
/// Convert a byte slice into a CString16.
pub fn utf16_bytes_to_cstring16(bytes: &[u8]) -> Result<CString16> {
// Validate the input bytes are the right length.
if !bytes.len().is_multiple_of(2) {
bail!("utf16 bytes must be a multiple of 2");
}
// Convert the bytes to UTF-16 data.
let mut data = bytes
// Chunk everything into two bytes.
.as_chunks::<2>()
.0
.iter()
// Reinterpret the bytes as u16 little-endian.
.map(|chunk| u16::from_le_bytes([chunk[0], chunk[1]]))
// Stop at the first null terminator, if any.
// This ignores any trailing data, such as extra null padding.
.take_while(|c| *c != 0)
// Collect the result into a vector.
.collect::<Vec<_>>();
// Add the null terminator, as the input is not guaranteed to be null-terminated.
data.push(0);
CString16::try_from(data).context("unable to convert utf16 bytes to CString16")
}
+157
View File
@@ -0,0 +1,157 @@
use crate::strings;
use alloc::format;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use anyhow::{Context, Result};
use log::warn;
use uefi::{CString16, guid};
use uefi_raw::Status;
use uefi_raw::table::runtime::{VariableAttributes, VariableVendor};
/// The classification of a variable.
/// This is an abstraction over various variable attributes.
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum VariableClass {
/// The variable is available in Boot Services and Runtime Services and is not persistent.
BootAndRuntimeTemporary,
}
impl VariableClass {
/// The [VariableAttributes] for this classification.
fn attributes(&self) -> VariableAttributes {
match self {
VariableClass::BootAndRuntimeTemporary => {
VariableAttributes::BOOTSERVICE_ACCESS | VariableAttributes::RUNTIME_ACCESS
}
}
}
}
/// Provides access to a particular set of vendor variables.
pub struct VariableController {
/// The GUID of the vendor.
vendor: VariableVendor,
}
impl VariableController {
/// Global variables.
pub const GLOBAL: VariableController = VariableController::new(VariableVendor(guid!(
"8be4df61-93ca-11d2-aa0d-00e098032b8c"
)));
/// Create a new [VariableController] for the `vendor`.
pub const fn new(vendor: VariableVendor) -> Self {
Self { vendor }
}
/// Convert `key` to a variable name as a CString16.
fn name(key: &str) -> Result<CString16> {
CString16::try_from(key).context("unable to convert variable name to CString16")
}
/// Retrieve the cstr16 value specified by the `key`.
/// Returns None if the value isn't set.
/// If the value is not decodable, we will return None and log a warning.
pub fn get_cstr16(&self, key: &str) -> Result<Option<String>> {
let name = Self::name(key)?;
// Retrieve the variable data, handling variable not existing as None.
match uefi::runtime::get_variable_boxed(&name, &self.vendor) {
Ok((data, _)) => {
// Try to decode UTF-16 bytes to a CString16.
match strings::utf16_bytes_to_cstring16(&data) {
Ok(value) => {
// We have a value, so return the UTF-8 value.
Ok(Some(value.to_string()))
}
Err(error) => {
// We encountered an error, so warn and return None.
warn!("efi variable '{}' is not valid UTF-16: {}", key, error);
Ok(None)
}
}
}
Err(error) => {
// If the variable does not exist, we will return None.
if error.status() == Status::NOT_FOUND {
Ok(None)
} else {
Err(error).with_context(|| format!("unable to get efi variable {}", key))
}
}
}
}
/// Retrieve a boolean value specified by the `key`.
pub fn get_bool(&self, key: &str) -> Result<bool> {
let name = Self::name(key)?;
// Retrieve the variable data, handling variable not existing as false.
match uefi::runtime::get_variable_boxed(&name, &self.vendor) {
Ok((data, _)) => {
// If the variable is zero-length, we treat it as false.
if data.is_empty() {
Ok(false)
} else {
// We treat the variable as true if the first byte is non-zero.
Ok(data[0] > 0)
}
}
Err(error) => {
// If the variable does not exist, we treat it as false.
if error.status() == Status::NOT_FOUND {
Ok(false)
} else {
Err(error).with_context(|| format!("unable to get efi variable {}", key))
}
}
}
}
/// Set a variable specified by `key` to `value`.
/// The variable `class` controls the attributes for the variable.
pub fn set(&self, key: &str, value: &[u8], class: VariableClass) -> Result<()> {
let name = Self::name(key)?;
uefi::runtime::set_variable(&name, &self.vendor, class.attributes(), value)
.with_context(|| format!("unable to set efi variable {}", key))?;
Ok(())
}
/// Set a variable specified by `key` to `value`, converting the value to
/// a [CString16]. The variable `class` controls the attributes for the variable.
pub fn set_cstr16(&self, key: &str, value: &str, class: VariableClass) -> Result<()> {
// Encode the value as a CString16 little endian.
let mut encoded = value
.encode_utf16()
.flat_map(|c| c.to_le_bytes())
.collect::<Vec<u8>>();
// Add a null terminator to the end of the value.
encoded.extend_from_slice(&[0, 0]);
self.set(key, &encoded, class)
}
/// Set a boolean variable specified by `key` to `value`, converting the value.
/// The variable `class` controls the attributes for the variable.
pub fn set_bool(&self, key: &str, value: bool, class: VariableClass) -> Result<()> {
self.set(key, &[value as u8], class)
}
/// Set the u64 little-endian variable specified by `key` to `value`.
/// The variable `class` controls the attributes for the variable.
pub fn set_u64le(&self, key: &str, value: u64, class: VariableClass) -> Result<()> {
self.set(key, &value.to_le_bytes(), class)
}
/// Remove the variable specified by `key`.
/// This can fail if the variable is not set.
pub fn remove(&self, key: &str) -> Result<()> {
let name = Self::name(key)?;
// Delete the variable from UEFI.
uefi::runtime::delete_variable(&name, &self.vendor)
.with_context(|| format!("unable to remove efi variable {}", key))
}
}
+19
View File
@@ -0,0 +1,19 @@
# 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-parsing"
description = "Sprout Parsing Utilities (UEFI-free)"
license.workspace = true
version.workspace = true
homepage.workspace = true
repository.workspace = true
edition.workspace = true
[dependencies]
hex.workspace = true
sha2.workspace = true
[lib]
name = "edera_sprout_parsing"
path = "src/lib.rs"
+487
View File
@@ -0,0 +1,487 @@
#![no_std]
extern crate alloc;
use alloc::collections::BTreeMap;
use alloc::format;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use core::cmp::Reverse;
use sha2::{Digest, Sha256};
/// 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.
pub 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)
}
/// Builds out multiple generations of `input` based on a matrix style.
/// For example, if input is: {"x": ["a", "b"], "y": ["c", "d"]}
/// It will produce:
/// x: a, y: c
/// x: a, y: d
/// x: b, y: c
/// x: b, y: d
pub fn build_matrix(input: &BTreeMap<String, Vec<String>>) -> Vec<BTreeMap<String, String>> {
// Convert the input into a vector of tuples.
let items: Vec<(String, Vec<String>)> = input.clone().into_iter().collect();
// The result is a vector of maps.
let mut result: Vec<BTreeMap<String, String>> = alloc::vec![BTreeMap::new()];
for (key, values) in items {
let mut new_result = Vec::new();
// Produce all the combinations of the input values.
for combination in &result {
for value in &values {
let mut new_combination = combination.clone();
new_combination.insert(key.clone(), value.clone());
new_result.push(new_combination);
}
}
result = new_result;
}
result.into_iter().filter(|item| !item.is_empty()).collect()
}
/// Combine a sequence of strings into a single string, separated by spaces, ignoring empty strings.
pub fn combine_options<T: AsRef<str>>(options: impl Iterator<Item = T>) -> String {
options
.flat_map(|item| empty_is_none(Some(item)))
.map(|item| item.as_ref().to_string())
.collect::<Vec<_>>()
.join(" ")
}
/// The alignment of each initrd when initrds are concatenated.
const INITRD_ALIGNMENT: usize = 4;
/// Append the `initrd` to the concatenated `initrds`, padding with zeros so that it starts
/// on a 4-byte boundary. Linux only finds a cpio archive at a 4-byte aligned offset when
/// unpacking concatenated initrds, as GRUB and systemd-boot pad them the same way.
pub fn append_initrd(initrds: &mut Vec<u8>, initrd: &[u8]) {
let padded = initrds.len().next_multiple_of(INITRD_ALIGNMENT);
initrds.resize(padded, 0);
initrds.extend_from_slice(initrd);
}
/// Produce a unique hash for the input.
/// This uses SHA-256, which is unique enough but relatively short.
pub fn unique_hash(input: &str) -> String {
hex::encode(Sha256::digest(input.as_bytes()))
}
/// Filter a string-like Option `input` such that an empty string is [None].
pub fn empty_is_none<T: AsRef<str>>(input: Option<T>) -> Option<T> {
input.filter(|input| !input.as_ref().is_empty())
}
/// Build a Xen EFI stub configuration file from pre-stamped `xen_options` and `kernel_options`.
/// The returned string is in the Xen ini-like config file format.
pub fn build_xen_config(xen_options: &str, kernel_options: &str) -> String {
[
// global section
"[global]",
// default configuration section
"default=sprout",
// configuration section for sprout
"[sprout]",
// xen options
&format!("options={}", xen_options),
// kernel options, stub replaces the kernel path
// the kernel is provided via media loader
&format!("kernel=stub {}", kernel_options),
// required or else the last line will be ignored
"",
]
.join("\n")
}
/// Filename prefixes used to identify Linux kernel images.
/// These must be lowercase, as file names are lowercased before matching.
pub const LINUX_KERNEL_PREFIXES: &[&str] = &["vmlinuz", "image"];
/// Filename prefixes used to identify initramfs images paired with a kernel.
pub const LINUX_INITRAMFS_PREFIXES: &[&str] = &["initramfs", "initrd", "initrd.img"];
/// Check whether `name` (already lowercased) matches one of the `kernel_prefixes`,
/// either exactly or as a dash-separated prefix (e.g. `"vmlinuz-6.1"`).
/// Returns the matched prefix string if found.
pub fn match_kernel_prefix<'a>(name: &str, kernel_prefixes: &[&'a str]) -> Option<&'a str> {
kernel_prefixes
.iter()
.find(|prefix| name == **prefix || name.starts_with(&format!("{}-", prefix)))
.copied()
}
/// Generate initramfs candidate filenames by combining each entry of `initramfs_prefixes`
/// with `suffix`, and then with `suffix` followed by `.img`, which many distributions use.
/// For example, Arch Linux pairs `vmlinuz-linux` with `initramfs-linux.img`.
/// The caller is expected to check which candidates actually exist.
pub fn initramfs_candidates(
suffix: &str,
initramfs_prefixes: &[&str],
) -> impl Iterator<Item = String> {
let mut candidates: Vec<String> = Vec::new();
for extension in ["", ".img"] {
for prefix in initramfs_prefixes {
let candidate = format!("{}{}{}", prefix, suffix, extension);
// Skip duplicates, like "initrd" with ".img" when "initrd.img" is also a prefix.
if !candidates.contains(&candidate) {
candidates.push(candidate);
}
}
}
candidates.into_iter()
}
#[cfg(test)]
mod tests {
use super::*;
fn map(pairs: &[(&str, &str)]) -> BTreeMap<String, String> {
pairs
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect()
}
#[test]
fn stamp_replaces_known_key() {
let values = map(&[("name", "world")]);
let (changed, result) = stamp_values(&values, "hello $name");
assert!(changed);
assert_eq!(result, "hello world");
}
#[test]
fn stamp_no_match_returns_unchanged() {
let values = map(&[("name", "world")]);
let (changed, result) = stamp_values(&values, "hello there");
assert!(!changed);
assert_eq!(result, "hello there");
}
#[test]
fn stamp_longer_key_takes_precedence_over_shorter() {
// Without longest-first ordering, "$ab" would be partially matched by "$a"
let values = map(&[("a", "WRONG"), ("ab", "RIGHT")]);
let (changed, result) = stamp_values(&values, "$ab");
assert!(changed);
assert_eq!(result, "RIGHT");
}
#[test]
fn stamp_empty_key_is_skipped() {
let values = map(&[("", "should-not-appear"), ("x", "val")]);
let (_, result) = stamp_values(&values, "$x");
assert_eq!(result, "val");
assert!(!result.contains("should-not-appear"));
}
#[test]
fn stamp_multiple_keys_replaced() {
let values = map(&[("a", "foo"), ("b", "bar")]);
let (changed, result) = stamp_values(&values, "$a and $b");
assert!(changed);
assert_eq!(result, "foo and bar");
}
#[test]
fn stamp_empty_text_returns_empty() {
let values = map(&[("a", "foo")]);
let (changed, result) = stamp_values(&values, "");
assert!(!changed);
assert_eq!(result, "");
}
#[test]
fn stamp_empty_map_returns_unchanged() {
let values = map(&[]);
let (changed, result) = stamp_values(&values, "hello $name");
assert!(!changed);
assert_eq!(result, "hello $name");
}
fn matrix_map(pairs: &[(&str, &[&str])]) -> BTreeMap<String, Vec<String>> {
pairs
.iter()
.map(|(k, vs)| (k.to_string(), vs.iter().map(|v| v.to_string()).collect()))
.collect()
}
#[test]
fn matrix_single_key_produces_one_entry_per_value() {
let input = matrix_map(&[("x", &["a", "b", "c"])]);
let result = build_matrix(&input);
assert_eq!(result.len(), 3);
}
#[test]
fn matrix_two_keys_produces_cartesian_product() {
let input = matrix_map(&[("x", &["a", "b"]), ("y", &["c", "d"])]);
let result = build_matrix(&input);
assert_eq!(result.len(), 4);
// Every combination of x and y should be present.
for x in &["a", "b"] {
for y in &["c", "d"] {
assert!(
result
.iter()
.any(|m| m.get("x").map(|s| s.as_str()) == Some(x)
&& m.get("y").map(|s| s.as_str()) == Some(y))
);
}
}
}
#[test]
fn matrix_empty_input_produces_no_entries() {
let input = matrix_map(&[]);
let result = build_matrix(&input);
assert!(result.is_empty());
}
#[test]
fn matrix_key_with_empty_values_produces_no_entries() {
let input = matrix_map(&[("x", &[])]);
let result = build_matrix(&input);
assert!(result.is_empty());
}
#[test]
fn combine_options_joins_with_space() {
let result = combine_options(["a", "b", "c"].iter().copied());
assert_eq!(result, "a b c");
}
#[test]
fn combine_options_skips_empty_strings() {
let result = combine_options(["a", "", "b"].iter().copied());
assert_eq!(result, "a b");
}
#[test]
fn combine_options_all_empty_returns_empty() {
let result = combine_options(["", ""].iter().copied());
assert_eq!(result, "");
}
#[test]
fn combine_options_empty_iterator_returns_empty() {
let result = combine_options(core::iter::empty::<&str>());
assert_eq!(result, "");
}
#[test]
fn empty_is_none_returns_none_for_empty_string() {
assert!(empty_is_none(Some("")).is_none());
}
#[test]
fn empty_is_none_returns_some_for_nonempty_string() {
assert_eq!(empty_is_none(Some("x")), Some("x"));
}
#[test]
fn empty_is_none_passthrough_on_none() {
assert!(empty_is_none(None::<&str>).is_none());
}
#[test]
fn unique_hash_is_deterministic() {
assert_eq!(unique_hash("hello"), unique_hash("hello"));
}
#[test]
fn unique_hash_differs_for_different_inputs() {
assert_ne!(unique_hash("hello"), unique_hash("world"));
}
#[test]
fn unique_hash_empty_string() {
// SHA-256 of "" is well-known; just check it produces a non-empty hex string.
let h = unique_hash("");
assert!(!h.is_empty());
assert!(h.chars().all(|c| c.is_ascii_hexdigit()));
}
#[test]
fn xen_config_contains_global_and_sprout_sections() {
let config = build_xen_config("", "");
assert!(config.contains("[global]"));
assert!(config.contains("[sprout]"));
assert!(config.contains("default=sprout"));
}
#[test]
fn xen_config_embeds_xen_options() {
let config = build_xen_config("--no-real-mode --iommu=no", "");
assert!(config.contains("options=--no-real-mode --iommu=no"));
}
#[test]
fn xen_config_embeds_kernel_options() {
let config = build_xen_config("", "quiet splash");
assert!(config.contains("kernel=stub quiet splash"));
}
#[test]
fn xen_config_ends_with_newline() {
// Required or the last line will be ignored by the Xen config parser.
let config = build_xen_config("", "");
assert!(config.ends_with('\n'));
}
#[test]
fn append_initrd_pads_to_four_bytes() {
let mut initrds = Vec::new();
append_initrd(&mut initrds, b"abcde");
assert_eq!(initrds, b"abcde");
append_initrd(&mut initrds, b"fg");
assert_eq!(initrds, b"abcde\0\0\0fg");
append_initrd(&mut initrds, b"h");
assert_eq!(initrds, b"abcde\0\0\0fg\0\0h");
}
#[test]
fn append_initrd_aligned_needs_no_padding() {
let mut initrds = b"abcd".to_vec();
append_initrd(&mut initrds, b"efgh");
assert_eq!(initrds, b"abcdefgh");
}
#[test]
fn kernel_prefix_exact_match() {
assert_eq!(
match_kernel_prefix("vmlinuz", LINUX_KERNEL_PREFIXES),
Some("vmlinuz")
);
}
#[test]
fn kernel_prefix_dash_suffix_match() {
assert_eq!(
match_kernel_prefix("vmlinuz-6.1.0", LINUX_KERNEL_PREFIXES),
Some("vmlinuz")
);
}
#[test]
fn kernel_prefix_matches_lowercased_arm64_image() {
// Callers lowercase file names before matching, so "Image-6.1" arrives as "image-6.1".
assert_eq!(
match_kernel_prefix("image-6.1", LINUX_KERNEL_PREFIXES),
Some("image")
);
assert_eq!(
match_kernel_prefix("image", LINUX_KERNEL_PREFIXES),
Some("image")
);
}
#[test]
fn kernel_prefix_case_sensitive_no_match() {
// match_kernel_prefix expects the caller to lowercase first; uppercase input won't match.
assert!(match_kernel_prefix("VMLINUZ-6.1", LINUX_KERNEL_PREFIXES).is_none());
}
#[test]
fn kernel_prefix_no_match() {
assert!(match_kernel_prefix("initramfs-6.1", LINUX_KERNEL_PREFIXES).is_none());
}
#[test]
fn kernel_prefix_partial_no_match() {
// "vmlinuz6.1" has no dash separator — should not match.
assert!(match_kernel_prefix("vmlinuz6.1", LINUX_KERNEL_PREFIXES).is_none());
}
#[test]
fn initramfs_candidates_with_suffix() {
let candidates: Vec<_> = initramfs_candidates("-6.1.0", LINUX_INITRAMFS_PREFIXES).collect();
assert_eq!(
candidates,
&[
"initramfs-6.1.0",
"initrd-6.1.0",
"initrd.img-6.1.0",
"initramfs-6.1.0.img",
"initrd-6.1.0.img",
"initrd.img-6.1.0.img",
]
);
}
#[test]
fn initramfs_candidates_include_img_extension() {
// Arch Linux, and Fedora or Gentoo with dracut, add .img to the initramfs name.
let arch: Vec<_> = initramfs_candidates("-linux", LINUX_INITRAMFS_PREFIXES).collect();
assert!(arch.iter().any(|c| c == "initramfs-linux.img"));
let fedora: Vec<_> =
initramfs_candidates("-6.8.5-301.fc40.x86_64", LINUX_INITRAMFS_PREFIXES).collect();
assert!(
fedora
.iter()
.any(|c| c == "initramfs-6.8.5-301.fc40.x86_64.img")
);
}
#[test]
fn initramfs_candidates_empty_suffix() {
let candidates: Vec<_> = initramfs_candidates("", LINUX_INITRAMFS_PREFIXES).collect();
assert_eq!(
candidates,
&[
"initramfs",
"initrd",
"initrd.img",
"initramfs.img",
"initrd.img.img"
]
);
}
#[test]
fn initramfs_candidates_empty_prefixes() {
let candidates: Vec<_> = initramfs_candidates("-6.1.0", &[]).collect();
assert!(candidates.is_empty());
}
}
+146
View File
@@ -0,0 +1,146 @@
# Setup Sprout for Debian with Secure Boot
## Prerequisites
- Modern Debian release: tested on Debian 13 ARM64
- EFI System Partition mounted on `/boot/efi` (the default)
- You will need the following packages installed: `openssl`, `shim-signed`, `mokutil`, `sbsigntool`
## Step 1: Generate and Install Secure Boot Key
```bash
# Create a directory to store the Secure Boot MOK key and certificates.
$ mkdir -p /etc/sprout/secure-boot
# Change to the created directory.
$ cd /etc/sprout/secure-boot
# Generate a MOK key and certificate.
$ openssl req \
-newkey rsa:4096 -nodes -keyout mok.key \
-new -x509 -sha256 -days 3650 -subj '/CN=Sprout Secure Boot/' \
-out mok.crt
# Generate a DER encoded certificate for enrollment.
$ openssl x509 -outform DER -in mok.crt -out mok.cer
# Import the certificate into the Secure Boot environment.
# This will ask you to make a password that will be used during enrollment.
$ mokutil --import mok.cer
# Reboot your machine.
# During boot, MOK enrollment should appear. If it doesn't, ensure you are booting into the shim.
# Press any key to begin MOK management. Select "Enroll MOK".
# Select "View key 0", and ensure the subject says "CN=Sprout Secure Boot".
# If the subject does not match, something has gone wrong with MOK enrollment.
# Press Enter to continue, then select the "Continue" option.
# When it asks to enroll the key, select the "Yes" option.
# Enter the password that you created during the mokutil --import step.
# Select "Reboot" to boot back into your Operating System.
```
## Step 2: Prepare the Secure Boot Environment
```bash
# Create a directory for Sprout EFI artifacts.
$ mkdir -p /boot/efi/EFI/sprout
# For x86_64, copy the following artifacts to the Sprout EFI directory.
$ cp /usr/lib/shim/shimx64.efi.signed /boot/efi/EFI/sprout/shimx64.efi
$ cp /usr/lib/shim/mmx64.efi.signed /boot/efi/EFI/sprout/mmx64.efi
$ cp /usr/lib/shim/fbx64.efi.signed /boot/efi/EFI/sprout/fbx64.efi
# For aarch64, copy the following artifacts to the Sprout EFI directory.
$ cp /usr/lib/shim/shimaa64.efi.signed /boot/efi/EFI/sprout/shimaa64.efi
$ cp /usr/lib/shim/mmaa64.efi.signed /boot/efi/EFI/sprout/mmaa64.efi
$ cp /usr/lib/shim/fbaa64.efi.signed /boot/efi/EFI/sprout/fbaa64.efi
```
## Step 3: Install Unsigned Sprout
Download the latest sprout.efi release from the [GitHub releases page](https://github.com/edera-dev/sprout/releases).
For x86_64 systems, download the `sprout-x86_64.efi` file, and for ARM64 systems, download the `sprout-aarch64.efi` file.
Copy the downloaded file to `/boot/efi/EFI/sprout/sprout.unsigned.efi` on your EFI System Partition.
## Step 4: Sign Sprout for Secure Boot
```bash
# For x86_64, sign the unsigned Sprout artifact and name it grubx64.efi which is what the shim will call.
$ sbsign \
--key /etc/sprout/secure-boot/mok.key \
--cert /etc/sprout/secure-boot/mok.crt \
--output /boot/efi/EFI/sprout/grubx64.efi \
/boot/efi/EFI/sprout/sprout.unsigned.efi
# For aarch64, sign the unsigned Sprout artifact and name it grubaa64.efi which is what the shim will call.
$ sbsign \
--key /etc/sprout/secure-boot/mok.key \
--cert /etc/sprout/secure-boot/mok.crt \
--output /boot/efi/EFI/sprout/grubaa64.efi \
/boot/efi/EFI/sprout/sprout.unsigned.efi
```
## Step 5: Install and Sign EFI Drivers
You will need a filesystem EFI driver if `/boot` is not FAT32 or ExFAT.
If `/boot` is FAT32 or ExFAT, you can skip this step.
Most Debian systems use an ext4 filesystem for `/boot`.
You can download an EFI filesystem driver from [EfiFs releases](https://github.com/pbatard/EfiFs/releases).
For ext4, download the `ext2` file for your platform. It should work for ext4 filesystems too.
If you have an EFI driver, copy the driver to `/boot/efi/EFI/sprout/DRIVER_NAME.unsigned.efi` for signing.
For example, the `ext4` driver, copy the `ext4.efi` file to `/boot/efi/EFI/sprout/ext4.unsigned.efi`.
Then sign the driver with the Sprout Secure Boot key:
```bash
# Sign the ext4 driver at ext4.unsigned.efi, placing it at ext4.efi, which will be used in the configuration.
$ sbsign \
--key /etc/sprout/secure-boot/mok.key \
--cert /etc/sprout/secure-boot/mok.crt \
--output /boot/efi/EFI/sprout/ext4.efi \
/boot/efi/EFI/sprout/ext4.unsigned.efi
```
## Step 6: Create Sprout Configuration
Write the following to the file `/boot/efi/sprout.toml`:
```toml
# sprout configuration: version 1
version = 1
# global values.
[values]
# your linux kernel command line.
linux-options = "root=UUID=MY_ROOT_UUID"
# load an ext4 EFI driver.
# skip this if you do not have a filesystem driver.
# if your filesystem driver is not named ext4, change accordingly.
[drivers.ext4]
path = "\\EFI\\sprout\\ext4.efi"
# global options.
[options]
# enable autoconfiguration by detecting installed kernels
# generating boot entries for them.
autoconfigure = true
```
Ensure you add the signed driver paths to the configuration, not the unsigned ones.
If you do not have any drivers, exclude the drivers section entirely.
## Step 7: Configure Sprout Boot Entry
In the following commands, replace /dev/BLOCK_DEVICE with the device that houses your GPT partition table,
and PARTITION_NUMBER with the partition number of the EFI System Partition. For example, if your EFI System Partition is
`/dev/sda1`, the BLOCK_DEVICE would be `/dev/sda` and the PARTITION_NUMBER would be `1`
```bash
# For x86_64, run this command to add Sprout as the default boot entry.
$ efibootmgr -d /dev/BLOCK_DEVICE -p PARTITION_NUMBER -c -L 'Sprout' -l '\EFI\sprout\shimx64.efi'
# For aarch64, run this command to add Sprout as the default boot entry.
$ efibootmgr -d /dev/BLOCK_DEVICE -p PARTITION_NUMBER -c -L 'Sprout' -l '\EFI\sprout\shimaa64.efi'
```
Reboot your machine and it should boot into Sprout.
If Sprout fails to boot, it should boot into the original bootloader.
+172
View File
@@ -0,0 +1,172 @@
# Setup Sprout for Fedora with Secure Boot
## Prerequisites
- Modern Fedora release: tested on Fedora 43 x86_64.
- EFI System Partition mounted on `/boot/efi` (the default)
- You will need the following packages installed: `openssl`, `mokutil`, `sbsigntools`, `efibootmgr`
**NOTE**: Fedora on ARM64 itself does not support Secure Boot consistently.
## Step 1: Generate and Install Secure Boot Key
```bash
# Create a directory to store the Secure Boot MOK key and certificates.
$ mkdir -p /etc/sprout/secure-boot
# Change to the created directory.
$ cd /etc/sprout/secure-boot
# Generate a MOK key and certificate.
$ openssl req \
-newkey rsa:4096 -nodes -keyout mok.key \
-new -x509 -sha256 -days 3650 -subj '/CN=Sprout Secure Boot/' \
-out mok.crt
# Generate a DER encoded certificate for enrollment.
$ openssl x509 -outform DER -in mok.crt -out mok.cer
# Import the certificate into the Secure Boot environment.
# This will ask you to make a password that will be used during enrollment.
$ mokutil --import mok.cer
# Reboot your machine.
# During boot, MOK enrollment should appear. If it doesn't, ensure you are booting into the shim.
# Press any key to begin MOK management. Select "Enroll MOK".
# Select "View key 0", and ensure the subject says "CN=Sprout Secure Boot".
# If the subject does not match, something has gone wrong with MOK enrollment.
# Press Enter to continue, then select the "Continue" option.
# When it asks to enroll the key, select the "Yes" option.
# Enter the password that you created during the mokutil --import step.
# Select "Reboot" to boot back into your Operating System.
```
## Step 2: Prepare the Secure Boot Environment
```bash
# Create a directory for Sprout EFI artifacts.
$ mkdir -p /boot/efi/EFI/sprout
# For x86_64, copy the following artifacts to the Sprout EFI directory.
$ cp /boot/efi/EFI/fedora/shimx64.efi /boot/efi/EFI/sprout/shimx64.efi
$ cp /boot/efi/EFI/fedora/mmx64.efi /boot/efi/EFI/sprout/mmx64.efi
# For aarch64, copy the following artifacts to the Sprout EFI directory.
$ cp /boot/efi/EFI/fedora/shimaa64.efi /boot/efi/EFI/sprout/shimaa64.efi
$ cp /boot/efi/EFI/fedora/mmaa64.efi /boot/efi/EFI/sprout/mmaa64.efi
```
## Step 3: Install Unsigned Sprout
Download the latest sprout.efi release from the [GitHub releases page](https://github.com/edera-dev/sprout/releases).
For x86_64 systems, download the `sprout-x86_64.efi` file, and for ARM64 systems, download the `sprout-aarch64.efi` file.
Copy the downloaded file to `/boot/efi/EFI/sprout/sprout.unsigned.efi` on your EFI System Partition.
## Step 4: Sign Sprout for Secure Boot
```bash
# For x86_64, sign the unsigned Sprout artifact and name it grubx64.efi which is what the shim will call.
$ sbsign \
--key /etc/sprout/secure-boot/mok.key \
--cert /etc/sprout/secure-boot/mok.crt \
--output /boot/efi/EFI/sprout/grubx64.efi \
/boot/efi/EFI/sprout/sprout.unsigned.efi
# For aarch64, sign the unsigned Sprout artifact and name it grubaa64.efi which is what the shim will call.
$ sbsign \
--key /etc/sprout/secure-boot/mok.key \
--cert /etc/sprout/secure-boot/mok.crt \
--output /boot/efi/EFI/sprout/grubaa64.efi \
/boot/efi/EFI/sprout/sprout.unsigned.efi
```
## Step 5: Install and Sign EFI Drivers
You will need a filesystem EFI driver if `/boot` is not FAT32 or ExFAT.
### ext4
Most Fedora systems use an ext4 filesystem for `/boot`, if that is the case, use the ext4 instructions here:
Install the necessary `edk2-ext4` package:
```bash
# Install the ext4 driver from the package manager.
$ dnf install edk2-ext4
```
Copy the ext4 driver to `/boot/efi/EFI/sprout/ext4.unsigned.efi`:
```bash
# For x86_64, copy the ext4x64.efi driver to the Sprout EFI directory.
$ cp /usr/share/edk2/drivers/ext4x64.efi /boot/efi/EFI/sprout/ext4.unsigned.efi
# For aarch64, copy the ext4aa64.efi driver to the Sprout EFI directory.
$ cp /usr/share/edk2/drivers/ext4aa64.efi /boot/efi/EFI/sprout/ext4.unsigned.efi
```
```bash
# Sign the ext4 driver at ext4.unsigned.efi, placing it at ext4.efi, which will be used in the configuration.
$ sbsign \
--key /etc/sprout/secure-boot/mok.key \
--cert /etc/sprout/secure-boot/mok.crt \
--output /boot/efi/EFI/sprout/ext4.efi \
/boot/efi/EFI/sprout/ext4.unsigned.efi
```
### Other Filesystems
If you need another driver, you can download EFI filesystem drivers from [EfiFs releases](https://github.com/pbatard/EfiFs/releases).
Copy the driver to `/boot/efi/EFI/sprout/DRIVER_NAME.unsigned.efi` for signing, then sign it like this:
```bash
# Sign your driver, placing it at DRIVER_NAME.efi, which will be used in the configuration.
$ sbsign \
--key /etc/sprout/secure-boot/mok.key \
--cert /etc/sprout/secure-boot/mok.crt \
--output /boot/efi/EFI/sprout/DRIVER_NAME.efi \
/boot/efi/EFI/sprout/DRIVER_NAME.unsigned.efi
```
You will add the driver in your Sprout configuration below, like this:
```toml
[drivers.DRIVER_NAME]
path = "\\EFI\\sprout\\DRIVER_NAME.efi"
```
## Step 6: Create Sprout Configuration
Write the following to the file `/boot/efi/sprout.toml`:
```toml
# sprout configuration: version 1
version = 1
# load an ext4 EFI driver.
# skip this if you do not have a filesystem driver.
# if your filesystem driver is not named ext4, change accordingly.
[drivers.ext4]
path = "\\EFI\\sprout\\ext4.efi"
# global options.
[options]
# enable autoconfiguration by detecting bls enabled
# filesystems and generating boot entries for them.
autoconfigure = true
```
Ensure you add the signed driver paths to the configuration, not the unsigned ones.
If you do not have any drivers, exclude the drivers section entirely.
## Step 7: Configure Sprout Boot Entry
In the following commands, replace /dev/BLOCK_DEVICE with the device that houses your GPT partition table,
and PARTITION_NUMBER with the partition number of the EFI System Partition. For example, if your EFI System Partition is
`/dev/sda1`, the BLOCK_DEVICE would be `/dev/sda` and the PARTITION_NUMBER would be `1`
```bash
# For x86_64, run this command to add Sprout as the default boot entry.
$ efibootmgr -d /dev/BLOCK_DEVICE -p PARTITION_NUMBER -c -L 'Sprout' -l '\EFI\sprout\shimx64.efi'
# For aarch64, run this command to add Sprout as the default boot entry.
$ efibootmgr -d /dev/BLOCK_DEVICE -p PARTITION_NUMBER -c -L 'Sprout' -l '\EFI\sprout\shimaa64.efi'
```
Reboot your machine and it should boot into Sprout.
If Sprout fails to boot, it should boot into the original bootloader.
+143
View File
@@ -0,0 +1,143 @@
# Setup Sprout for openSUSE with Secure Boot
**NOTE:** This guide may not function as written if the system validates hashes.
If your system validates hashes in the shim, you will need to use MokManager to enroll the hashes
of every EFI file involved, such as Sprout and any EFI drivers.
## Prerequisites
- Modern openSUSE release: tested on openSUSE Tumbleweed ARM64
- EFI System Partition mounted on `/boot/efi` (the default)
- You will need the following packages installed: `openssl`, `shim`, `mokutil`, `sbsigntools`
## Step 1: Generate and Install Secure Boot Key
```bash
# Create a directory to store the Secure Boot MOK key and certificates.
$ mkdir -p /etc/sprout/secure-boot
# Change to the created directory.
$ cd /etc/sprout/secure-boot
# Generate a MOK key and certificate.
$ openssl req \
-newkey rsa:4096 -nodes -keyout mok.key \
-new -x509 -sha256 -days 3650 -subj '/CN=Sprout Secure Boot/' \
-out mok.crt
# Generate a DER encoded certificate for enrollment.
$ openssl x509 -outform DER -in mok.crt -out mok.cer
# Import the certificate into the Secure Boot environment.
# This will ask you to make a password that will be used during enrollment.
$ mokutil --import mok.cer
# Reboot your machine.
# During boot, MOK enrollment should appear. If it doesn't, ensure you are booting into the shim.
# Press any key to begin MOK management. Select "Enroll MOK".
# Select "View key 0", and ensure the subject says "CN=Sprout Secure Boot".
# If the subject does not match, something has gone wrong with MOK enrollment.
# Press Enter to continue, then select the "Continue" option.
# When it asks to enroll the key, select the "Yes" option.
# Enter the password that you created during the mokutil --import step.
# Select "Reboot" to boot back into your Operating System.
```
## Step 2: Prepare the Secure Boot Environment
```bash
# Create a directory for Sprout EFI artifacts.
$ mkdir -p /boot/efi/EFI/sprout
# For x86_64, copy the following artifacts to the Sprout EFI directory.
$ cp /usr/share/efi/x86_64/shim.efi /boot/efi/EFI/sprout/shim.efi
$ cp /usr/share/efi/x86_64/MokManager.efi /boot/efi/EFI/sprout/MokManager.efi
$ cp /usr/share/efi/x86_64/fallback.efi /boot/efi/EFI/sprout/fallback.efi
# For aarch64, copy the following artifacts to the Sprout EFI directory.
$ cp /usr/share/efi/aarch64/shim.efi /boot/efi/EFI/sprout/shim.efi
$ cp /usr/share/efi/aarch64/MokManager.efi /boot/efi/EFI/sprout/MokManager.efi
$ cp /usr/share/efi/aarch64/fallback.efi /boot/efi/EFI/sprout/fallback.efi
```
## Step 3: Install Unsigned Sprout
Download the latest sprout.efi release from the [GitHub releases page](https://github.com/edera-dev/sprout/releases).
For x86_64 systems, download the `sprout-x86_64.efi` file, and for ARM64 systems, download the `sprout-aarch64.efi`
file.
Copy the downloaded file to `/boot/efi/EFI/sprout/sprout.unsigned.efi` on your EFI System Partition.
## Step 4: Sign Sprout for Secure Boot
```bash
# Sign the unsigned Sprout artifact and name it grub.efi which is what the shim will call.
$ sbsign \
--key /etc/sprout/secure-boot/mok.key \
--cert /etc/sprout/secure-boot/mok.crt \
--output /boot/efi/EFI/sprout/grub.efi \
/boot/efi/EFI/sprout/sprout.unsigned.efi
```
## Step 5: Install and Sign EFI Drivers
You will need a filesystem EFI driver if `/boot` is not FAT32 or ExFAT.
If `/boot` is FAT32 or ExFAT, you can skip this step.
openSUSE puts `/boot` on a Btrfs filesystem by default, but it may be ext4 or another filesystem depending on how
the system was installed. This guide uses an ext4 driver as the example, so use the driver for your filesystem instead.
You can download an EFI filesystem driver from [EfiFs releases](https://github.com/pbatard/EfiFs/releases).
For Btrfs, download the `btrfs` file for your platform.
For ext4, download the `ext2` file for your platform. It should work for ext4 filesystems too.
If you have an EFI driver, copy the driver to `/boot/efi/EFI/sprout/DRIVER_NAME.unsigned.efi` for signing.
For example, the `ext4` driver, copy the `ext4.efi` file to `/boot/efi/EFI/sprout/ext4.unsigned.efi`.
Then sign the driver with the Sprout Secure Boot key:
```bash
# Sign the ext4 driver at ext4.unsigned.efi, placing it at ext4.efi, which will be used in the configuration.
$ sbsign \
--key /etc/sprout/secure-boot/mok.key \
--cert /etc/sprout/secure-boot/mok.crt \
--output /boot/efi/EFI/sprout/ext4.efi \
/boot/efi/EFI/sprout/ext4.unsigned.efi
```
## Step 6: Create Sprout Configuration
Write the following to the file `/boot/efi/sprout.toml`:
```toml
# sprout configuration: version 1
version = 1
# global values.
[values]
# your linux kernel command line.
linux-options = "root=UUID=MY_ROOT_UUID"
# load an ext4 EFI driver.
# skip this if you do not have a filesystem driver.
# if your filesystem driver is not named ext4, change accordingly.
[drivers.ext4]
path = "\\EFI\\sprout\\ext4.efi"
# global options.
[options]
# enable autoconfiguration by detecting bls enabled filesystems
# or linux kernels and generating boot entries for them.
autoconfigure = true
```
Ensure you add the signed driver paths to the configuration, not the unsigned ones.
If you do not have any drivers, exclude the drivers section entirely.
## Step 7: Configure Sprout Boot Entry
In the following commands, replace /dev/BLOCK_DEVICE with the device that houses your GPT partition table,
and PARTITION_NUMBER with the partition number of the EFI System Partition. For example, if your EFI System Partition is
`/dev/sda1`, the BLOCK_DEVICE would be `/dev/sda` and the PARTITION_NUMBER would be `1`
```bash
# Run this command to add Sprout as the default boot entry.
$ efibootmgr -d /dev/BLOCK_DEVICE -p PARTITION_NUMBER -c -L 'Sprout' -l '\EFI\sprout\shim.efi'
```
Reboot your machine and it should boot into Sprout.
If Sprout fails to boot, it should boot into the original bootloader.
Loaded 100 of 149 files, more files were not shown because too many files have changed in this diff. Show more