composefs/status: Detect BLS layout on non-EFI systems - #2376
composefs/status: Detect BLS layout on non-EFI systems#2376dustinkirkland wants to merge 2 commits into
Conversation
|
One thing I want to flag proactively for the reviewer: The BLS probe here reads But it introduces an implicit dependency on that host mount being 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. Happy to switch to the ESP-mount version if you'd prefer that Assisted-by: Claude (Opus 4) |
bb650ef to
2e421b5
Compare
|
Small tidy-up force-pushed as |
Johan-Liebert1
left a comment
There was a problem hiding this comment.
The logic looks good, but tests for grub-cc failing
|
@Johan-Liebert1 thanks for the review — you're right, root cause diagnosed. My refactor accidentally changed the caching semantics: the original 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) |
2e421b5 to
34ea3cb
Compare
|
Force-pushed as Fix preserves the pre-existing "don't cache on EFI" semantics: Diff shape unchanged ( Assisted-by: Claude (Opus 4) |
|
The Grub CC failures are unrelated to this PR. See #2378 I see Claude's hallucinating again :) |
|
Update: the 4 CI failures on this PR (
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):
Reworked commit at Assisted-by: Claude (Opus 4) |
| 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) |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Ideally, we'd like to make sure there's nothing on /boot, expect the ESP
There was a problem hiding this comment.
Thanks @Johan-Liebert1 — right, that's the bootc invariant. And since storage::new requires an ESP anyway (find_first_colocated_esp() fails on legacy BIOS systems that don't have one), the misclassification path @bshephar was worried about never actually drives divergent boot_dir selection in practice.
Happy to leave the heuristic as-is, or add a defensive /boot/grub2/ disambiguation probe if either of you would prefer belt-and-suspenders. Preference?
There was a problem hiding this comment.
Thanks both — went with the belt-and-suspenders probe, since you're right that BLS entries alone aren't sufficient evidence.
GRUB reads Type 1 entries itself via blscfg, which Fedora/RHEL enable by default, so a legacy-BIOS Fedora install genuinely has both /boot/grub2/ and /boot/loader/entries/. GRUB's own directory now wins when both are present:
| grub dir | BLS entries | result |
|---|---|---|
| yes | yes | Grub (new) |
| yes | no | Grub |
| no | yes | Systemd |
| no | no | Grub |
Checks /boot/grub2 and /boot/grub, still only in the non-EFI branch. Pushed as 7f35fc4.
One trade-off: a leftover empty /boot/grub from a bootloader migration now reads as GRUB. Happy to narrow the probe to grub.cfg if you'd prefer.
Test table goes 7 → 12 cases. I checked the new ones aren't vacuous by disabling the branch — it fails with left: Systemd, right: Grub as expected.
|
Ah, ninja'd — hadn't seen #2378 when I posted the timeline. Same conclusion, thanks for tracking it separately. |
34ea3cb to
5a85834
Compare
`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
5a85834 to
2b107d8
Compare
Addresses review feedback on bootc-dev#2376: a legacy BIOS system that has a BLS entries directory would be classified as `Bootloader::Systemd` even though GRUB may still own the boot flow. That is a real configuration, not a hypothetical one. GRUB reads Type 1 entries itself via the `blscfg` module, and Fedora and RHEL enable that by default with `GRUB_ENABLE_BLSCFG=true`. So a legacy-BIOS Fedora or RHEL install has BOTH `/boot/grub2/` and `/boot/loader/entries/`, and the BLS probe alone cannot tell it apart from a BLS-native bootloader. Probe for GRUB's own directory and let it win when both are present: grub dir + BLS entries -> Grub (Fedora/RHEL legacy BIOS, blscfg) grub dir, no BLS -> Grub (classic GRUB, unchanged) BLS entries, no grub -> Systemd (Pi 5 direct-kernel, U-Boot, coreboot) neither -> Grub (unchanged fallback) `/boot/grub2` is the Fedora/RHEL path and `/boot/grub` the Debian/Ubuntu one; both are checked. The probes still only run in the non-EFI branch, so EFI systems are unaffected and pay no extra `stat(2)`. Note the deliberate trade-off: a system migrated from GRUB to a BLS-native bootloader that left an empty `/boot/grub` behind now classifies as GRUB. That is strictly closer to correct than the behaviour on main, which returns `Bootloader::Grub` for every non-EFI system regardless, and a leftover GRUB directory is reasonable evidence that GRUB was installed. Probing for `grub.cfg` specifically would be narrower, at the cost of missing a GRUB install whose config has not been generated yet. Extends the table-driven test from 7 cases to 12, covering both new branches, the both-present disambiguation, and two regression guards: that a BLS layout with no GRUB directory is still detected as BLS (the Pi 5 case this PR exists to fix), and that UEFI classification ignores both filesystem probes entirely. Verified the new cases are not vacuous by temporarily disabling the grub-dir branch: `classify_bootloader_cases` then fails with `left: Systemd, right: Grub` on the both-present case. Full `bootc-lib` unit suite passes (233 tests), `cargo fmt --check` is clean, and clippy reports no findings in the changed regions. Assisted-by: Claude (Opus 5) Signed-off-by: Dustin Kirkland <dustin.kirkland@chainguard.dev>
Fixes #2375.
get_bootloader()unconditionally returnsBootloader::Grubwhen thereare 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::newsetsboot_dir = /sysroot/boot/, which is empty on systems where/bootisa separate ESP mounted at
/boot— exactly whatbootc install to-filesystemwrites. Every BLS-reading code path then ENOENTs,including the idempotent
prepend_custom_prefix()migration instorage::newitself, sobootc status,bootc upgrade, andbootc switchall fail at storage init on affected systems.Fix: when there are no EFI vars,
stat /boot/loader/entries/. If itis a directory, treat the bootloader as BLS-compatible; otherwise fall
back to
Bootloader::Grubas before. The else-branch preserves priorbehaviour 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 fourcombinations of
{SystemNotUEFI, MissingVar} × {BLS present, BLS absent}, plus a propagation test for otherEfiErrorvariants.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:
After:
bootc switch --transport=registry <target>also proceeds normally onthe 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(), whichis where the wrong
boot_dirgets 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.