lsm: Use walk API with noxdev for recursive SELinux relabeling#2045
lsm: Use walk API with noxdev for recursive SELinux relabeling#2045jeckersb wants to merge 1 commit intobootc-dev:mainfrom
Conversation
There was a problem hiding this comment.
Code Review
This pull request refactors ensure_dir_labeled_recurse to use the cap_std_ext walk API with noxdev, which is a good improvement to prevent crossing mount point boundaries during recursive SELinux relabeling. However, a critical security vulnerability has been introduced: the use of ControlFlow::Break(()) prematurely terminates the SELinux relabeling process, leaving the filesystem in an inconsistently labeled state. Additionally, the logic for counting relabeled items no longer includes the root directory, leading to an inaccurate count, and the handling of skipped items incorrectly terminates the entire walk instead of just skipping the specified entry.
crates/lib/src/lsm.rs
Outdated
| 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}"); | ||
| return Ok(ControlFlow::Break(())); |
There was a problem hiding this comment.
The use of ControlFlow::Break(()) here is a significant security regression. It prematurely terminates the entire directory traversal, leaving subsequent files and directories without correct SELinux labels if a skipped entry is found. This is a regression from the previous implementation that would only skip that single entry. You should use ControlFlow::Continue(()) to skip only the current item and continue the walk. Note that if the skipped entry is a directory, walk() will still descend into it, which might be an unintended side effect if skip can be a directory.
| return Ok(ControlFlow::Break(())); | |
| return Ok(ControlFlow::Continue(())); |
There was a problem hiding this comment.
Hm really we should do both; Break() for skipped directories and Continue() for skipped non-directories.
There was a problem hiding this comment.
Wait no I think Gemini is wrong here, it probably just didn't load the docs, but see https://docs.rs/cap-std-ext/latest/cap_std_ext/dirext/trait.CapStdExtDirExt.html#tymethod.walk
Break is documented to be used for exactly this purpose.
There was a problem hiding this comment.
This is a problem with the Gemini review, my understand is what happens on the backend is it might clone the git repo, but it doesn't usually proactively try to load content from outside of that, and so it just works off ambient knowledge, and in this particular case I think it was just guessing about semantics of cap-std-ext.
It's for reasons like this that honestly (as good as it is sometimes) I want to try to go to a place where AI reviews are triggered and owned by humans and we've tweaked them to ensure they include relevant context by default and crucially are running in our default devcontainer so they can actually test things and not just guess.
There was a problem hiding this comment.
Well I think it's mostly correct though. Usually you'd skip a directory so that should be fine to Break, but if you happen to skip a non-directory using Break is going to skip not just the skipped entry but also its siblings which is not what we want.
There was a problem hiding this comment.
We know what we passed in is a directory though. How about a check to that effect + comment here?
There was a problem hiding this comment.
In the "big picture" outside of here, yes we only skip directories, but as far as this little bit of API goes we only have the (dev_t, ino64_t) pair so in theory you could skip regular files for some reason. I'm going to push a new version that checks for both and chooses accordingly but if I'm misunderstanding something we can change it again.
There was a problem hiding this comment.
On a related note I have some code laying around in a branch or stash somewhere that changes the skip from Option<(libc::dev_t, libc::ino64_t)> to &[(libc::dev_t, libc::ino64_t)] since as we untangle all of this I think if we're going to label the entire physical root we're going to need to skip composefs/objects (after labeling them usr_t prior) and also the ostree deployment root(s), so it's going to need to take multiple things to skip.
There was a problem hiding this comment.
Why would we skip composefs/objects? I think the label on them can just be usr_t too.
We do probably do need to skip the ostree deployment roots, but again I think ostree nowadays is already setting labels for those regardless, and THIS code should be doing "only set label if none exists" right?
There was a problem hiding this comment.
Ah that's true, I was thinking after we labeled the composefs objects we would need to be careful not to re-label them in the bulk relabel, but yeah this code skips any labeled files, even if they have the "wrong" label per the policy.
Rewrite ensure_dir_labeled_recurse to use the cap_std_ext walk API with noxdev 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 <jeckersb@redhat.com>
| return Ok(ControlFlow::Break(())); | ||
| } else { | ||
| return Ok(ControlFlow::Continue(())); | ||
| } |
There was a problem hiding this comment.
If this is just wrong let me know but that's how I understand it to work from the docs.
|
/gemini review |
There was a problem hiding this comment.
Code Review
The pull request refactors the ensure_dir_labeled_recurse function to utilize the cap_std_ext::dirext::walk API with the noxdev option. This change correctly prevents recursive SELinux relabeling from traversing across mount point boundaries, improving robustness, especially when dealing with pseudo-filesystems like sysfs. However, this introduces a significant bug in the directory skipping logic where using ControlFlow::Break will cause the entire walk to terminate prematurely, potentially leaving many files unlabeled. Additionally, the new implementation follows symlinks at the top level, which is a change in behavior from the previous manual recursion and could lead to unintended relabeling.
| if component.file_type.is_dir() { | ||
| return Ok(ControlFlow::Break(())); | ||
| } else { | ||
| return Ok(ControlFlow::Continue(())); | ||
| } |
There was a problem hiding this comment.
In the new implementation of ensure_dir_labeled_recurse, the skip logic for directories returns ControlFlow::Break(()). In the cap_std_ext walk API (and standard Rust ControlFlow usage), Break signals the operation to terminate early. This means that if a directory matches the skip criteria, the entire walk will stop, and all subsequent files and directories will be skipped. This is a regression from the previous manual recursion which correctly continued with siblings. This can lead to a significant portion of the filesystem being left with incorrect SELinux labels if a skipped directory is encountered early in the walk.
To skip a directory and its children while continuing with siblings, the walk API's iterator should be used directly, allowing the use of the skip_current_dir() method. Alternatively, if the walk method with a closure is used, it must be ensured that the return value correctly reflects the intended behavior (though the current cap_std_ext API may not support skipping a directory via the closure's return value without stopping the walk).
There was a problem hiding this comment.
Yeah it's still confused but by my reading of the walk API this is correct now.
| .noxdev() | ||
| .path_base(path_for_read.as_std_path()); | ||
|
|
||
| root.open_dir(path_for_read)? |
There was a problem hiding this comment.
The function now uses root.open_dir(path_for_read)? to begin the directory walk. In cap-std, open_dir follows symlinks by default. The previous implementation used symlink_metadata and manual recursion, which did not follow symlinks to directories. This change means that if the starting path is a symlink to a directory, bootc will now follow it and relabel the contents of the target directory. This could lead to unintended relabeling of files outside the intended scope if the path is a symlink.
Check if the starting path is a symlink before opening it with open_dir, or use an opening method that does not follow symlinks if the intention is to maintain the previous behavior of not recursing into symlinked directories.
There was a problem hiding this comment.
This also appears to be AI incorrectness
🤨 |
Rewrite ensure_dir_labeled_recurse to use the cap_std_ext walk API
with noxdev 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 jeckersb@redhat.com