From e19fc391ff5b4c6a7b129b500a4c26c2ed37058a Mon Sep 17 00:00:00 2001 From: Colin Walters Date: Thu, 20 Aug 2026 11:37:59 -0400 Subject: [PATCH] lib/container_export: Always skip /tmp and /var/tmp during export CentOS 10 UKI CI jobs fail "bootc container export --format=tar" with "No label found in policy ... for /var/tmp/rhc". rhc's post-install scriptlet drops runtime state under /var/tmp during image build (same rhc-1:0.3.12-1.el10 build in both passing and failing CI runs, so this isn't a version regression in rhc itself), and the SELinux targeted policy simply has no file-context entry for it. This only reproduces reliably on the composefs+uki matrix legs, likely because that build path takes long enough for the scriptlet's async write to land before the image layer is committed - the file can be present or absent on other legs depending on timing. Rather than trying to tolerate arbitrary unlabeled paths anywhere in the tree (which risks silently exporting genuinely mislabeled files), extend the existing SKIP_PATHS list to always exclude /tmp and /var/tmp. These are meant to hold only ephemeral, runtime-created content - ostree-ext::commit's FORCE_CLEAN_PATHS already treats the same two paths (plus /run and /var/cache) this way for regular ostree commits, so tar export dropping them is consistent with how bootc already treats the real /var as not being part of the shippable content. Add a unit test exercising export_filesystem_walk() directly (with SELinux labeling disabled) against a synthetic root, verifying /tmp and /var/tmp content is dropped while everything else is kept. Assisted-by: AI Signed-off-by: Colin Walters --- crates/lib/src/container_export.rs | 68 +++++++++++++++++++++++++++++- 1 file changed, 66 insertions(+), 2 deletions(-) diff --git a/crates/lib/src/container_export.rs b/crates/lib/src/container_export.rs index 4246dd46cc..eb77c7139a 100644 --- a/crates/lib/src/container_export.rs +++ b/crates/lib/src/container_export.rs @@ -126,8 +126,13 @@ fn tar_header_dir_root() -> tar::Header { } /// Paths that should be skipped during export. -/// These are bootc/ostree-specific paths that shouldn't be in the exported tarball. -const SKIP_PATHS: &[&str] = &["sysroot/ostree"]; +/// - `sysroot/ostree` is bootc/ostree-specific and shouldn't be in the exported tarball. +/// - `tmp` and `var/tmp` are meant to hold only ephemeral, runtime-created content (the +/// same paths `ostree-ext::commit` always cleans before committing). They can end up +/// containing arbitrary files dropped by package post-install scripts (e.g. `rhc`) +/// that the SELinux policy has no file-context entry for, which would otherwise turn +/// into a hard failure when computing labels for the tar entries. +const SKIP_PATHS: &[&str] = &["sysroot/ostree", "tmp", "var/tmp"]; fn export_filesystem_walk( tar_builder: &mut tar::Builder, @@ -412,3 +417,62 @@ fn add_selinux_pax_extension( .context("Failed to add SELinux PAX extension")?; Ok(()) } + +#[cfg(test)] +mod tests { + use super::*; + use cap_std_ext::cap_std::{ambient_authority, fs::Dir}; + + /// Walk `root` (with SELinux labeling disabled) and return the set of + /// relative paths that ended up in the resulting tar archive. + fn exported_paths(root: &std::path::Path) -> Result> { + let dir = Dir::open_ambient_dir(root, ambient_authority())?; + let mut buf = Vec::new(); + { + let mut tar_builder = tar::Builder::new(&mut buf); + export_filesystem_walk(&mut tar_builder, &dir, None)?; + tar_builder.finish()?; + } + tar::Archive::new(buf.as_slice()) + .entries()? + .map(|e| Ok(e?.path()?.to_string_lossy().into_owned())) + .collect() + } + + #[test] + fn test_export_skips_tmp_and_var_tmp() -> Result<()> { + let tmpdir = tempfile::tempdir()?; + let root = tmpdir.path(); + + // Content that must be skipped, including a stand-in for the + // `/var/tmp/rhc` file dropped by package post-install scripts that + // the SELinux policy has no file-context entry for. + std::fs::create_dir_all(root.join("tmp/nested"))?; + std::fs::write(root.join("tmp/nested/junk"), b"junk")?; + std::fs::create_dir_all(root.join("var/tmp"))?; + std::fs::write(root.join("var/tmp/rhc"), b"rhc-state")?; + + // Content that must be preserved. + std::fs::create_dir_all(root.join("usr/bin"))?; + std::fs::write(root.join("usr/bin/keep-me"), b"binary")?; + std::fs::create_dir_all(root.join("var/lib"))?; + std::fs::write(root.join("var/lib/keep-me-too"), b"state")?; + + let paths = exported_paths(root)?; + + assert!(paths.contains("usr/bin/keep-me")); + assert!(paths.contains("var/lib/keep-me-too")); + assert!( + !paths.iter().any(|p| p == "tmp" || p.starts_with("tmp/")), + "expected no /tmp entries, got: {paths:?}" + ); + assert!( + !paths + .iter() + .any(|p| p == "var/tmp" || p.starts_with("var/tmp/")), + "expected no /var/tmp entries, got: {paths:?}" + ); + + Ok(()) + } +}