Skip to content

composefs: Install systemd-boot via bootupd to keep shim in boot path - #2512

Draft
LorbusChris wants to merge 11 commits into
bootc-dev:mainfrom
LorbusChris:systemd-boot-via-bootupd
Draft

LorbusChris wants to merge 11 commits into
bootc-dev:mainfrom
LorbusChris:systemd-boot-via-bootupd

Conversation

@LorbusChris

@LorbusChris LorbusChris commented Sep 29, 2026 •

Copy link
Copy Markdown

With --bootloader systemd on the composefs backend, bootc runs a bare bootctl install. That writes systemd-boot to EFI/BOOT/BOOT<arch>.EFI, so firmware loads it directly and shim is never in the boot path, even when the image ships bootupd and shim. Such an install boots under Secure Boot only if the firmware's db trusts systemd-boot's signer, and it loses the shim that fwupd chains its UEFI capsule updates through.

This series hands systemd-boot to bootupd when the image's bootupd can install it: its --bootloader accepts systemd (bootupd 0.3.0 and newer), its update metadata lists a systemd-boot component, and the target has a single ESP. Otherwise bootc keeps the bare bootctl install and says that shim is left out. bootupd installs shim alongside whichever bootloader it is asked for, so the ESP ends up with shim + systemd-boot the same way GRUB is installed today. Images without bootupd keep the current behaviour, which remains the way to get a shim-less systemd-boot install (as discussed in #2265).

Auto-detection is unchanged: with bootupd present and nothing requested, GRUB is still selected. Only an explicit request (--bootloader systemd, or bootloader = "systemd" in the install configuration) takes the new path.

Note on Secure Boot today: Fedora signs systemd-boot 262 only with its 2025 CA, while Fedora's shim embeds the 2020 CA (GRUB is dual-signed), so with stock firmware keys shim does not yet accept Fedora's systemd-boot (rhbz#2268695). Until it does, systemd-boot's signing certificate has to be enrolled as a MOK. The docs show how.

Commits

  1. bootloader: Separate probing bootupd's options from parsing them (prep): bootupd_install_help runs bootupd's install help and returns its output, help_advertises_flag is a pure predicate over that text with unit tests. The predicate anchors on the option column so --boot no longer matches --bootloader.
  2. bootloader: Split Secure Boot key staging out of install_systemd_boot (prep): write_autoenroll_keys, no behaviour change.
  3. bootloader: Pass --bootloader to bootupd when supported: passes the bootloader's Display name when the target bootupd accepts that value, parsed from [possible values: ...] in its -h output, which lists values inline even where --help would not. Packaged bootupd 0.2.36 builds advertise the flag but accept only grub; the parser is tested against the real help of 0.2.35, 0.2.36 and 0.3.2. A bootupd that cannot be asked keeps its own choice for GRUB; asking it for anything else is an error. Callers can target bootupd's EFI component instead of --auto, and the grub-cc swap now does: --auto picks BIOS when the installing host booted in BIOS/CSM mode, and bootupd then exits 0 with nothing on the ESP. The ostree path now always asks for grub, which takes precedence over a default_bootloader in the image's metadata.
  4. install: Remove bootupd's ESP state file when cleaning the ESP (prep): since 54078f2 only EFI/ and loader/ are removed, so a bootupd-state.json left at the ESP root by a grub-cc or systemd-boot install would make bootupd refuse a to-existing-root reinstall. The multi-device ESP test now seeds that file and checks it is gone.
  5. blockdev: List an ESP shared by several disks once (prep): on firmware RAID such as Intel VROC, find_colocated_esps listed the array's one ESP once per member disk.
  6. composefs: Let bootupd install grub-cc itself when the image allows: reads bootupd's EFI update metadata (usr/lib/bootupd/updates/EFI.json, with a FIXME to replace it by a bootupd query) to learn which components the image has, because bootupctl status --json cannot tell from an unbooted image. grub-cc goes to bootupd natively only with a grub-cc component, a bootupd that accepts it, and a single ESP; otherwise the existing swap stays. The decision is a pure, table-tested function.
  7. composefs: Install systemd-boot via bootupd to keep shim in boot path: the behaviour change, extending that decision to systemd-boot. Secure Boot keys are read before bootupd runs, and staged after it with a visible warning, since the enrolled db must still trust shim's signer. Docs: the two installation paths, the Secure Boot situation above with a MOK enrollment recipe, regenerating the metadata after adding the package, bootupd owning systemd-boot afterwards (later images must keep the component), and the effect of a systemd-boot component on GRUB installs (no default bootloader for tools that pass no --bootloader). Sealed images keep requiring that the image does not ship bootupd.
  8. docs: Note bootupd images must generate update metadata: what composefs images must run bootupctl backend generate-update-metadata during build or bootloader installation will fail #2265 was actually asking.
  9. tests: Check the ESP layout of systemd-boot installs: one readonly test per layout.
  10. packaging: Let switch-to-sdboot keep bootupd and shim: BOOTC_sdboot_shim=1 keeps both and registers the distro-signed systemd-boot-<arch> with bootupd, refusing to build unless bootupd accepts --bootloader systemd and the package is laid out as a bootupd component. The default mode now also removes that package, whose .efi.signed bootctl would otherwise install instead of the test-signed binary.
  11. ci: Add a systemd-boot+shim integration leg: one composefs leg (unsealed BLS on ext4) on Fedora 45 (gating) and rawhide (allowed to fail).

Relationship to other work

Testing

  • make validate (fmt, clippy, doc, test --no-run) and the full lib unit test suite pass in the project's container build; each commit builds, passes clippy and passes the lib and blockdev unit tests on its own.
  • On the previous revision, the systemd-shim legs (Fedora 45, rawhide) passed every plan except the repart one above, including the new layout checks; the shim-less layout check passed on the existing composefs systemd-boot legs, and the grub-cc legs passed with the swap. Secure Boot stays off in those VMs, as on all unsealed legs, so they cover the install and boot path through shim but not shim's verification of systemd-boot.
  • Not covered by CI: bootupd installing grub-cc natively (no package ships a grub-cc component yet), installer hosts booted in BIOS mode, and roots with several ESPs.

Generated-by: AI

I am familiar with the bootc concepts and have prior experience building atomic OS images. I am however not familiar with this codebase and used AI to generate these code changes.

@github-actions github-actions Bot added area/install Issues related to `bootc install` area/documentation Updates to the documentation labels Sep 29, 2026
@bootc-bot
bootc-bot Bot requested a review from jeckersb September 29, 2026 01:00
@cgwalters

Copy link
Copy Markdown
Collaborator

Hi, thanks for working on this, I would agree that good systemd-boot+shim support makes a ton of sense (and I was unaware of the fwupd interactions there).

We discussed this a lot in the bootupd side, I think coreos/bootupd#1113 is one of the most relevant.

My understanding of the high level is basically it's "install systemd-boot as if it was grub" and then bootupd should just be able to handle it; is that what this is doing?

We probably need a test fixture for this (I guess a base image variant would be ideal, but barring that a canonical tested transformation) that we document in the midstream in https://docs.fedoraproject.org/en-US/bootc/ or so.

@LorbusChris
LorbusChris force-pushed the systemd-boot-via-bootupd branch 2 times, most recently from ac496de to 34778a8 Compare September 30, 2026 11:01
@LorbusChris

LorbusChris commented Sep 30, 2026 •

Copy link
Copy Markdown
Author

Hi @cgwalters 👋 🙂

Yes, exactly that, and it builds directly on coreos/bootupd#1113's --bootloader flag. It also needs coreos/bootupd#1144, so that a missing systemd-boot payload fails instead of leaving shim alone and broken on the ESP. Fedora's signed systemd-boot-x64 ships as usr/lib/efi/systemd-boot/<evr>/EFI/fedora/grubx64.efi. bootupd then installs shim + systemd-boot for --bootloader systemd. bootc's part is only to pass the --bootloader flag and otherwise take the unchanged grub path.

Proposed Fixture:

FROM quay.io/fedora/fedora-bootc:rawhide
RUN dnf -y install systemd-boot-x64 && bootupctl backend generate-update-metadata

then bootc install --composefs-backend --bootloader systemd.

There's one caveat: that image has two candidates and no default, so any bootc before this change (which never passes --bootloader), needs an explicit bootupctl backend set-default-bootloader grub.

For CI, both resulting ESP layouts are now covered:

  1. systemd-boot alone (image without bootupd, bootc runs bootctl install): EFI/BOOT/BOOTX64.EFI is systemd-boot, EFI/systemd/systemd-bootx64.efi is bootctl's copy of it, and there is no shim anywhere on the ESP. The existing composefs systemd-boot legs keep testing this.

  2. shim + systemd-boot (image with bootupd, bootc runs bootupctl backend install --bootloader systemd): EFI/BOOT/BOOTX64.EFI and EFI/fedora/shimx64.efi are shim, EFI/fedora/grubx64.efi is systemd-boot, and bootupd manages it from there (bootupctl status works). A new systemd-shim leg tests this on Fedora 45 (previous Fedora versions don't ship the signed sd-boot package), using the transformation above (plus bootupd systemd-boot from updates-testing while F45 is frozen). Secure Boot stays off on that leg, as on all unsealed legs, so it exercises the install and boot path through shim but not shim's verification of systemd-boot.

Added two tests, one per case, with both checking that the running loader is systemd-boot.

Happy to document that in forge.fedoraproject.org/atomic/docs.

@LorbusChris
LorbusChris force-pushed the systemd-boot-via-bootupd branch 3 times, most recently from 2f0864a to 6af97d3 Compare September 30, 2026 13:15
Comment thread crates/lib/src/bootc_composefs/boot.rs Outdated
}
let chroot_target = Utf8Path::from_path(mounted_root.root_path())
.ok_or_else(|| anyhow!("composefs tmpdir path is not valid UTF-8"))?;
let supported = crate::bootloader::bootupd_supports_bootloader(Some(chroot_target))?;

@Johan-Liebert1 Johan-Liebert1 Oct 1, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The issue with this, and #2327, is that we also need systemd-boot to be present in /usr/lib/efi/systemd-boot/** else bootupd is just going to fail. We need a better way of figuring out if we have systemd-boot available for bootupd or not. One way to do that would be to do bootupctl status --json and check if we have systemd-boot as an option

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, done: bootc now only hands systemd-boot to bootupd when bootupd accepts --bootloader and its update metadata lists a systemd-boot component; otherwise it stays on bootctl and says that shim is left out.

Instead of bootupctl status --json, bootc now:

  1. checks that bootupctl backend install --help advertises --bootloader;
  2. reads usr/lib/bootupd/updates/EFI.json from the image and maps component names to bootloaders the way bootupd does (grub2, grub-cc, systemd-boot);
  3. runs bootupctl backend install --bootloader systemd if the flag exists and systemd-boot is listed, and bootctl install otherwise.

bootupctl status --json doesn't work for this: bootc install runs bootupctl from the unbooted image in a chroot, and there it only returns {"components":["BIOS","EFI"]}.

Side effect: an image whose systemd-boot package isn't laid out as a bootupd component now gets a shim-less bootctl install instead of a shim-only ESP, without needing coreos/bootupd#1144.

Comment thread crates/lib/src/bootc_composefs/boot.rs Outdated
postfetch.detected_bootloader,
)?;

// FIXME: Remove this hack once we have support in bootupd

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should also remove this hack as the new version of bootupd supports grub-cc. Again, will need to check if grub-cc is in the correct path

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bootupd can install grub-cc now, but only from an image that ships it as its own component under usr/lib/efi/grub-cc/. The grub-cc package CI uses (ELN's grub2-efi-x64-cc 2.12-82) still installs it at usr/lib/efi/grub2/<evr>/EFI/fedora/cc/grubx64-cc.efi, inside the grub2 component, so dropping the hack outright would break the grub-cc leg.

So I made it conditional, in a separate commit: when the metadata lists a grub-cc component and bootupd accepts --bootloader, bootc asks bootupd for grub-cc and leaves the ESP alone; otherwise it keeps the swap. The flag check matters because bootupd 0.2.30 to 0.2.35, which CS10 and the F44 base image ship, already list components but can't be asked for one. Happy to drop the fallback once the package moves, or now if you'd rather require the component.

Comment thread crates/lib/src/bootloader.rs Outdated
/// install (distributions ship that binary under the grub2 component), so the
/// composefs path installs grub and then overwrites the binary on the ESP. See
/// the FIXME in [`crate::bootc_composefs::boot`]. `None` selects nothing.
fn bootupd_bootloader_arg(bootloader: crate::spec::Bootloader) -> Option<&'static str> {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can just use Bootloader::display()

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, the mapping helper is gone and --bootloader gets bootloader.to_string(). A unit test pins the three names bootupd accepts, so renaming one on bootc's side fails it. Asking a bootupd that's too old for the flag for anything but GRUB is now an error rather than a silent GRUB install.

@LorbusChris
LorbusChris force-pushed the systemd-boot-via-bootupd branch 2 times, most recently from 5357985 to 156b9ca Compare October 2, 2026 15:08
@cgwalters

Copy link
Copy Markdown
Collaborator

Do you guys think this would be ready for the 1.18 milestone tentatively?

@LorbusChris
LorbusChris marked this pull request as ready for review October 2, 2026 15:44
@LorbusChris

Copy link
Copy Markdown
Author

Moving to ready since this doesn't strictly require coreos/bootupd#1144

bootupd_supports_filesystem fused running `bootupctl backend install
--help` inside a chroot with the substring check on its output, so the
check could not be unit tested, and each further flag to probe for would
have meant another copy of it spawning its own process.

Split it the way systemd_version/parse_systemd_version already are in
this file: bootupd_install_help runs the command and returns its output,
help_advertises_flag is a pure predicate over that text, and
install_via_bootupd probes once up front and asks the predicate per
flag.

The predicate also anchors on the flag beginning an option line rather
than matching anywhere, so that `--boot` no longer matches
`--bootloader`, and a flag named in another option's description is not
mistaken for support for it.

Generated-by: AI
Signed-off-by: Christian Glombek <c.glombek@cosa.systems>
Prep for installing systemd-boot through bootupd: the keys from
usr/lib/bootc/install/secureboot-keys belong to systemd-boot, which
reads them from loader/keys on the ESP, not to bootctl. A later commit
needs to stage them after bootupd has installed systemd-boot as well.

Move the staging into write_autoenroll_keys, with no change in
behaviour.

Generated-by: AI
Signed-off-by: Christian Glombek <c.glombek@cosa.systems>
install_via_bootupd never told bootupd which bootloader to install,
leaving the choice to bootupd. bootupd 0.3.0 and newer take the default
bootloader recorded in the EFI update metadata, or the only candidate,
and otherwise fail with "Multiple bootloaders found as install
candidates" when only the EFI component is targeted. Versions 0.2.30 to
0.2.35, and 0.2.36 builds from the published crate, do not choose at
all: they copy every component under usr/lib/efi, so an image that also
ships, say, a systemd-boot component gets it on top of GRUB's second
stage without any error.

Pass the caller's bootloader as --bootloader when the target bootupd
accepts it. Probe the accepted values rather than the flag: the help
lists them as [possible values: ...], and packaged 0.2.36 builds, made
from a crate without build.rs, advertise the flag but accept only grub.
Probe with -h, whose short help always lists the values inline, while
clap's long help switches to a list once the values get help text of
their own, and match with whitespace collapsed, because clap wraps long
lines. The values are the names Bootloader's Display already produces
(grub, grub-cc, systemd). The parser is tested against the real help of
CentOS Stream 10's 0.2.35, Fedora's 0.2.36 and Fedora's 0.3.2, which
also pins those names. A bootupd that cannot be asked for GRUB is left
to its own choice as before; asking one for anything else is an error
rather than a silent GRUB install. Only a bootupd that also accepts
grub-cc or systemd leaves the other bootloaders' components out; one
that accepts only grub still copies them all.

Let callers target bootupd's EFI component instead of passing --auto,
and do so for grub-cc. On x86_64, --auto picks the BIOS component when
the installing host itself booted in BIOS/CSM mode, and bootupd then
installs nothing on the ESP and exits 0, so the composefs path's grub-cc
swap found no GRUB on the ESP to replace. grub-cc is still requested as
grub, because the grub-cc packages available today put the binary inside
the grub2 component, so the composefs path installs GRUB and then swaps
the binary in, as bootupd's own choice did before this change.

Note the ostree path now always asks for grub when bootupd accepts it,
also when nothing was requested and Grub was only picked because bootupd
is present. That takes precedence over a default_bootloader recorded in
the image's bootupd metadata, deliberately: bootc's own choice already
governs the boot layout it writes, and the ostree backend supports
nothing else.

Generated-by: AI
Signed-off-by: Christian Glombek <c.glombek@cosa.systems>
bootupd keeps its state in bootupd-state.json at the root of the ESP
when it installs grub-cc or systemd-boot (for GRUB it lives in /boot),
and checks every bootloader's state file before installing: an existing
one fails any later install, GRUB included, with "invalid re-install
attempted: bootupd-state.json already exists in the ESP". Since 54078f2
("install: Only remove bootloader dirs from the ESP"), cleaning the ESP
for to-existing-root keeps everything outside EFI/ and loader/, so
reinstalling over a system whose bootloader bootupd installed that way
would fail.

Remove the state file along with the bootloader directories; bootupd
writes a new one. The multi-device ESP integration test now seeds the
file before its single-disk to-existing-root install and checks that it
is gone. Prep for having bootupd install grub-cc and systemd-boot.

Generated-by: AI
Signed-off-by: Christian Glombek <c.glombek@cosa.systems>
find_colocated_esps looks for an ESP on every disk backing the device.
On a firmware RAID such as Intel VROC, the ESP is a partition of the
array, which find_partition_of_esp_optional reaches through each member
disk, so the one ESP was listed once per disk. A caller that counts the
ESPs, as the composefs install is about to, would take it for several.

List each ESP once, by device path, and test that with the existing
software and firmware RAID fixtures. find_first_colocated_esp, the only
caller so far, is unaffected.

Generated-by: AI
Signed-off-by: Christian Glombek <c.glombek@cosa.systems>
The composefs path installs grub-cc with a workaround from before
bootupd could: it asks bootupd for GRUB and then swaps in a grub-cc
binary the image stages at usr/lib/grub-cc/grub-cc.efi, as bootc's own
test images do. bootupd can install grub-cc itself since 0.3.0, but only
from an image that ships grub-cc as a bootupd component of its own,
under usr/lib/efi/grub-cc. The grub-cc packages available today
(grub2-efi-x64-cc 2.12-82 in ELN) put the binary inside the grub2
component instead, where bootupd cannot tell it apart from GRUB.

bootc cannot ask bootupd which bootloaders it could install from an
unbooted image: there, `bootupctl status --json` only names the BIOS and
EFI components. bootupd derives that from its EFI update metadata,
usr/lib/bootupd/updates/EFI.json, which names every component it found
since 0.2.30. Read that metadata in bootc with the same name mapping,
with a FIXME to replace it by a bootupd query once one exists. Ask
bootupd for grub-cc directly when the metadata lists a grub-cc component
and bootupd accepts grub-cc for --bootloader; otherwise keep the swap.
Both are needed: bootupd 0.2.30 to 0.2.35 list components but cannot be
asked for one, and packaged 0.2.36 builds accept only GRUB.

bootupd also installs on every ESP of the target, while bootc writes the
BLS entries grub-cc reads to the first ESP only, so roots with several
ESPs keep the swap. Only consider the image's bootupd when the image
itself ships bootupctl, since that is the one bootc runs. CI keeps
exercising the swap: no available package ships a grub-cc component yet,
so the native path is untested for now. The Dockerfile comment that
explains the swap in bootc's test images now gives that reason.

Generated-by: AI
Signed-off-by: Christian Glombek <c.glombek@cosa.systems>
Bootloader::Systemd fell through to install_systemd_boot(), which runs a
plain `bootctl install`. That writes systemd-boot to
EFI/BOOT/BOOT<arch>.EFI, so firmware loads it directly and shim is never
in the boot path, even when the image ships bootupd and shim. Such an
install boots under Secure Boot only if the firmware's db trusts
systemd-boot's signer, and it loses the shim that fwupd chains its UEFI
capsule updates through.

bootupd installs shim alongside whichever bootloader it is asked for,
and Fedora ships systemd-boot under the second-stage name baked into
shim (systemd-boot-x64 since 262), so routing Systemd through the path
GRUB already takes yields shim + systemd-boot with no bootc-side special
casing. Shim then lets systemd-boot boot with the firmware's stock keys,
provided shim trusts systemd-boot's signer. Fedora's shim does not yet:
Fedora signs systemd-boot only with its 2025 CA, while its shim embeds
the 2020 CA (rhbz#2268695), so for now that certificate has to be
enrolled as a MOK; the docs show how.

Take that route only when the decision from the previous commit finds
that the image's bootupd accepts --bootloader systemd and its update
metadata lists a systemd-boot component, and the target has a single
ESP; otherwise keep today's bare `bootctl install` and say that shim is
left out, rather than silently changing what lands on the ESP. The
metadata check matters because a systemd-boot package laid out where
bootupd does not look is invisible to it (Fedora shipped it that way
until https://src.fedoraproject.org/rpms/systemd-boot/pull-request/4),
and bootupd up to 0.3.2 then installs shim alone and reports success;
coreos/bootupd#1144, still open, would make that an error on bootupd's
side. Several ESPs stay with bootctl because bootupd installs on every
ESP and points the firmware at the last one, while bootc writes the
entries systemd-boot reads to the first one only.

Secure Boot key enrollment belongs to systemd-boot rather than to
whatever installed it, so the keys from
usr/lib/bootc/install/secureboot-keys are staged on this path too. They
are read before bootupd runs, so a malformed key directory still fails
the install before anything is written. Note this makes a new
combination reachable: keys staged for an install that boots via shim.
Enrolling a db without shim's signer leaves shim unverifiable, and
systemd-boot enrolls a key set named `auto` by itself in a VM in setup
mode. A db that keeps shim's signer is legitimate though, so warn
visibly rather than refuse, and say so in bootc-install(8).

Other differences from `bootctl install`: bootupd writes no
loader/random-seed, loader/entries.srel or loader/loader.conf, so UKI
installs through bootupd get the 5-second menu timeout bootc writes when
loader.conf is missing (UKI through bootupd is untested); and without
--generic-image, bootupd replaces the firmware boot entries carrying the
OS name with one for shim, where bootctl --root leaves EFI variables
alone.

Note this path is not exercised by the pre-existing CI legs:
contrib/packaging/switch-to-sdboot removes bootupd and shim from every
bootloader=systemd test image, so the bootctl path is taken. A later
commit adds a leg for it.

Generated-by: AI
Signed-off-by: Christian Glombek <c.glombek@cosa.systems>
bootc decides whether an image has bootupd by looking for the payload
that `bootupctl backend generate-update-metadata` writes to
/usr/lib/bootupd/updates, not only for the bootupctl binary. An image
that installs bootupd but skips that build step is treated as having
none: auto-detection picks systemd-boot, and an explicit --bootloader
grub cannot install GRUB either, because bootupd has no payload to
install. Depending on the bootupd version and the firmware, bootupd then
fails, or skips its components and reports success without installing a
bootloader. Neither outcome points at the missing build step, and the
bootloader docs did not mention it.

Say so where the bootupd flow is described.

Closes: bootc-dev#2265

Generated-by: AI
Signed-off-by: Christian Glombek <c.glombek@cosa.systems>
The readonly suite verified that a composefs systemd-boot install boots,
but not what ended up on the ESP, so the difference between a bare
`bootctl install` and systemd-boot installed through bootupd -- whether
shim is in the boot path -- went untested.

Add one readonly test per layout, keyed on whether the image ships
bootupd: the test images that ship it also ship a systemd-boot component
and a bootupd that can install it, so bootc takes the bootupd path there
and the bootctl path everywhere else. Without bootupd, systemd-boot
itself must be the removable-media fallback and no shim may exist on the
ESP. With it, the fallback must be shim, systemd-boot must sit under
shim's second-stage name in the vendor directory, bootctl's own copy
must be absent, and `bootupctl status` must report what bootupd
installed. Both check that the running loader really is systemd-boot.
The binaries are told apart by their embedded identifiers:
systemd-boot's LoaderInfo string and shim's SBAT entry.

Generated-by: AI
Signed-off-by: Christian Glombek <c.glombek@cosa.systems>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Shim presence is not enforced and the packaging capability probe accepts unsupported bootupd versions.

Review effort: Balanced
Findings: 1 High severity · 1 Medium severity

Open (2)
What changed in this PR

Routes eligible composefs systemd-boot and grub-cc installations through bootupd so shim remains in the EFI boot path, while preserving existing fallbacks.

Changes:

  • Adds bootupd capability/component detection and bootloader-specific installation routing.
  • Cleans bootupd ESP state and deduplicates shared ESP discovery.
  • Adds packaging, documentation, CI, and layout tests for shim-backed systemd-boot.
File Description
.github/​workflows/​ci.yml Adds systemd-boot-with-shim integration legs.
CONTRIBUTING.md Documents the new test-image variant.
Dockerfile Installs and retains shim-capable bootupd payloads.
Justfile Exposes the systemd-boot shim build option.
contrib/​packaging/​switch-to-sdboot Supports retaining bootupd and shim.
crates/​blockdev/​src/​blockdev.rs Deduplicates ESPs shared through firmware RAID.
crates/​lib/​src/​bootc_composefs/​boot.rs Selects bootupd or fallback installation methods.
crates/​lib/​src/​bootloader.rs Parses bootupd capabilities and metadata.
crates/​lib/​src/​fixtures/​bootupctl-install-help-0.2.35.txt Adds legacy help fixture.
crates/​lib/​src/​fixtures/​bootupctl-install-help-0.2.36.txt Adds GRUB-only help fixture.
crates/​lib/​src/​fixtures/​bootupctl-install-help-0.3.2.txt Adds multi-bootloader help fixture.
crates/​lib/​src/​install.rs Passes bootloader selection and removes stale ESP state.
docs/​src/​bootc-bootloaders.7.md Documents bootupd routing and Secure Boot implications.
docs/​src/​bootc-experimental-composefs.7.md Updates composefs bootloader behavior.
docs/​src/​bootc-installation.7.md Updates installation overview.
docs/​src/​man/​bootc-install.8.md Documents key enrollment with shim.
tmt/​tests/​booted/​readonly/​055-test-sdboot-shim.nu Verifies the shim-backed ESP layout.
tmt/​tests/​booted/​readonly/​056-test-sdboot-noshim.nu Verifies the bootctl fallback layout.
tmt/​tests/​booted/​tap.nu Adds EFI inspection helpers.
tmt/​tests/​booted/​test-multi-device-esp.nu Tests stale state cleanup and adjusts applicability.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread crates/lib/src/bootc_composefs/boot.rs
Comment thread contrib/packaging/switch-to-sdboot
Every bootloader=systemd test image strips bootupd and shim, so the
systemd-boot-through-bootupd path added earlier in this series has no
image to run on.

Add an SDBOOT_SHIM mode to switch-to-sdboot that keeps both packages and
registers the distribution's signed systemd-boot with bootupd instead of
installing our test-signed binary, since the distribution's binary is
what an image would put behind shim. The package has to ship the binary
laid out as a bootupd component under shim's second-stage name, which
Fedora's systemd-boot-<arch> does since 262. The mode refuses to proceed
otherwise, and refuses a bootupd that does not accept --bootloader
systemd, rather than letting the image silently end up with shim alone
or on the bootctl path; it probes the value because packaged 0.2.36
builds advertise the option but accept only grub.

Thread it through the Justfile and Dockerfile as BOOTC_sdboot_shim. The
fetch stage installs the systemd-boot package for the build's
architecture and upgrades bootupd when it is set, also from
updates-testing where that repository exists, because a stable release
may carry the new layout only there at first (Fedora 45's main
repository still has systemd-boot 261 with the old layout).

In the default mode, also remove the distribution's signed
systemd-boot-<arch> package when present, because bootctl would install
its .efi.signed instead of our test-signed binary, and correct the
mode's stated reason for removing bootupd, which the
systemd-boot-through-bootupd change made stale.

Generated-by: AI
Signed-off-by: Christian Glombek <c.glombek@cosa.systems>
No CI leg installs systemd-boot through bootupd: every
bootloader=systemd test image drops bootupd and shim, so the bootctl
path is all that runs.

Add one composefs leg, unsealed BLS on ext4, whose image keeps bootupd
and shim and registers the distribution's signed systemd-boot with
bootupd. Its matrix value, systemd-shim, is the only one that is not a
bootc bootloader: the job maps it to BOOTC_bootloader=systemd, which the
image's install config hands to bootc, plus BOOTC_sdboot_shim=1.

Fedora 45 and rawhide are the only tested OSes that ship a signed
systemd-boot laid out as a bootupd component, so both join the
integration OS list for this leg alone: everything else stays excluded
there, the leg stays excluded everywhere else, and test-baseconfigs,
which reads the same OS list, excludes them too. Rawhide is allowed to
fail, as it already is in the package job; Fedora 45 is the leg that
gates.

Generated-by: AI
Signed-off-by: Christian Glombek <c.glombek@cosa.systems>
@LorbusChris
LorbusChris force-pushed the systemd-boot-via-bootupd branch from 30b9615 to d7a899d Compare October 2, 2026 21:20
@LorbusChris

Copy link
Copy Markdown
Author

Moving back to draft: After further investigation, this has grown quite a bit. Happy to split preparatory fixes out into their own PRs if that helps. And if we want this to work upstream first, we now need #2532 for systemd v262 support.

@LorbusChris
LorbusChris marked this pull request as draft October 2, 2026 21:49
@cgwalters

Copy link
Copy Markdown
Collaborator

cc @Johan-Liebert1 how critical is the repart stuff? OK to make it opt-in for next Monday's release?

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/documentation Updates to the documentation area/install Issues related to `bootc install`

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants