From 17f1a1d009332618d636cf8a5a5bbc3cbf89bf45 Mon Sep 17 00:00:00 2001 From: Pragyan Poudyal Date: Fri, 3 Jul 2026 14:13:49 +0530 Subject: [PATCH 1/4] cfs/status: Output manifest digest instead of image id We were writing out the ImageId in output to `bootc status` instead of writing the manifest digest Fixes: bootc-dev/bink#90 Signed-off-by: Pragyan Poudyal --- crates/lib/src/bootc_composefs/export.rs | 2 +- crates/lib/src/bootc_composefs/status.rs | 17 ++++++++++++----- crates/lib/src/bootc_composefs/update.rs | 2 +- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/crates/lib/src/bootc_composefs/export.rs b/crates/lib/src/bootc_composefs/export.rs index ea760e603..745519a80 100644 --- a/crates/lib/src/bootc_composefs/export.rs +++ b/crates/lib/src/bootc_composefs/export.rs @@ -43,7 +43,7 @@ pub async fn export_repo_to_image( let depl_verity = depl_verity.ok_or_else(|| anyhow::anyhow!("Image {source} not found"))?; - let imginfo = get_imginfo(storage, &depl_verity)?; + let (imginfo, _) = get_imginfo(storage, &depl_verity)?; let config_digest = imginfo.manifest.config().digest().clone(); diff --git a/crates/lib/src/bootc_composefs/status.rs b/crates/lib/src/bootc_composefs/status.rs index c8e7851c3..62e906628 100644 --- a/crates/lib/src/bootc_composefs/status.rs +++ b/crates/lib/src/bootc_composefs/status.rs @@ -7,6 +7,7 @@ use composefs_ctl::composefs::fsverity::Sha512HashValue; use composefs_ctl::composefs_oci; use composefs_oci::OciImage; use fn_error_context::context; +use openssl::sha::Sha256; use serde::{Deserialize, Serialize}; use crate::{ @@ -480,7 +481,10 @@ pub(crate) fn get_bootloader() -> Result { /// Falls back to reading legacy `.imginfo` files for backwards compatibility /// with deployments created before the manifest digest was stored in `.origin`. #[context("Reading image info for deployment {deployment_id}")] -pub(crate) fn get_imginfo(storage: &Storage, deployment_id: &str) -> Result { +pub(crate) fn get_imginfo( + storage: &Storage, + deployment_id: &str, +) -> Result<(ImgConfigManifest, String)> { let ini = read_origin(&storage.physical_root, deployment_id)? .ok_or_else(|| anyhow::anyhow!("No origin file for deployment {deployment_id}"))?; @@ -501,7 +505,7 @@ pub(crate) fn get_imginfo(storage: &Storage, deployment_id: &str) -> Result Result(&buffer) .context("Failed to parse .imginfo file as JSON")?; - Ok(img_conf) + // Compute the manifest digest + let mut hasher = Sha256::new(); + hasher.update(&serde_json::to_vec(&img_conf.manifest).context("Serializing image manifest")?); + let manifest_digest_str = hex::encode(hasher.finish()); + Ok((img_conf, manifest_digest_str)) } #[context("Getting composefs deployment metadata")] @@ -543,9 +551,8 @@ fn boot_entry_from_composefs_deployment( let ostree_img_ref = OstreeImageReference::from_str(&img_name_from_config)?; let img_ref = crate::spec::ImageReference::from(ostree_img_ref); - let img_conf = get_imginfo(storage, &verity)?; + let (img_conf, image_digest) = get_imginfo(storage, &verity)?; - let image_digest = img_conf.manifest.config().digest().to_string(); let architecture = img_conf.config.architecture().to_string(); let version = img_conf .manifest diff --git a/crates/lib/src/bootc_composefs/update.rs b/crates/lib/src/bootc_composefs/update.rs index e0e677958..1f48021c7 100644 --- a/crates/lib/src/bootc_composefs/update.rs +++ b/crates/lib/src/bootc_composefs/update.rs @@ -529,7 +529,7 @@ pub(crate) async fn upgrade_composefs( } if opts.check { - let current_manifest = get_imginfo(storage, &*composefs.cmdline.digest)?; + let (current_manifest, _) = get_imginfo(storage, &*composefs.cmdline.digest)?; let diff = ManifestDiff::new(¤t_manifest.manifest, &img_config.manifest); diff.print(); return Ok(()); From 8104413856a9339b6f8166fcaab36d5b4cc3d9e0 Mon Sep 17 00:00:00 2001 From: Pragyan Poudyal Date: Mon, 6 Jul 2026 13:41:23 +0530 Subject: [PATCH 2/4] etc-merge: Use `symlink_metadata_optional` cap_std's `metadata_optional` follows symlinks and errors out if the symlink target is outside the current root Use `symlink_metadata_optional` when checking for removed files in new etc so that symlinks that resolve to a path outside of new_etc don't get followed Fixes: #2278 Signed-off-by: Pragyan Poudyal --- crates/etc-merge/src/lib.rs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/crates/etc-merge/src/lib.rs b/crates/etc-merge/src/lib.rs index 0b78a0af5..1ea0d1f72 100644 --- a/crates/etc-merge/src/lib.rs +++ b/crates/etc-merge/src/lib.rs @@ -794,7 +794,9 @@ pub fn merge( .context("Merging modified files")?; for removed in &diff.removed { - let stat = new_etc_fd.metadata_optional(&removed)?; + // Use symlink_metadata_optional so that symlinks that resolve to a path + // outside the new_etc_fd don't get followed + let stat = new_etc_fd.symlink_metadata_optional(&removed)?; let Some(stat) = stat else { // File/dir doesn't exist in new_etc @@ -1071,6 +1073,12 @@ mod tests { n.create_dir_all("dir/perms")?; n.write("dir/perms/some-file", "Some-file")?; + // File exists in pristine and new_etc (as a symlink pointing outside the root), + // but was deleted in current_etc. The merge should remove it from new_etc + // without following the symlink. + p.write("ext-symlink", "pristine content")?; + symlinkat("/usr/bin/bash", &n, "ext-symlink")?; + let (pristine_etc_files, current_etc_files, new_etc_files) = traverse_etc(&p, &c, Some(&n))?; let diff = compute_diff( @@ -1128,6 +1136,9 @@ mod tests { DIR_BITS | 0o775 ); + // External symlink should be removed without following it + assert!(!n.exists("ext-symlink")); + Ok(()) } From aa3a2fbad5433a654f413e720b9e35b7c0392b4b Mon Sep 17 00:00:00 2001 From: Pragyan Poudyal Date: Tue, 7 Jul 2026 10:30:02 +0530 Subject: [PATCH 3/4] tests: Skip test-loader-entries-source for ostree v2026.2 This test ends up failing for systems with ostree v2026.2. Disable it until we have a fix Signed-off-by: Pragyan Poudyal --- tmt/tests/booted/test-loader-entries-source.nu | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tmt/tests/booted/test-loader-entries-source.nu b/tmt/tests/booted/test-loader-entries-source.nu index b52656798..423f80a97 100644 --- a/tmt/tests/booted/test-loader-entries-source.nu +++ b/tmt/tests/booted/test-loader-entries-source.nu @@ -25,6 +25,13 @@ use std assert use tap.nu +let is_bad_version = ostree --version | lines | any {|l| $l | str contains "2026.2" } + +if $is_bad_version { + print "Found Ostree v2026.2, skipping test" + exit 0 +} + def parse_cmdline [] { open /proc/cmdline | str trim | split row " " } From 445ac69884f85b56b9c701da7004779a545c9b11 Mon Sep 17 00:00:00 2001 From: Colin Walters Date: Tue, 7 Jul 2026 15:27:32 -0400 Subject: [PATCH 4/4] tests: Fix test_storage_labels for Podman 5.0+ The defaultNetworkBackend file is no longer created by Podman 5.0+ (CentOS 10), which removed CNI support in favor of Netavark as the sole network backend. When getfattr was called on this non-existent path, it produced empty stdout, causing Nushell's split/get to fail with an unhelpful "index too large (max: 0)" error. Remove defaultNetworkBackend from the checked paths; the remaining "." and "overlay-images" are sufficient to verify recursive SELinux relabeling. Also harden get_file_selinux_type with explicit checks so future failures produce clear diagnostic messages. --- .../booted/test-logically-bound-install.nu | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/tmt/tests/booted/test-logically-bound-install.nu b/tmt/tests/booted/test-logically-bound-install.nu index b97066b70..d2c36fcbf 100644 --- a/tmt/tests/booted/test-logically-bound-install.nu +++ b/tmt/tests/booted/test-logically-bound-install.nu @@ -44,14 +44,28 @@ def test_bootc_image_list [] { # Get just the type (foo_t) from a security context def get_file_selinux_type [p] { - getfattr --only-values -n security.selinux $p | split row ':' | get 2 + if not ($p | path exists) { + error make {msg: $"Path ($p) does not exist"} + } + let context = (getfattr --only-values -n security.selinux $p) + if ($context | is-empty) { + error make {msg: $"SELinux context on ($p) is empty or missing"} + } + let parts = $context | split row ':' + if ($parts | length) < 3 { + error make {msg: $"SELinux context on ($p) is malformed: ($context)"} + } + $parts | get 2 } # Verify that the SELinux labels on the main "containers-storage:" instance match ours. # See the relabeling we do in imgstorage.rs. We only verify types, because the role # may depend on the creating user. def test_storage_labels [] { - for v in [".", "overlay-images", "defaultNetworkBackend"] { + # Verify a representative set of storage paths. Note that + # "defaultNetworkBackend" was removed; Podman 5.0+ dropped CNI + # support and no longer creates that file. + for v in [".", "overlay-images"] { let base = (get_file_selinux_type $"/var/lib/containers/storage/($v)") let target = (get_file_selinux_type $"/usr/lib/bootc/storage/($v)") assert equal $base $target