Skip to content

Conversation

rishi-jat
Copy link

@rishi-jat rishi-jat commented Sep 1, 2025

Description

This adds a new container lint (etc-uid-drift) that warns when files or directories under /etc are owned by non-root users or groups that aren’t numerically pinned in systemd-sysusers and aren’t covered by systemd-tmpfiles chown rules. The goal is to help catch ownership that can “drift” across upgrades when /etc persists.

The lint parses usr/lib/sysusers.d to find users/groups with fixed numeric IDs, and scans usr/lib/tmpfiles.d for z/Z entries (chown at boot, either exact or recursive). It walks /etc (skipping symlinks, handling non‑UTF8 paths), and warns on entries where uid/gid is non-root, not pinned via sysusers, and not covered by a tmpfiles chown. For example, if a component uses a floating user but ships a tmpfiles rule like Z /etc/polkit-1/…, the lint will not warn.

This is a warning-only check (can be promoted to fatal with --fatal-warnings) and does not change runtime behavior.

Fixes #1562

@bootc-bot bootc-bot bot requested a review from cgwalters September 1, 2025 03:22
…coverage (z/Z) (bootc-dev#1562)

Signed-off-by: Rishi Jat <rishijat098@gmail.com>
@rishi-jat rishi-jat force-pushed the fix/1562-uid-drift-lint branch from 947bdf2 to 488a48b Compare September 1, 2025 03:23
Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a valuable new lint to detect potential UID/GID drift in /etc, which is a great addition for ensuring system integrity across upgrades. The implementation logic for parsing sysusers.d and tmpfiles.d files and checking file ownership is sound. I've provided a few suggestions for the new helper functions in crates/tmpfiles/src/lib.rs to improve code clarity and maintainability by removing dead code and using more idiomatic Rust. Overall, this is a solid contribution.

Signed-off-by: Rishi Jat <rishijat098@gmail.com>
Copy link
Collaborator

@cgwalters cgwalters left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for working on this!

Offhand it looks plausible but this really does need careful review which I'd like to do once we have non-synthetic tests that are actually doing file chowning.

Comment on lines +817 to +819
// Depth-first walk under /etc
let mut stack: Vec<(Dir, std::path::PathBuf)> = vec![(etcd, std::path::PathBuf::from("/etc"))];
while let Some((dir, abspath)) = stack.pop() {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is probably OK as is but I've been trying to have recursive filesystem walks use https://docs.rs/cap-std-ext/latest/cap_std_ext/dirext/trait.CapStdExtDirExt.html#tymethod.walk

root.create_dir_all("etc/uncovered")?;

let config = &LintExecutionConfig { no_truncate: true };
// Since we cannot alter uid/gid in this test easily, fake non-root by relying on the logic
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right...but we are going to need some "real" tests for this.

pub(crate) fn run_hostpriv(image: &str, testargs: libtest_mimic::Arguments) -> Result<()> {
is one possible place - that test suite is defined to run as root and so it's supported to make tempdirs with arbitrary owned files.

Another (which is perhaps overdue) is one where we have Dockerfile fixtures and we explicitly just build each one and run bootc container lint in there and check the result.

// Only process .conf files
let name = entry.file_name();
let path = Path::new(&name);
if path.extension() != Some(OsStr::new("conf")) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it'd be a lot cleaner to have this run read_tmpfiles() and then just filter the result.

Comment on lines +551 to +553
if kind != 'z' && kind != 'Z' {
continue;
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it'd be cleaner to just fold this into the match

impl TmpfilesChowners {
/// Returns true if a chown entry would apply to the specified absolute path
pub fn covers(&self, p: &Path) -> bool {
self.exact.contains(p) || p.ancestors().any(|anc| self.recursive.contains(anc))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we could have some unit tests for this pretty easily

@cgwalters cgwalters self-assigned this Sep 2, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

lint: Check for potential uid drift for files in /etc
2 participants