From c76e936829a6cbe5e2d9a96d743da9282f69358e Mon Sep 17 00:00:00 2001 From: John Eckersberg Date: Wed, 4 Mar 2026 16:39:03 -0500 Subject: [PATCH] lsm: Use walk API with noxdev/skip_mountpoints for recursive SELinux relabeling Rewrite ensure_dir_labeled_recurse to use the cap_std_ext walk API with noxdev and skip_mountpoints instead of manually recursing through directories. This prevents the recursive labeling from crossing mount point boundaries, avoiding failures when pseudo-filesystems like sysfs are mounted under the target root. Assisted-by: OpenCode (claude-opus-4-6) Signed-off-by: John Eckersberg --- Cargo.lock | 24 +++++++-------- Cargo.toml | 3 +- crates/lib/src/lsm.rs | 71 +++++++++++++++++++++++++++++-------------- 3 files changed, 62 insertions(+), 36 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index acb81221f..2f2947623 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -201,7 +201,7 @@ dependencies = [ "bootc-internal-utils", "bootc-mount", "camino", - "cap-std-ext 5.0.0", + "cap-std-ext 5.1.1", "fn-error-context", "indoc", "libc", @@ -219,7 +219,7 @@ version = "0.1.0" dependencies = [ "anstream 1.0.0", "anyhow", - "cap-std-ext 5.0.0", + "cap-std-ext 5.1.1", "chrono", "owo-colors", "rustix", @@ -261,7 +261,7 @@ dependencies = [ "bootc-tmpfiles", "camino", "canon-json", - "cap-std-ext 5.0.0", + "cap-std-ext 5.1.1", "cfg-if", "cfsctl", "chrono", @@ -315,7 +315,7 @@ dependencies = [ "anyhow", "bootc-internal-utils", "camino", - "cap-std-ext 5.0.0", + "cap-std-ext 5.1.1", "fn-error-context", "indoc", "libc", @@ -332,7 +332,7 @@ dependencies = [ "anyhow", "bootc-internal-utils", "camino", - "cap-std-ext 5.0.0", + "cap-std-ext 5.1.1", "fn-error-context", "hex", "indoc", @@ -350,7 +350,7 @@ dependencies = [ "anyhow", "bootc-internal-utils", "camino", - "cap-std-ext 5.0.0", + "cap-std-ext 5.1.1", "fn-error-context", "indoc", "rustix", @@ -485,9 +485,9 @@ dependencies = [ [[package]] name = "cap-std-ext" -version = "5.0.0" +version = "5.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d201b8b353bc963f2e8c374be7866dba47082ff041fb0db2d7543f2b9c8367ce" +checksum = "56f9d7cf114dea582f663f03f4c563d0fc5ca2c8fa4c496eb538d8f01981ea51" dependencies = [ "cap-primitives 4.0.2", "cap-tempfile 4.0.2", @@ -1145,7 +1145,7 @@ version = "0.1.0" dependencies = [ "anstream 1.0.0", "anyhow", - "cap-std-ext 5.0.0", + "cap-std-ext 5.1.1", "composefs", "fn-error-context", "hex", @@ -1977,7 +1977,7 @@ checksum = "784ff94e3f5a9b89659b8a4442104df6b5e7974c9b355c4f4ae6e9794af1ad2b" dependencies = [ "camino", "canon-json", - "cap-std-ext 5.0.0", + "cap-std-ext 5.1.1", "chrono", "flate2", "hex", @@ -2077,7 +2077,7 @@ dependencies = [ "bootc-internal-utils", "camino", "canon-json", - "cap-std-ext 5.0.0", + "cap-std-ext 5.1.1", "chrono", "clap", "clap_mangen", @@ -2896,7 +2896,7 @@ dependencies = [ "anyhow", "bootc-kernel-cmdline", "camino", - "cap-std-ext 5.0.0", + "cap-std-ext 5.1.1", "clap", "fn-error-context", "indoc", diff --git a/Cargo.toml b/Cargo.toml index 7e0add2c5..ce2e9fc20 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,7 +35,7 @@ anstream = "1.0" anyhow = "1.0.82" camino = "1.1.6" canon-json = "0.2.1" -cap-std-ext = "5.0.0" +cap-std-ext = "5.1.1" cfg-if = "1.0" chrono = { version = "0.4.38", default-features = false } clap = "4.5.4" @@ -121,3 +121,4 @@ todo = "deny" # to trigger, and among the least valuable to fix. needless_borrow = "allow" needless_borrows_for_generic_args = "allow" + diff --git a/crates/lib/src/lsm.rs b/crates/lib/src/lsm.rs index fa014ba1e..2c059406d 100644 --- a/crates/lib/src/lsm.rs +++ b/crates/lib/src/lsm.rs @@ -387,14 +387,21 @@ pub(crate) fn relabel_recurse( relabel_recurse_inner(root, &mut path, as_path.as_mut(), policy) } -/// A wrapper for creating a directory, also optionally setting a SELinux label. -/// The provided `skip` parameter is a device/inode that we will ignore (and not traverse). +/// Recursively ensure all files under a directory have SELinux labels. +/// Uses the `walk` API with `noxdev` and `skip_mountpoints` to avoid crossing +/// mount point boundaries +/// (e.g. into sysfs, procfs, etc.). +/// The provided `skip` parameter is a device/inode pair that we will ignore +/// (and not traverse into). pub(crate) fn ensure_dir_labeled_recurse( root: &Dir, path: &mut Utf8PathBuf, policy: &ostree::SePolicy, skip: Option<(libc::dev_t, libc::ino64_t)>, ) -> Result<()> { + use cap_std_ext::dirext::WalkConfiguration; + use std::ops::ControlFlow; + // Juggle the cap-std requirement for relative paths vs the libselinux // requirement for absolute paths by special casing the empty string "" as "." // just for the initial directory enumeration. @@ -406,6 +413,7 @@ pub(crate) fn ensure_dir_labeled_recurse( let mut n = 0u64; + // Label the starting directory itself; the walk API only visits children. let metadata = root.symlink_metadata(path_for_read)?; match ensure_labeled(root, path, &metadata, policy)? { SELinuxLabelState::Unlabeled => { @@ -414,35 +422,52 @@ pub(crate) fn ensure_dir_labeled_recurse( SELinuxLabelState::Unsupported => return Ok(()), SELinuxLabelState::Labeled => {} } - - for ent in root.read_dir(path_for_read)? { - let ent = ent?; - let metadata = ent.metadata()?; - if let Some((skip_dev, skip_ino)) = skip.as_ref().copied() { - if (metadata.dev(), metadata.ino()) == (skip_dev, skip_ino) { - tracing::debug!("Skipping dev={skip_dev} inode={skip_ino}"); - continue; + let config = WalkConfiguration::default() + .noxdev() + .skip_mountpoints() + .path_base(path_for_read.as_std_path()); + + root.open_dir(path_for_read)? + .walk::<_, anyhow::Error>(&config, |component| { + let metadata = component.entry.metadata()?; + + // Check if this entry should be skipped + if let Some((skip_dev, skip_ino)) = skip { + if (metadata.dev(), metadata.ino()) == (skip_dev, skip_ino) { + tracing::debug!("Skipping dev={skip_dev} inode={skip_ino}"); + // For directories, Break skips traversal into the directory + // but continues with the next sibling. For non-directories, + // Break would skip all remaining siblings, so use Continue + // to skip only this entry. + if component.file_type.is_dir() { + return Ok(ControlFlow::Break(())); + } else { + return Ok(ControlFlow::Continue(())); + } + } } - } - let name = ent.file_name(); - let name = name - .to_str() - .ok_or_else(|| anyhow::anyhow!("Invalid non-UTF-8 filename: {name:?}"))?; - path.push(name); - if metadata.is_dir() { - ensure_dir_labeled_recurse(root, path, policy, skip)?; - } else { + let path = Utf8Path::from_path(component.path) + .ok_or_else(|| anyhow::anyhow!("Invalid non-UTF-8 path: {:?}", component.path))?; + match ensure_labeled(root, path, &metadata, policy)? { SELinuxLabelState::Unlabeled => { n += 1; } - SELinuxLabelState::Unsupported => break, + // We check for Unsupported on the starting directory above, + // and the walk uses noxdev + skip_mountpoints to stay on + // the same filesystem, so hitting Unsupported here is + // unexpected. + SELinuxLabelState::Unsupported => { + anyhow::bail!( + "Unexpected SELinuxLabelState::Unsupported during walk at {path}" + ); + } SELinuxLabelState::Labeled => {} } - } - path.pop(); - } + + Ok(ControlFlow::Continue(())) + })?; if n > 0 { tracing::debug!("Relabeled {n} objects in {path}");