Skip to content

composefs: Use an XBOOTLDR partition with systemd-boot - #2573

Open
krissedout wants to merge 3 commits into
bootc-dev:mainfrom
krissedout:composefs-xbootldr
Open

krissedout wants to merge 3 commits into
bootc-dev:mainfrom
krissedout:composefs-xbootldr

Conversation

@krissedout

Copy link
Copy Markdown
Contributor

The Boot Loader Specification lets systemd-boot read Type #1 entries and UKIs from an Extended Boot Loader Partition (XBOOTLDR) on the ESP's disk. The composefs backend always writes them to the ESP, which doesn't work when the ESP is small. That's the usual case when installing next to Windows: its ESP is often 100 MB, a UKI with a full initramfs can be bigger than that, and composefs keeps the booted, staged and rollback ones side by side.

With systemd-boot, bootc now looks for a VFAT XBOOTLDR partition next to the ESP and, if there is one, puts the UKIs, BLS entries and their kernels and initramfs images there: on install, on upgrade, and through BootedStorage for rollback, finalize and GC. When booted, it uses the XBOOTLDR partition next to the ESP named by LoaderDevicePartUUID. The boot loader and loader.conf, which systemd-boot only reads from the ESP, stay on the ESP. Systems without an XBOOTLDR partition behave as before. GRUB keeps its own /boot handling, and grub-cc is left alone since I couldn't confirm it reads XBOOTLDR. Only VFAT partitions are used, so an ext4 /boot typed as XBOOTLDR for GRUB keeps working as it does now.

We use this in Luft, a bootc-based desktop that boots sealed UKIs through shim and systemd-boot, where we've carried it as a patch on 1.16.13. There it's been tested with Secure Boot on: installing with bootc install to-filesystem next to a Windows-style 100 MB ESP with a 2 GB XBOOTLDR partition, booting, upgrading and rolling back. The ESP only gets the boot loader, and Windows' files on it are untouched.

On this branch, just validate and just unit-tests pass, including a table-driven test for the XBOOTLDR lookup. The new tmt test, test-61-composefs-xbootldr, shrinks the VM's ESP to 100 MiB, puts a VFAT XBOOTLDR partition in the space that frees, moves bootc's entries there, then switches, upgrades and rolls back. It checks that the entries and binaries are on the XBOOTLDR partition, that GC keeps exactly what the entries refer to, and that bootc writes nothing to the ESP. It passes with systemd-boot for both BLS entries and unsealed UKIs on centos-bootc stream10, and skips itself with GRUB. My libvirt has no storage driver, so rather than through just test-tmt I ran the same script stage by stage in a qemu VM installed with bcvk to-disk.

Out of scope: bootc install to-disk doesn't create an XBOOTLDR partition, so this is for install to-filesystem setups where the installer lays out the disk. bootctl install still only gets the ESP, without --boot-path, since bootc writes the entries itself.

Assisted-by: AI
I reviewed and tested the changes myself.

@github-actions github-actions Bot added the area/documentation Updates to the documentation label Oct 15, 2026
@bootc-bot
bootc-bot Bot requested a review from jeckersb October 15, 2026 22:43
@cgwalters

Copy link
Copy Markdown
Collaborator

Luft

Would be good to link to this in our ADOPTERS.md or so?

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.

🟡 Changes recommended

Runtime selection can break existing ESP-based installations, and the new test generates unsigned UKIs for Secure Boot runs.

2 open findings
What changed in this PR

Adds composefs support for storing systemd-boot entries and boot images on a VFAT XBOOTLDR partition, reducing space requirements on the ESP.

Changes:

  • Detects XBOOTLDR and uses it during installation and runtime operations.
  • Keeps bootloader installation on the ESP.
  • Documents support and adds lookup and lifecycle tests.
File Description
tmt/​tests/​tests.fmf Registers the XBOOTLDR test.
tmt/​tests/​booted/​test-composefs-xbootldr.nu Tests partition layout, upgrades and rollback.
tmt/​plans/​integration.fmf Adds the composefs test plan.
docs/​src/​bootc-composefs.7.md Updates XBOOTLDR limitations.
docs/​src/​bootc-bootloaders.7.md Documents XBOOTLDR support.
crates/​lib/​src/​store/​mod.rs Selects the runtime boot partition.
crates/​lib/​src/​bootloader.rs Clarifies bootctl behavior in a comment.
crates/​lib/​src/​bootc_composefs/​boot.rs Routes entries and images to XBOOTLDR.
crates/​blockdev/​src/​blockdev.rs Adds partition discovery and unit tests.

🧠 Review effort: Balanced


💡 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/store/mod.rs Outdated
Comment on lines +604 to +605
let boot_partition =
BootPartition::for_esp(find_booted_from_esp(&physical_root)?, bootloader)?;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Yes this is true...I think our logic here must indeed handle the case where detect bootloader entries on the ESP but not XBOOTLDR and prefer that.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed. On a booted system bootc now only uses the XBOOTLDR partition if it actually holds an entry for the booted deployment (checking both entries and entries.staged), and falls back to the ESP otherwise. So installs from older bootc, or systems that got an XBOOTLDR partition added later, keep using the ESP. The selected partition is kept in Storage, so upgrade writes go through the same choice instead of redoing the lookup. I added unit tests for the detection. The tmt test now starts with an empty XBOOTLDR partition next to an ESP that still holds everything, checks that status works and that a switch stays on the ESP, and only then moves the files over and tests upgrade and rollback on XBOOTLDR.

Comment thread tmt/tests/booted/test-composefs-xbootldr.nu
cgwalters
cgwalters previously approved these changes Oct 16, 2026

@cgwalters cgwalters left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks so much for doing this! Offhand looks sane.

Comment thread crates/blockdev/src/blockdev.rs Outdated
pub const BIOS_BOOT: &str = "21686148-6449-6e6f-744e-656564454649";

/// Extended Boot Loader Partition (XBOOTLDR) type GUID for GPT
pub const XBOOTLDR: &str = "bc13c2ff-59e6-4262-a352-b275fd6f7172";

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Don't we have this in our DPS module? But that's also true of the ESP one above...

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catch. The DPS module was in bootc-lib, which blockdev can't depend on, so blockdev had its own copies. I moved the module into the blockdev crate as bootc_blockdev::dps in a separate first commit and dropped the duplicate ESP and BIOS boot constants there too, so XBOOTLDR now comes from the same place.

Comment thread crates/lib/src/bootc_composefs/boot.rs Outdated
/// with systemd-boot and there is one, and the ESP otherwise.
pub(crate) fn for_esp(esp: String, bootloader: Bootloader) -> Result<Self> {
if bootloader != Bootloader::Systemd {
return Ok(Self::Esp(esp));

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Hmm this is subtle...I think what we really want here is a "BLS conformant" path, which ideally grub-cc becomes too

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yeah, that makes sense. I replaced the systemd check with Bootloader::is_bls_conformant(), meaning the bootloader reads entries from XBOOTLDR if there is one and the ESP otherwise. Right now it's only true for systemd-boot since I couldn't confirm grub-cc reads XBOOTLDR, but flipping it on for grub-cc later is a one-line change. The selection logic itself moved to its own module, boot_partition.rs.

Comment thread crates/lib/src/bootc_composefs/boot.rs Outdated
// TODO: support XBOOTLDR. Per BLS, the ESP should be mounted at /efi
// when a separate XBOOTLDR partition is present at /boot. bootc does
// not yet detect or use XBOOTLDR in the composefs install path, so
// TODO: Per BLS, the ESP should be mounted at /efi when a separate

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Are you creating /efi as a dir in your base image?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

No, our image doesn't have /efi, and neither does fedora-bootc:45 as far as I can see. bootc mounts the ESP or XBOOTLDR partition itself when it needs them, and at install time our installer doesn't mount /boot or /efi into the target. This mount is only used for bootctl install, which gets the ESP path explicitly with --esp-path, and the entries are written separately, so mounting the ESP on /boot there is fine. I replaced the TODO with a comment saying that, since bootc can't create /efi in a read-only image root anyway.

krissedout added a commit to Lantharos/Luft that referenced this pull request Oct 16, 2026
Backport the revised bootc-dev/bootc#2573: only systemd-boot uses an
XBOOTLDR partition, and a running system keeps using the partition that
holds the entry it started from, so a Luft on a shared EFI system
partition keeps working when another system adds an XBOOTLDR partition.
@cgwalters-bot

cgwalters-bot commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

Triage of cgwalters's comment: claimed; tracked in cgwalters-forge/tracker#702.

Generated-by: https://github.com/cgwalters/#llms

@krissedout

Copy link
Copy Markdown
Contributor Author

Sure, happy to, opened #2576 for it so it doesn't hold this one up.

The partition type GUIDs from the Discoverable Partitions Specification
lived in bootc-lib, while bootc-internal-blockdev kept its own copies of
the ESP and BIOS boot ones because it can't depend on bootc-lib. Move
the module into the blockdev crate as `dps` and use it from both, so
there's one copy of each GUID.

Assisted-by: AI
Signed-off-by: Kristof Imeri <kristof@lanth.me>
Prep for writing composefs boot entries to an Extended Boot Loader
Partition next to the ESP.

Only VFAT partitions count, since the firmware has to read what's on
them; an ext4 /boot that's typed as XBOOTLDR for GRUB is left alone.

Assisted-by: AI
Signed-off-by: Kristof Imeri <kristof@lanth.me>
The Boot Loader Specification lets systemd-boot read Type bootc-dev#1 entries
and UKIs from an Extended Boot Loader Partition (XBOOTLDR) on the
ESP's disk. The composefs backend always wrote them to the ESP, which
doesn't work when the ESP is small. That's the usual case next to
Windows: its ESP is often 100 MB, a UKI with a full initramfs can be
bigger than that, and composefs keeps the booted, staged and rollback
ones side by side.

For bootloaders that read entries the way BLS describes, which for
now is only systemd-boot, install the UKIs, BLS entries and their
kernels and initramfs images to a VFAT XBOOTLDR partition next to the
ESP when there is one. The bootloader and loader.conf, which
systemd-boot only reads from the ESP, stay there.

When booted, BootedStorage, which upgrades, rollback, finalize and GC
go through, uses the XBOOTLDR partition next to the ESP named by
LoaderDevicePartUUID only if it holds the booted deployment's entries,
and the ESP otherwise. That way systems installed by an older bootc,
or before an XBOOTLDR partition was added, keep their entries on the
ESP.

GRUB keeps its own /boot handling, and grub-cc is left alone since
it's unclear whether it reads XBOOTLDR.

The new tmt test splits the VM's ESP in half with an empty XBOOTLDR
partition, and checks that a switch keeps the entries on the ESP. It
then moves them to the XBOOTLDR partition, as an install there would
have put them, and upgrades and rolls back. Sealed runs skip it, since
the derived images are built in the VM without keys to sign them.

Assisted-by: AI
Signed-off-by: Kristof Imeri <kristof@lanth.me>

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