-
Notifications
You must be signed in to change notification settings - Fork 57
update: Skip bootloader update when no block devices back the root #1163
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| #!/bin/bash | ||
| # Smoke test for bcvk ephemeral (virtiofs direct-boot) environments. | ||
| # This runs *inside* the ephemeral VM and verifies that bootupd | ||
| # handles the diskless virtiofs root gracefully. | ||
| set -xeuo pipefail | ||
|
|
||
| # Verify we're actually on virtiofs — this test is meaningless otherwise. | ||
| root_fstype=$(findmnt -n -o FSTYPE /) | ||
| if [ "$root_fstype" != "virtiofs" ]; then | ||
| echo "ERROR: expected root fstype 'virtiofs', got '${root_fstype}'" >&2 | ||
| exit 1 | ||
| fi | ||
| echo "ok: root filesystem is virtiofs" | ||
|
|
||
| # The bootloader-update.service should have already run at boot (it's | ||
| # enabled by preset on Fedora). Verify it succeeded rather than failed. | ||
| systemctl is-active bootloader-update.service | ||
| echo "ok: bootloader-update.service is active (ran successfully at boot)" | ||
|
|
||
| # Also verify a manual invocation skips cleanly. | ||
| output=$(bootupctl update 2>&1) | ||
| echo "$output" | ||
| if ! echo "$output" | grep -qi 'skipping'; then | ||
| echo "ERROR: expected skip message in output" >&2 | ||
| exit 1 | ||
| fi | ||
| echo "ok: bootupctl update skipped cleanly on virtiofs" | ||
|
|
||
| echo "All ephemeral smoke tests passed." |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -577,17 +577,65 @@ pub(crate) fn adopt_and_update( | |
| /// Get the block device backing the current root by trying `/boot` first, | ||
| /// then falling back to `/sysroot`. This avoids issues with virtual | ||
| /// filesystems like composefs that are mounted on `/`. | ||
| #[context("Finding block device from boot or sysroot")] | ||
| pub(crate) fn list_dev_current_root() -> Result<Device> { | ||
| let auth = cap_std::ambient_authority(); | ||
| for path in ["/boot", "/sysroot"] { | ||
| if let Ok(dir) = Dir::open_ambient_dir(path, auth) { | ||
| if let Ok(dev) = bootc_internal_blockdev::list_dev_by_dir(&dir) { | ||
| return Ok(dev); | ||
| } | ||
| /// | ||
| /// Returns `Ok(None)` when no block-backed filesystem is found (e.g. virtiofs | ||
| /// in bcvk ephemeral, NFS root, ISO boot), so callers can skip gracefully. | ||
| /// Any other failure is an error. | ||
| #[context("Finding block device from /boot or /sysroot")] | ||
| pub(crate) fn list_dev_current_root() -> Result<Option<Device>> { | ||
| let root = Dir::open_ambient_dir("/", ambient_authority()).context("Opening /")?; | ||
| for path in ["boot", "sysroot"] { | ||
| if let Some(dev) = list_dev_of_mount(&root, path)? { | ||
| return Ok(Some(dev)); | ||
| } | ||
| } | ||
| anyhow::bail!("Failed to find block device from /boot or /sysroot") | ||
| Ok(None) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift 🔎 Supported by static analysis🏁 Script executed: sed -n '540,625p' src/bootupd.rs
sed -n '760,870p' src/bootupd.rs
rg -n 'findmnt|list_dev_current_root|find.*device|block.*device' src/bootupd.rs src/backend/statefile.rsRepository: coreos/bootupd Length of output: 8020 🏁 Script executed: set -eu
printf '%s\n' '--- dependency and symbol locations ---'
rg -n -g 'Cargo.toml' -g 'Cargo.lock' -g '*.rs' 'bootc_internal_blockdev|list_dev_by_dir|list_dev_current_root' .
printf '%s\n' '--- relevant diff ---'
git diff --unified=40 3ca1f3d006c4f297c5105aef2da8aa4f77c3ae65 b017de2d1af80525339e1d93236dce75b03b7576 -- src/bootupd.rs src/backend/statefile.rs
printf '%s\n' '--- statefile caller ---'
sed -n '1,90p' src/backend/statefile.rs
printf '%s\n' '--- dependency metadata ---'
rg -n -C 3 'name = "bootc-internal|bootc_internal_blockdev|bootc-internal' Cargo.toml Cargo.lockRepository: coreos/bootupd Length of output: 20474 🏁 Script executed: set -eu
printf '%s\n' '--- local cargo registry candidates ---'
find "$HOME/.cargo/registry/src" -type f \( -path '*bootc-internal-blockdev-1.16.12*' -o -path '*bootc-internal-blockdev-1.16.0*' \) -maxdepth 6 2>/dev/null | head -80
printf '%s\n' '--- local cargo source directories ---'
find "$HOME/.cargo/registry/src" -maxdepth 4 -type d -iname 'bootc-internal-blockdev-*' 2>/dev/null | head -20
printf '%s\n' '--- vendored dependency candidates ---'
find . -type f -path '*bootc*blockdev*' -o -type d -iname '*blockdev*' 2>/dev/null | head -80Repository: coreos/bootupd Length of output: 270 🏁 Script executed: set -eu
printf '%s\n' '--- local cargo registry candidates ---'
find "$HOME/.cargo/registry/src" -type f \( -path '*bootc-internal-blockdev-1.16.12*' -o -path '*bootc-internal-blockdev-1.16.0*' \) -maxdepth 6 2>/dev/null | head -80
printf '%s\n' '--- local cargo source directories ---'
find "$HOME/.cargo/registry/src" -maxdepth 4 -type d -iname 'bootc-internal-blockdev-*' 2>/dev/null | head -20
printf '%s\n' '--- vendored dependency candidates ---'
find . \( -type f -path '*bootc*blockdev*' -o -type d -iname '*blockdev*' \) 2>/dev/null | head -80Repository: coreos/bootupd Length of output: 270 🌐 Web query:
💡 Result: 🌐 Web query:
💡 Result: 🏁 Script executed: set -eu
url='https://raw.githubusercontent.com/bootc-dev/bootc/v1.16.12/crates/blockdev/src/blockdev.rs'
tmp="$(mktemp)"
trap 'rm -f "$tmp"' EXIT
curl --fail --silent --show-error --location "$url" -o "$tmp"
printf '%s\n' '--- list_dev_by_dir and adjacent source ---'
sed -n '3035,3105p' "$tmp"
printf '%s\n' '--- list_dev definitions and error handling ---'
rg -n -C 10 'fn list_dev\b|pub.*list_dev\b|No.*device|not a block device|list_dev_by_dir' "$tmp"Repository: coreos/bootupd Length of output: 6823 🏁 Script executed: set -eu
url='https://raw.githubusercontent.com/bootc-dev/bootc/v1.16.12/crates/blockdev/src/blockdev.rs'
tmp="$(mktemp)"
trap 'rm -f "$tmp"' EXIT
curl --fail --silent --show-error --location "$url" -o "$tmp"
sed -n '455,510p' "$tmp"Repository: coreos/bootupd Length of output: 2000 Do not convert device lookup errors into a successful no-device result.
Keep the 🤖 Prompt for AI Agents
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @cgwalters thoughts on this? And if we're keeping as-is, is there any point in the function returning a
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should certainly propagate (not swallow/suppress) errors apart from ENOENT or equivalent so yes should likely rework this There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @cgwalters-bot do ⬆️ But with a careful look at the cases where listing the blockdev might fail - like with the virtiofs case, I think we need to ensure again we're gracefully handling the "no blockdevs" cases. |
||
| } | ||
|
|
||
| /// Get the block device backing the filesystem mounted at `path`, or `None` | ||
| /// if `path` does not exist, is not a mountpoint, or is not block-backed. | ||
| #[context("Finding block device for {path}")] | ||
| fn list_dev_of_mount(root: &Dir, path: &str) -> Result<Option<Device>> { | ||
| let Some(dir) = root.open_dir_optional(path)? else { | ||
| return Ok(None); | ||
| }; | ||
| // `None` means the kernel can't tell us; let findmnt decide below. | ||
| if dir.is_mountpoint(".")? == Some(false) { | ||
| return Ok(None); | ||
|
Comment on lines
+603
to
+604
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Do not exclude a block-backed When Discovery then returns Allow filesystem classification for existing directories, or add a block-backed 🤖 Prompt for AI Agents |
||
| } | ||
| list_dev_by_dir_optional(&dir) | ||
| } | ||
|
|
||
| /// The statfs magic of ZFS (`ZFS_SUPER_MAGIC` in OpenZFS), which libc lacks. | ||
| const ZFS_SUPER_MAGIC: u32 = 0x2fc12fc1; | ||
|
|
||
| /// Whether a filesystem with the given `st_dev` and statfs magic is backed by | ||
| /// a block device. | ||
| /// | ||
| /// The kernel gives filesystems without one (virtiofs, NFS, tmpfs, | ||
| /// overlayfs, ...) an anonymous device number, whose major is 0. btrfs and | ||
| /// ZFS get anonymous device numbers too (btrfs one per subvolume) even though | ||
| /// they sit on block devices, so those are recognized by their magic. | ||
| fn is_block_backed(st_dev: u64, fs_magic: u32) -> bool { | ||
| rustix::fs::major(st_dev) != 0 | ||
| || fs_magic == libc::BTRFS_SUPER_MAGIC as u32 | ||
| || fs_magic == ZFS_SUPER_MAGIC | ||
| } | ||
|
|
||
| /// List the device containing the filesystem mounted at `dir`, or `None` if | ||
| /// that filesystem is not backed by a block device. | ||
| /// | ||
| /// TODO: Replace with `bootc_internal_blockdev::list_dev_by_dir_optional` | ||
| /// once a bootc release has it; this is a copy. | ||
| fn list_dev_by_dir_optional(dir: &Dir) -> Result<Option<Device>> { | ||
| let st_dev = rustix::fs::fstat(dir)?.st_dev; | ||
| // Filesystem magic numbers are 32 bits; f_type's width varies by arch. | ||
| let fs_magic = rustix::fs::fstatfs(dir)?.f_type as u32; | ||
| if !is_block_backed(st_dev, fs_magic) { | ||
| log::debug!("No block device: st_dev={st_dev:#x} f_type={fs_magic:#x}"); | ||
| return Ok(None); | ||
| } | ||
| bootc_internal_blockdev::list_dev_by_dir(dir).map(Some) | ||
| } | ||
|
|
||
| /// daemon implementation of component validate | ||
|
|
@@ -597,7 +645,9 @@ pub(crate) fn validate(name: &str) -> Result<ValidationResult> { | |
| let Some(inst) = state.installed.get(name) else { | ||
| anyhow::bail!("Component {} is not installed", name); | ||
| }; | ||
| let device = list_dev_current_root()?; | ||
| let Some(device) = list_dev_current_root()? else { | ||
| return Ok(ValidationResult::Skip); | ||
| }; | ||
| component.validate(inst, &device) | ||
| } | ||
|
|
||
|
|
@@ -771,17 +821,27 @@ impl RootContext { | |
| } | ||
| } | ||
|
|
||
| /// Initialize parent devices to prepare the update | ||
| fn prep_before_update() -> Result<RootContext> { | ||
| /// Initialize parent devices to prepare the update. | ||
| /// | ||
| /// Returns `Ok(None)` when no block-backed boot filesystem is found, | ||
| /// so the caller can skip the update gracefully. | ||
| fn prep_before_update() -> Result<Option<RootContext>> { | ||
| let path = "/"; | ||
| let sysroot = Dir::open_ambient_dir(path, ambient_authority()).context("Opening root dir")?; | ||
| let device = list_dev_current_root()?; | ||
| Ok(RootContext::new(sysroot, path, device)) | ||
| let Some(device) = list_dev_current_root()? else { | ||
| println!( | ||
| "No block-backed boot filesystem found; bootloader update is not applicable, skipping." | ||
| ); | ||
| return Ok(None); | ||
| }; | ||
| Ok(Some(RootContext::new(sysroot, path, device))) | ||
| } | ||
|
|
||
| pub(crate) fn client_run_update() -> Result<()> { | ||
| crate::try_fail_point!("update"); | ||
| let rootcxt = prep_before_update()?; | ||
| let Some(rootcxt) = prep_before_update()? else { | ||
| return Ok(()); | ||
| }; | ||
| let status: Status = status()?; | ||
| if status.components.is_empty() && status.adoptable.is_empty() { | ||
| println!("No components installed."); | ||
|
|
@@ -836,7 +896,9 @@ pub(crate) fn client_run_update() -> Result<()> { | |
| } | ||
|
|
||
| pub(crate) fn client_run_adopt_and_update(with_static_config: bool) -> Result<()> { | ||
| let rootcxt = prep_before_update()?; | ||
| let Some(rootcxt) = prep_before_update()? else { | ||
| return Ok(()); | ||
| }; | ||
| let status: Status = status()?; | ||
| if status.adoptable.is_empty() { | ||
| println!("No components are adoptable."); | ||
|
|
@@ -1008,6 +1070,50 @@ fn strip_grub_config_file( | |
| mod tests { | ||
| use super::*; | ||
|
|
||
| #[test] | ||
| fn test_is_block_backed() { | ||
| use rustix::fs::makedev; | ||
| // Filesystem magic numbers are 32 bits; see list_dev_by_dir_optional. | ||
| let cases = [ | ||
| (makedev(252, 3), libc::XFS_SUPER_MAGIC as u32, true), | ||
| (makedev(253, 0), libc::EXT4_SUPER_MAGIC as u32, true), | ||
| (makedev(0, 38), libc::BTRFS_SUPER_MAGIC as u32, true), | ||
| (makedev(0, 51), ZFS_SUPER_MAGIC, true), | ||
| (makedev(0, 29), libc::FUSE_SUPER_MAGIC as u32, false), | ||
| (makedev(0, 52), libc::NFS_SUPER_MAGIC as u32, false), | ||
| (makedev(0, 40), libc::OVERLAYFS_SUPER_MAGIC as u32, false), | ||
| (makedev(0, 23), libc::TMPFS_MAGIC as u32, false), | ||
| ]; | ||
| for (st_dev, fs_magic, expected) in cases { | ||
| assert_eq!( | ||
| is_block_backed(st_dev, fs_magic), | ||
| expected, | ||
| "st_dev={st_dev:#x} f_type={fs_magic:#x}" | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_list_dev_by_dir_optional_procfs() -> Result<()> { | ||
| let proc = Dir::open_ambient_dir("/proc", ambient_authority())?; | ||
| assert!(list_dev_by_dir_optional(&proc)?.is_none()); | ||
| Ok(()) | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_list_dev_of_mount() -> Result<()> { | ||
| let td = tempfile::tempdir()?; | ||
| let root = Dir::open_ambient_dir(td.path(), ambient_authority())?; | ||
| // Missing, or a plain directory rather than a mountpoint: skip. | ||
| assert!(list_dev_of_mount(&root, "boot")?.is_none()); | ||
| root.create_dir("boot")?; | ||
| assert!(list_dev_of_mount(&root, "boot")?.is_none()); | ||
| // Anything else propagates. | ||
| root.write("sysroot", "not a directory")?; | ||
| assert!(list_dev_of_mount(&root, "sysroot").is_err()); | ||
| Ok(()) | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_failpoint_update() { | ||
| let guard = fail::FailScenario::setup(); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
BTW followup let's bump to 26.04 across the board