Skip to content

Commit 355684a

Browse files
committed
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)
1 parent c3c1602 commit 355684a

3 files changed

Lines changed: 52 additions & 25 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,3 +121,7 @@ todo = "deny"
121121
# to trigger, and among the least valuable to fix.
122122
needless_borrow = "allow"
123123
needless_borrows_for_generic_args = "allow"
124+
125+
# Temporary: use cap-std-ext fork with skip_mountpoints() support
126+
[patch.crates-io]
127+
cap-std-ext = { git = "https://github.com/jeckersb/cap-std-ext", branch = "skip_mountpoints" }

crates/lib/src/lsm.rs

Lines changed: 47 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -387,14 +387,20 @@ pub(crate) fn relabel_recurse(
387387
relabel_recurse_inner(root, &mut path, as_path.as_mut(), policy)
388388
}
389389

390-
/// A wrapper for creating a directory, also optionally setting a SELinux label.
391-
/// The provided `skip` parameter is a device/inode that we will ignore (and not traverse).
390+
/// Recursively ensure all files under a directory have SELinux labels.
391+
/// Uses the `walk` API with `noxdev` to avoid crossing mount point boundaries
392+
/// (e.g. into sysfs, procfs, etc.).
393+
/// The provided `skip` parameter is a device/inode pair that we will ignore
394+
/// (and not traverse into).
392395
pub(crate) fn ensure_dir_labeled_recurse(
393396
root: &Dir,
394397
path: &mut Utf8PathBuf,
395398
policy: &ostree::SePolicy,
396399
skip: Option<(libc::dev_t, libc::ino64_t)>,
397400
) -> Result<()> {
401+
use cap_std_ext::dirext::WalkConfiguration;
402+
use std::ops::ControlFlow;
403+
398404
// Juggle the cap-std requirement for relative paths vs the libselinux
399405
// requirement for absolute paths by special casing the empty string "" as "."
400406
// just for the initial directory enumeration.
@@ -406,6 +412,7 @@ pub(crate) fn ensure_dir_labeled_recurse(
406412

407413
let mut n = 0u64;
408414

415+
// Label the starting directory itself; the walk API only visits children.
409416
let metadata = root.symlink_metadata(path_for_read)?;
410417
match ensure_labeled(root, path, &metadata, policy)? {
411418
SELinuxLabelState::Unlabeled => {
@@ -414,35 +421,52 @@ pub(crate) fn ensure_dir_labeled_recurse(
414421
SELinuxLabelState::Unsupported => return Ok(()),
415422
SELinuxLabelState::Labeled => {}
416423
}
417-
418-
for ent in root.read_dir(path_for_read)? {
419-
let ent = ent?;
420-
let metadata = ent.metadata()?;
421-
if let Some((skip_dev, skip_ino)) = skip.as_ref().copied() {
422-
if (metadata.dev(), metadata.ino()) == (skip_dev, skip_ino) {
423-
tracing::debug!("Skipping dev={skip_dev} inode={skip_ino}");
424-
continue;
424+
let config = WalkConfiguration::default()
425+
.noxdev()
426+
.skip_mountpoints()
427+
.path_base(path_for_read.as_std_path());
428+
429+
root.open_dir(path_for_read)?
430+
.walk::<_, anyhow::Error>(&config, |component| {
431+
let metadata = component.entry.metadata()?;
432+
433+
// Check if this entry should be skipped
434+
if let Some((skip_dev, skip_ino)) = skip {
435+
if (metadata.dev(), metadata.ino()) == (skip_dev, skip_ino) {
436+
tracing::debug!("Skipping dev={skip_dev} inode={skip_ino}");
437+
// For directories, Break skips traversal into the directory
438+
// but continues with the next sibling. For non-directories,
439+
// Break would skip all remaining siblings, so use Continue
440+
// to skip only this entry.
441+
if component.file_type.is_dir() {
442+
return Ok(ControlFlow::Break(()));
443+
} else {
444+
return Ok(ControlFlow::Continue(()));
445+
}
446+
}
425447
}
426-
}
427-
let name = ent.file_name();
428-
let name = name
429-
.to_str()
430-
.ok_or_else(|| anyhow::anyhow!("Invalid non-UTF-8 filename: {name:?}"))?;
431-
path.push(name);
432448

433-
if metadata.is_dir() {
434-
ensure_dir_labeled_recurse(root, path, policy, skip)?;
435-
} else {
449+
let path = Utf8Path::from_path(component.path)
450+
.ok_or_else(|| anyhow::anyhow!("Invalid non-UTF-8 path: {:?}", component.path))?;
451+
436452
match ensure_labeled(root, path, &metadata, policy)? {
437453
SELinuxLabelState::Unlabeled => {
438454
n += 1;
439455
}
440-
SELinuxLabelState::Unsupported => break,
456+
// We check for Unsupported on the starting directory above,
457+
// and the walk uses noxdev + skip_mountpoints to stay on
458+
// the same filesystem, so hitting Unsupported here is
459+
// unexpected.
460+
SELinuxLabelState::Unsupported => {
461+
anyhow::bail!(
462+
"Unexpected SELinuxLabelState::Unsupported during walk at {path}"
463+
);
464+
}
441465
SELinuxLabelState::Labeled => {}
442466
}
443-
}
444-
path.pop();
445-
}
467+
468+
Ok(ControlFlow::Continue(()))
469+
})?;
446470

447471
if n > 0 {
448472
tracing::debug!("Relabeled {n} objects in {path}");

0 commit comments

Comments
 (0)