mount, store: Handle pre-mounted ESP in status and upgrade paths - #2356
Conversation
How would that be correct?
No, I think we need to detect the case when the ESP is We should handle This PR has all the hallmarks of LLM generation but is missing an |
|
Also forgot to mention - thanks so much for the patch! This is definitely a bootc bug where we're just not internally consistent on the |
e924462 to
ca03307
Compare
|
Thanks Colin — right on both counts, and apologies for the missing
Re: tooling — Claude Opus 4.7 via Claude Code. I authored issue #2355, |
ca03307 to
febce43
Compare
|
I dug in with my agents (and my eyeballs), came up with this, WDYT? Agree and OK to add that to this PR? |
`bootc install to-filesystem` writes
`systemd.mount-extra=UUID=<ESP>:/boot:auto:ro` into the deployment
cmdline; systemd's fstab-generator then mounts /boot ro at boot. Every
subsequent `bootc status` failed with
error: Status: Mounting /dev/vda1: Device or resource busy (os error 16)
and any code path that writes the ESP (BLS install, UKI install, gc)
would hit the same EBUSY on a pre-mounted host.
Split the ESP-mount primitive into two intent-typed helpers, and thread
the choice through BootedStorage so callers pick their access mode
explicitly:
- `mount_esp_readonly` for `bootc status` — reuses an existing mount
as-is via `open_tree(OPEN_TREE_CLONE)`, safe because we're not
writing.
- `mount_esp_writable` for install/upgrade/gc — must produce a mount
that supports writes even when the ESP is pre-mounted ro. Its
implementation is refined in a follow-up commit; the primary
purpose of this commit is establishing the typed API surface.
`BootedStorage::new` grows an `EspAccess` argument; `status::get_host`
passes `ReadOnly`, `cli::get_storage` passes `ReadWrite`. The raw
`mount_esp` primitive is now module-private and only reachable through
the typed wrappers.
Also adds `bootc_mount::find_mount_target_by_source(dev)`, a walk over
the mountinfo tree that returns the existing target path for a source
device (or None), plus a table-driven unit test covering top-level,
nested, absent, empty, and deeply-nested mountinfo shapes.
Verified live on QEMU x86_64 (systemd 261.2, linux-qemu-6.18-bootc,
/boot mounted ro via systemd.mount-extra). Before: `bootc status`
fails with EBUSY. After: full YAML output and JSON output both work.
Assisted-by: Claude (Opus 4.7)
Fixes: bootc-dev#2355
Signed-off-by: Dustin Kirkland <[email protected]>
febce43 to
aa53610
Compare
|
Nice catch on the Applied your patch as-is via Thanks for the review. |
|
Ah heh I didn't add a signoff on my patch, but you can do so, otherwise looks good and thank you so much again for fixing this! |
`open_tree(OPEN_TREE_CLONE)` creates a mount that shares the original's underlying superblock, and a bind remount only clears the clone's own `MNT_READONLY` flag, never the shared superblock's `SB_RDONLY` (`do_reconfigure_mnt()` in `fs/namespace.c` says as much). So the existing `mount_esp_writable` silently produced a mount that still failed writes with `EROFS`. Verified with a loopback vfat image. Fix it to remount the *existing* mount read-write in place instead of a private clone, the same handling `open_dir_remount_rw` already gives `/sysroot`. This needs `find_mount_target_by_source` to look up mounts in the caller's own mount namespace rather than pid 1's, since bootc already runs unshared by the time these functions are called. Apply the same fix to `mount_esp_at()`, used during install. Use `rustix::mount::mount_remount()` directly instead of shelling out to `mount(8)`, matching the rest of this module. Assisted-by: AI Signed-off-by: Dustin Kirkland <[email protected]>
|
Cool, signoff added, thanks for the reviews and taking the fix! |
aa53610 to
27255fc
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.
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: AI
Closes: #<TBD>
`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.
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 <[email protected]>
Closes: bootc-dev#2375
`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.
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 <[email protected]>
Closes: bootc-dev#2375
`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 <[email protected]>
Closes: bootc-dev#2375
`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 <[email protected]>
Closes: bootc-dev#2375
`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 <[email protected]>
Closes: bootc-dev#2375
`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 <[email protected]>
Closes: bootc-dev#2375
`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 <[email protected]>
Closes: bootc-dev#2375
`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 <[email protected]>
Closes: bootc-dev#2375
`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 #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 <[email protected]>
Closes: #2375
Fixes #2355.
bootc install to-filesystemwritessystemd.mount-extra=UUID=<ESP>:/boot:auto:rointo the deploymentcmdline; systemd's fstab-generator then mounts /boot ro at boot. Every
subsequent
bootc statusfailed withand any code path that writes the ESP (BLS install, UKI install, gc)
would hit the same EBUSY on a pre-mounted host.
Two commits:
Split the ESP-mount primitive into two intent-typed helpers and
thread the choice through
BootedStorage.mount_esp_readonlyforbootc status(reuses an existing mount as-is viaopen_tree(OPEN_TREE_CLONE));mount_esp_writableforinstall/upgrade/gc.
BootedStorage::newgrows anEspAccessargument;
status::get_hostpassesReadOnly,cli::get_storagepasses
ReadWrite. The rawmount_espprimitive is now module-private, reachable only via the typed wrappers. Also adds
find_mount_target_by_source(dev)with a table-driven unit test.Fix
mount_esp_writableto actually grant write access (from@cgwalters). A
MS_BIND | MS_REMOUNTon anOPEN_TREE_CLONEonlyclears the clone's
MNT_READONLY, never the shared superblock'sSB_RDONLY— so the first version would have silently produced amount that failed writes with
EROFS. Replace with a superblock-level remount of the existing mount in place, matching the
pattern
open_dir_remount_rwalready uses for/sysroot. Alsoapplies the same fix to
mount_esp_at(install path).Verified live on QEMU x86_64 (systemd 261.2, linux-qemu-6.18-bootc,
/boot mounted ro via systemd.mount-extra):
bootc statusfails with EBUSY.bootc statusreturns full YAML and JSON.test —
touch /boot/xsucceeds after remount, host/bootstaysread-only.
Trailers on both commits document AI assistance per bootc's
AGENTS.md. See #issuecomment-5138400886 and the reply threadfor context on tooling and human review.