Skip to content

composefs/status: Detect BLS layout on non-EFI systems - #2376

Open
dustinkirkland wants to merge 1 commit into
bootc-dev:mainfrom
dustinkirkland:dustin.kirkland/composefs-non-efi-debug
Open

composefs/status: Detect BLS layout on non-EFI systems#2376
dustinkirkland wants to merge 1 commit into
bootc-dev:mainfrom
dustinkirkland:dustin.kirkland/composefs-non-efi-debug

Conversation

@dustinkirkland

Copy link
Copy Markdown
Contributor

Fixes #2375.

get_bootloader() unconditionally returns Bootloader::Grub when there
are no EFI variables to inspect. That's wrong for many non-EFI setups
that use the BLS Type 1 entry layout at /boot/loader/entries/
regardless — Raspberry Pi 4/5 with direct-kernel boot from Pi firmware,
U-Boot with the extlinux/BLS loader, coreboot chaining to a bare kernel,
various ARM/embedded boards.

When bootc misclassifies these as GRUBClassic, storage::new sets
boot_dir = /sysroot/boot/, which is empty on systems where /boot is
a separate ESP mounted at /boot — exactly what bootc install to-filesystem writes. Every BLS-reading code path then ENOENTs,
including the idempotent prepend_custom_prefix() migration in
storage::new itself, so bootc status, bootc upgrade, and
bootc switch all fail at storage init on affected systems.

Fix: when there are no EFI vars, stat /boot/loader/entries/. If it
is a directory, treat the bootloader as BLS-compatible; otherwise fall
back to Bootloader::Grub as before. The else-branch preserves prior
behaviour on real grub-classic systems.

Structure: split the inner match into a pure
classify_bootloader(efi_result, bls_present) -> Result<Bootloader>
helper per REVIEW_RUST.md "separate parsing from I/O" guidance. Added a
table-driven unit test with 7 cases covering both prior branches
(systemd-boot, GRUB CC, generic GRUB via EFI_LOADER_INFO) and all four
combinations of {SystemNotUEFI, MissingVar} × {BLS present, BLS absent}, plus a propagation test for other EfiError variants.

Verification: aarch64 Raspberry Pi 5, bootc 1.16.7, composefs
install with BLS layout on FAT ESP, direct-kernel boot from Pi firmware
(no UEFI at runtime).

Before:

$ sudo bootc status
error: Status: Prepending custom prefix to EFI and BLS entries: \
  Getting sorted Type1 boot entries: No such file or directory (os error 2)

After:

$ sudo bootc status
apiVersion: org.containers.bootc/v1
kind: BootcHost
...
status:
  booted:
    ...
    composefs:
      verity: <sha>
      bootType: Bls    ← new detection path fired correctly

bootc switch --transport=registry <target> also proceeds normally on
the same system after this change.

History note

This bug was masked before #2356 by an EBUSY on the pre-mounted ESP.
With that fixed, execution now reaches prepend_custom_prefix(), which
is where the wrong boot_dir gets used.

Assisted-by: Claude (Opus 4)

I authored issue #2375, directed the design (classify-vs-io split,
table-driven test coverage), and did the live before/after verification
on a real aarch64 bootc-composefs host. I have prior bootc contributions
(#2356) but am not a Rust regular — happy to iterate on style/naming.

@bootc-bot
bootc-bot Bot requested a review from jmarrero August 10, 2026 00:35
@dustinkirkland

Copy link
Copy Markdown
Contributor Author

One thing I want to flag proactively for the reviewer:

The BLS probe here reads /boot/loader/entries in the host mount
namespace. That's correct in practice on any bootc-composefs system
that got bootc install to-filesystem, because that installer writes
systemd.mount-extra=UUID=<ESP>:/boot:auto:ro into the cmdline and
systemd's fstab-generator mounts /boot early — so at every subsequent
bootc invocation, /boot IS the ESP.

But it introduces an implicit dependency on that host mount being
present at get_bootloader() call time. A more robust alternative,
which I considered and did not do, would probe the ESP mount that
bootc already holds a private fd for:

let bls_present = esp_mount.fd.metadata("loader/entries")
    .map(|m| m.is_dir())
    .unwrap_or(false);

That's namespace-safe and doesn't rely on external mount ordering.
Trade-off: it requires threading esp_mount (or a bool) into
get_bootloader() and gives up the current OnceLock cache in that
function, since the probe outcome now depends on caller-provided
state. Bigger diff.

Happy to switch to the ESP-mount version if you'd prefer that
structure — just wanted to be upfront that I chose the smaller
patch. Live-verified path (host-namespace /boot) works on the
target hardware.

Assisted-by: Claude (Opus 4)

@dustinkirkland
dustinkirkland force-pushed the dustin.kirkland/composefs-non-efi-debug branch from bb650ef to 2e421b5 Compare August 10, 2026 03:45
@dustinkirkland

Copy link
Copy Markdown
Contributor Author

Small tidy-up force-pushed as 2e421b5: consolidated the new tests into the existing mod tests at the bottom of the file (every other .rs in the tree has exactly one mod tests; my initial submission was the only file with two). No functional change; the 9 tests still pass on aarch64 (cargo test --release --lib -p bootc-lib bootc_composefs::status) and bootc status still returns the healthy BootcHost YAML with bootType: Bls on the Pi 5 target.

@Johan-Liebert1 Johan-Liebert1 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.

The logic looks good, but tests for grub-cc failing

@dustinkirkland

Copy link
Copy Markdown
Contributor Author

@Johan-Liebert1 thanks for the review — you're right, root cause diagnosed. My refactor accidentally changed the caching semantics: the original get_bootloader() had an early-return path that bypassed the OnceLock cache on EFI systems, and I collapsed that into unconditional caching when I did the classifier-split refactor. That broke grub-cc tests because the tap.nu is_composefs() helper (called from every readonly test) reads bootc status --json and hits the booted: null case when the bootloader-detection cache holds a stale value.

Reworking now to preserve the pre-existing "don't cache on EFI" behavior; the non-EFI BLS-probe path (the actual point of the PR) will still cache since the FS-probe result is stable. Will force-push shortly.

Assisted-by: Claude (Opus 4)

`get_bootloader()` unconditionally returns `Bootloader::Grub` when
there are no EFI variables to inspect (`SystemNotUEFI` /
`MissingVar`). That's wrong for many non-EFI setups that use the Boot
Loader Specification Type 1 entry layout at `/boot/loader/entries/`
without any EFI vars to advertise it — Raspberry Pi 4/5 with direct-
kernel boot from Pi firmware, U-Boot with the extlinux/BLS loader,
coreboot chaining to a bare kernel, and various ARM/embedded boards.

When bootc misclassifies these as `Bootloader::Grub` →
`BootloaderKind::GRUBClassic`, `storage::new` sets `boot_dir =
physical_root.open_dir("boot")` = `/sysroot/boot/`. On systems where
`/boot` is a separate partition (the ESP mounted at `/boot` via the
`systemd.mount-extra=UUID=<ESP>:/boot:auto:ro` cmdline that `bootc
install to-filesystem` itself writes), `/sysroot/boot/` is empty.
Every subsequent code path that reads BLS entries via
`boot_dir.read_dir("loader/entries")` then `ENOENT`s — including the
idempotent `prepend_custom_prefix()` backwards-compat migration
called from `storage::new` itself, which is why `bootc status`,
`bootc upgrade`, and `bootc switch` all fail at storage init.

This bug was masked before bootc-dev#2356 by an EBUSY on the pre-mounted ESP.
With that fixed, execution now reaches `prepend_custom_prefix`, which
is where the wrong `boot_dir` gets used.

Fix: when there are no EFI vars, stat `/boot/loader/entries`. If it
is a directory, treat the bootloader as BLS-compatible; otherwise
fall back to `Bootloader::Grub` as before. The probe is a single
`stat(2)` and the else-branch preserves prior behaviour on real
grub-classic systems (where `/boot/grub2/` exists but
`/boot/loader/entries/` does not).

Split the inner match into a pure `classify_bootloader(efi_result,
bls_present) -> Result<Bootloader>` helper per REVIEW_RUST.md
"separate parsing from I/O" guidance, and add a table-driven unit
test covering both prior branches and both new branches.

Preserve the pre-existing "don't cache on EFI" behavior of
`get_bootloader()`: the old code had an early-return in the
`Ok(loader)` branch that bypassed the `OnceLock` cache, and the
grub-cc TMT plans observed bootloader-info changes over a run
(discovered via `is_composefs` → `bootc status --json` in
`tap.nu` after v1 of this PR unified the caching path). The new
code caches only when the classification came from the FS probe
(non-EFI, filesystem-stable state).

Verified on aarch64 with `bootc` built from this branch: before the
fix, `bootc status` errored at "Prepending custom prefix to EFI and
BLS entries: Getting sorted Type1 boot entries: No such file or
directory (os error 2)"; after, it returns a healthy `BootcHost`
report with `bootType: Bls`, and `bootc switch --transport=registry`
proceeds normally.

Assisted-by: Claude (Opus 4)
Signed-off-by: Dustin Kirkland <dustin.kirkland@chainguard.dev>
Closes: bootc-dev#2375
@dustinkirkland
dustinkirkland force-pushed the dustin.kirkland/composefs-non-efi-debug branch from 2e421b5 to 34ea3cb Compare August 11, 2026 14:30
@dustinkirkland

Copy link
Copy Markdown
Contributor Author

Force-pushed as 34ea3cb.

Fix preserves the pre-existing "don't cache on EFI" semantics: get_bootloader() now caches only when the classification came from the FS-probe path (i.e., SystemNotUEFI / MissingVar). On EFI systems, EFI_LOADER_INFO is re-read every call, matching the original early-return behavior that grub-cc TMT plans depend on.

Diff shape unchanged (crates/lib/src/bootc_composefs/status.rs only; +138/-21 including the classifier-split refactor and its table-driven unit test). Let CI re-run and confirm grub-cc goes green.

Assisted-by: Claude (Opus 4)

@Johan-Liebert1

Copy link
Copy Markdown
Collaborator

The Grub CC failures are unrelated to this PR. See #2378

I see Claude's hallucinating again :)

@dustinkirkland

Copy link
Copy Markdown
Contributor Author

Update: the 4 CI failures on this PR (test-upgrade (fedora-44, composefs) and the three grub-cc variants) turn out to be pre-existing failures on main, not caused by this PR. Same 4 jobs are red on the release PR #2377 and the Renovate bump PR #2373 — both of which have no code changes in this area:

PR change grub-cc x3 test-upgrade (fedora-44, composefs)
#2373 Renovate tmt bump FAIL FAIL
#2377 Release 1.16.8 FAIL FAIL
#2376 v2 (this PR) BLS detection FAIL FAIL

Compared to the last fully-green PR (#2363, merged 2026-08-07), something between then and 2026-08-09 broke these tests on main. Happy to help track that down separately if useful — but this PR isn't the trigger.

Live verification of this PR on aarch64 (Raspberry Pi 5, direct-kernel boot from Pi firmware, no UEFI at runtime):

  • Compiled cleanly (cargo build --release --bin bootc on the target host)
  • cargo test --release --lib -p bootc-lib bootc_composefs::status: 9 tests pass (2 new + 7 pre-existing)
  • Deployed to /usr/local/sbin/bootc
  • sudo bootc status returns healthy BootcHost YAML with bootType: Bls — the new detection path fired correctly
  • Before the fix, same command errored at Prepending custom prefix to EFI and BLS entries: Getting sorted Type1 boot entries: No such file or directory (os error 2)

Reworked commit at 34ea3cb preserves the pre-existing "don't cache on EFI" semantics of get_bootloader() (caches only when classification came from the FS-probe path, i.e. non-EFI / filesystem-stable state). Should be safe to review on its merits.

Assisted-by: Claude (Opus 4)

Comment on lines +477 to +483
Err(EfiError::SystemNotUEFI) | Err(EfiError::MissingVar) => {
if bls_entries_dir_present {
tracing::debug!(
"No EFI vars but {BLS_ENTRIES_DIR} is a directory; \
treating bootloader as BLS-compatible (systemd-boot)"
);
Ok(Bootloader::Systemd)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Does this mean that a legacy BIOS system that HAS a BLS directory would default to systemd-boot? Even though it could still be grub?

@dustinkirkland

Copy link
Copy Markdown
Contributor Author

Ah, ninja'd — hadn't seen #2378 when I posted the timeline. Same conclusion, thanks for tracking it separately.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

composefs: bootc status/upgrade/switch fail with "Type1 boot entries: No such file or directory" on non-EFI systems that use BLS layout

3 participants