Commit Graph
131 Commits
Author SHA1 Message Date
alex 5a4bfefb6e fix(boot): count tries without using them, add XBOOTLDR once, and warn about dropped initrds
An entry that is not counted in strict mode still has its tries, so it
can be bad. Strict mode adds the extended boot loader partition to the
first BLS generator only. A chainload chain that does not list an initrd
that the entry has now warns, as the kernel may not find its root
without it, and strict autoconfigure says when it can't find the
partition of Sprout.
2026-10-04 10:55:07 -07:00
alex 47020c7b92 fix(bls): keep the Sprout name and version fields outside strict mode 2026-10-04 10:55:07 -07:00
alex 975df0457e fix(boot): show the path of a missing BLS file in the warning 2026-10-04 10:55:07 -07:00
alex 14463d5776 feat(boot): read only the boot partitions for BLS in strict mode
In strict mode autoconfigure reads BLS entries from the partition Sprout
was loaded from and the extended boot loader partition of its disk, with
one generator, and no longer makes entries from other disks. Other
filesystems that have BLS are still not scanned for Linux kernels.
2026-10-04 10:55:07 -07:00
alex ca01353df4 feat(boot): make strict mode follow systemd-boot's selection and menu rules
In strict mode a one-shot entry only becomes the default, the default
entry may be one with no boot counter tries left, no menu timeout means a
hidden menu, the menu is shown again when a started image returns, and
entries that mix linux, efi and uki, entries whose file is missing, and
unified kernel images with no name are hidden. Plain EFI entries are not
counted, the XBOOTLDR partition is always read, and a devicetree that
can't be installed fails the entry.

Outside of strict mode these keep working as they did, and each one is
now logged where it applies. The initrd limit of BLS entries is 32.
A devicetree that can't be installed is skipped with a warning.
2026-10-04 10:55:07 -07:00
alex bd37e9058c feat(bls): follow the systemd-boot fallbacks for unified kernel image names 2026-10-04 10:55:07 -07:00
alex 7e1207097b feat(boot): add the bls-strict-mode option
Add --bls-strict-mode and bls-strict-mode in the options of sprout.toml.
It does nothing yet; the behaviors it controls follow.
2026-10-04 10:55:07 -07:00
alex bfca43fe07 fix(boot): let the bootloader interface outrank the configuration
The default entry and the menu timeout that bootctl sets in LoaderEntryDefault,
LoaderEntryPreferred and LoaderConfigTimeout now win over default-entry and
menu-timeout in sprout.toml, as they do over loader.conf in systemd-boot.
--menu-timeout and the one-shot timeout still win over everything.
2026-10-04 10:55:07 -07:00
alex 4e10d57143 fix(boot): tidy the devicetree, reboot and profile handling after review
Only warn about a devicetree under Secure Boot when the entry has one,
skip the fixup if its protocol can't be opened, and reject a devicetree
whose size is smaller than its header. Announce a reboot before the
delay, advertise the multi-profile UKI feature, and treat a Type 1 entry
with a profile after the first as an extra profile that is not picked as
the fallback default. A failure to read Sprout's own partition no longer
drops the entries of the XBOOTLDR partition, and the README lists every
initrd slot and describes the reboot behaviour more exactly.
2026-10-03 20:23:57 -07:00
alex 1658ce0bef feat(bls): read the extended boot loader partition in the BLS generator
Add an xbootldr option to the BLS generator. When it is enabled, the
partition with the XBOOTLDR type on the same disk as Sprout's partition
is read as well, and its entries and unified kernel images are sorted
with the others. As their files are on that partition, every entry has
an entry-root value, which is the device of the partition or nothing for
Sprout's own, to use in a path like $entry-root\$chainload. It is off
by default, as existing actions would look for the files on the wrong
partition.
2026-10-03 20:23:57 -07:00
alex 50f62f5bb6 feat(bls): one entry for each profile of a unified kernel image
A unified kernel image can have several profiles, each started by a
.profile section and made of the sections after it on top of the base.
Every profile is an entry. The first has the id of the file, and the
others add @ and the identifier or the number of the profile, with a
title that names the profile and the profile number as the load options.
The profiles share the boot counter of the file, sort in the order they
are in the image, and a profile after the first is not picked as the
default entry when no default was asked for.

The PE reader keeps every section in order, so repeated names and the
sections that a profile removes can be told apart, and reads images with
more than 96 sections.
2026-10-03 20:23:57 -07:00
alex ac49899294 fix(eficore): log when the devicetree of the firmware is restored 2026-10-03 20:23:57 -07:00
alex ac4ec69c02 feat(boot): install the devicetree of a BLS entry
Add a devicetree option to the chainload action, set from the devicetree
key of a BLS entry. The file is installed as the EFI devicetree table
with the memory type that a devicetree has to be in, patched by the
fixup protocol when the firmware has one, and the table of the firmware
is put back when the image returns or fails to start. It is ignored when
Secure Boot is enabled, as it can't be verified, and the DeviceTree
loader feature is advertised.
2026-10-03 20:23:57 -07:00
alex ef14e5bd98 feat(bls): parse the devicetree key 2026-10-03 20:23:57 -07:00
alex 5b345c82d1 feat(eficore): find the extended boot loader partition on the disk of a partition
Look for a filesystem on the same disk whose GPT partition type is the
XBOOTLDR type, using the partition info protocol. open_shared moves to
the handle module so the graphical menu and the discovery share it.
2026-10-03 20:23:57 -07:00
alex d41bcaa292 feat(bls): reboot when an entry with boot counting fails to start
Add the reboot-on-error loader.conf setting. With auto, which is the
default, the machine resets after a failed start only if a boot counter
try was just used up and there were tries left, so the entry eventually
runs out of tries and the next boot picks another one. yes always resets
and no never does.
2026-10-03 20:23:57 -07:00
alex 14bcfc2653 fix(bls): write boot counters systemd can read and match systemd on small details
Do not pad the boot counter digits or cap the tries done counter, as
systemd-bless-boot reads a counter with a leading zero as octal and
systemd-boot never writes one. A profile only selects a profile when it
is a number above zero, an unterminated bracket in a pattern matches
nothing, and files that start with auto- are ignored in any case and for
unified kernel images too. The saved entry is compared ignoring case, and
entry settings are only expanded for sources that are looked at.
2026-10-03 19:41:39 -07:00
alex 8bb5f1132d fix(menu): let a key press show a hidden menu
A menu with a zero timeout or menu-hidden now waits 100 ms for a key press
before booting the default entry, which is how the boot loader interface
lets a user interrupt a hidden menu. menu-disabled still skips the wait.
2026-10-03 19:41:39 -07:00
alex 590eeead5e fix(boot): match and publish BLS entry ids like systemd-boot
Entries now have an id, the file name without the boot counter and with
its .conf or .efi extension in lower case, which is what systemd-boot
publishes in LoaderEntries and LoaderEntrySelected and what bootctl
expects. LoaderEntryDefault, LoaderEntryOneShot and loader.conf match by
id, ignoring case, with *, ? and [a-z], and no longer match titles or
numbers. A type 1 and a type 2 entry with the same name no longer clash.

@saved now works from LoaderEntryDefault as well, stops saving when it
is not asked for, and removes a stale saved entry. LoaderEntryPreferred
and the loader.conf preferred key select an entry that skips bad boot
counter entries. An entry with no tries left that is booted by hand is
counted too, and a read-only entry file is left alone.

Type 1 entries can name an architecture, which hides them on another
architecture, and a uki with an optional profile. Files that start with
auto- are ignored. loader.conf is read lossily, and a failed measurement
no longer stops the boot.
2026-10-03 19:41:39 -07:00
alex a26a5f1639 feat(eficore): advertise the sort key and preferred entry features 2026-10-03 19:41:39 -07:00
alex 6ea9fa9dbf feat(bls): parse the architecture, uki and profile keys 2026-10-03 19:41:39 -07:00
alex 9d123d6a13 feat(bls): keep boot counter digit widths and sort ties by tries 2026-10-03 19:41:39 -07:00
alex cd49b27ccb feat(bls): read the loader.conf preferred key and strip quotes 2026-10-03 19:41:39 -07:00
alex 4794de9db5 feat(parsing): match entry ids the way systemd-boot does 2026-10-03 19:41:39 -07:00
alex f4221131f1 feat(bls): boot unified kernel images (Type #2 entries) (#111)
* feat(bls): read PE sections without loading the whole image

* feat(bls): produce entries from unified kernel images

* feat(bls): boot unified kernel images as type 2 entries

Scan the EFI/Linux directory, or the new uki-path option of the BLS
generator, for .efi files and turn each one into an entry using the
metadata in its PE sections. Autoconfigure now also detects a disk that
only has unified kernel images. An image with the id of a type 1 entry is
skipped, and boot counting works on the .efi file name.

* fix(boot): rename boot counter files in place instead of copying them

* fix(bls): skip hidden files and keep unified kernel images when the type 1 scan fails

Check file name suffixes without slicing by byte offset, drop the PE
section bytes once an image has been parsed, and only fail the generator
on a type 1 error when unified kernel images are disabled.
2026-10-03 19:09:17 -07:00
alex 27d2a5c56e feat(menu): inset the graphical menu in a panel with the logo above it
Entries now sit in a centered panel instead of spanning the screen, and only
the selected entry gets a pill. The logo and name are in a header above the
panel and always show. Long titles end in an ellipsis, and a scrollbar and
position show when the list doesn't fit. Text scale is capped so 1440p
displays aren't dominated by the titles.
2026-10-03 18:05:12 -07:00
alex 1bebcd2ecc fix(menu): draw the graphical menu to every display
The first graphics output may not be a display anyone is looking at. Open
each output that has a device path, and skip the console splitter's output
unless it is the only one. The cursor is shown at the same relative place on
each display, and hovering over an entry on any of them selects it.
2026-10-03 18:05:12 -07:00
alex 795e310ae7 fix(menu): open the graphics output shared to keep the text console connected 2026-10-03 18:05:12 -07:00
alex e12e5dd7cf fix(menu): open the pointers shared so the graphical menu doesn't hang
An exclusive open makes the firmware disconnect the drivers that use the
protocol, and on some firmware that call never returns. Prefer the pointer
on the console input handle, which belongs to the console splitter.
2026-10-03 18:05:12 -07:00
alex de2df4a183 fix(eficore): don't panic when filling a rectangle past the right edge
The slice start was after its end when the rectangle began beyond the
framebuffer width, which happens when the cursor is at the right edge.
2026-10-03 18:05:12 -07:00
alex 7d913b3ab4 fix(boot): load loader.conf before one-shot variables and normalize the boot count path 2026-10-03 17:39:38 -07:00
alex 2c18f229c0 fix(boot): skip bad entries when picking the default source, don't save forced boots for @saved 2026-10-03 17:39:38 -07:00
alex eac6215a76 feat(boot): use loader.conf, @saved and the boot count path
Read \loader\loader.conf from the partition Sprout was loaded from and
use its default and timeout as the lowest priority source. Sprout now
picks the timeout in this order: LoaderConfigTimeoutOneShot,
--menu-timeout, the configuration, LoaderConfigTimeout, loader.conf. The
default entry order is the configuration, LoaderEntryDefault, loader.conf,
and a source that matches no entry falls through to the next one.

To tell an explicit menu-timeout from the built-in default, the
configuration option is now optional. Existing files are unaffected.

A loader.conf default of @saved selects the entry in
LoaderEntryLastBooted and saves the booted entry there on every boot.

After consuming a boot counter try, publish the renamed entry file in
LoaderBootCountPath and advertise the boot counting and saved entry
features.
2026-10-03 17:39:38 -07:00
alex 5c517473e3 feat(bls): parse loader.conf 2026-10-03 17:39:38 -07:00
alex 1207f1d4e9 feat(bls): boot counting (#107)
* feat(bls): parse and render boot counters

* feat(bls): sort entries with no boot tries left last

* feat(boot): strip boot counters from bls entry ids

* feat(boot): consume bls boot counter tries before booting

* fix(boot): never default to a bad boot counter entry while a good one exists
2026-10-03 17:19:27 -07:00
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