Commit Graph
96 Commits
Author SHA1 Message Date
alex 5e8fe7c783 feat(menu): add a graphical boot menu (#106)
* feat(menu): add a graphical boot menu

Adds the graphical menu style, selected with menu-style = "graphical" or
--menu-style=graphical. It draws to the graphics output with the existing
framebuffer, using a built-in 8x8 font, and takes input from the keyboard
and from relative or absolute pointers. The Sprout logo is drawn in the
bottom right corner. If there is no graphics output, the simple menu is
used, which still falls back to the basic menu.

The logo is converted from assets/logo-small.png to raw pixels by the build
crate, which adds the png crate as a build-time dependency only.

The default menu style is still simple. Pointer input is untested, as
OVMF has no mouse driver.

* fix(build): only build the logo conversion on the host

CI builds the whole workspace for the UEFI target, which also builds the
build crate for UEFI. The png dependency pulls in simd-adler32, which
crashes LLVM on that target. The logo conversion only runs in build
scripts on the host, so png and the conversion are now left out when
targeting UEFI.
2026-10-03 17:01:48 -07:00
alex 96b6c6f86a feat(sprout): generator variants (#105)
Generators can now declare variant axes. Every generated entry is
multiplied by each combination of choices, one per axis. A choice sets
the axis name to the choice name and inserts its own values, and the
entry name is suffixed with the choice names.

Generators can also declare exclude rules, which remove generated
entries whose values match every glob pattern in a rule. This covers
unwanted variant combinations and entries like BLS rescue kernels.

Entry matching for default-entry and the bootloader interface now
accepts `*` anywhere in the pattern instead of only at the end, so
generated entry names can be matched regardless of the kernel version
in the middle of the name.

The BLS generator gains a pin-names option (default true, the existing
behavior). Two BLS generators reading the same directory previously
produced entries with identical names; disabling pin-names prefixes
them with the generator name instead. Sprout now warns when more than
one entry has the same name.

* docs(config): note that variants suffix pinned BLS entry names

Pinned BLS entry names match the BLS entry ids, but variants still
append their choice names, so those entries need a pattern like
<id>-* to be selected.
2026-10-03 15:16:05 -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 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 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