1
0
mirror of https://github.com/containers/bootc.git synced 2026-02-05 06:45:13 +01:00

lints: Add nonempty-run-tmp warning for runtime-only directories

Add a lint that warns when /run or /tmp contain any content. These
directories are tmpfs at runtime and should be empty in container images.

Common causes of content in these directories include:
- podman/buildah's RUN --mount leaving directory stubs
- Build tools leaving temporary files

This is particularly important for bootc with composefs because content
in these directories can cause digest mismatches between build-time
(mounted filesystem) and install-time (OCI tar layers) views, leading
to sealed boot failures.

The lint uses the walk API with noxdev() to automatically skip mount
points, and filters out content injected by container runtimes
(.containerenv, secrets, packages).

Assisted-by: OpenCode (Opus 4.5)
Signed-off-by: Colin Walters <walters@verbum.org>
This commit is contained in:
Colin Walters
2026-01-14 13:55:50 -05:00
parent 69ec7d6c04
commit eecf5ae15c

View File

@@ -788,6 +788,58 @@ fn check_boot(root: &Dir, config: &LintExecutionConfig) -> LintResult {
format_lint_err_from_items(config, header, items)
}
/// Directories that should be empty in container images.
/// These are tmpfs at runtime and any content is build-time artifacts.
const RUNTIME_ONLY_DIRS: &[&str] = &["run", "tmp"];
#[distributed_slice(LINTS)]
static LINT_RUNTIME_ONLY_DIRS: Lint = Lint::new_warning(
"nonempty-run-tmp",
indoc! { r#"
The `/run` and `/tmp` directories should be empty in container images.
These directories are normally mounted as `tmpfs` at runtime
(masking any content in the underlying image) and any content here is typically build-time
artifacts that serve no purpose in the final image.
"#},
check_runtime_only_dirs,
);
fn check_runtime_only_dirs(root: &Dir, config: &LintExecutionConfig) -> LintResult {
let mut found_content = BTreeSet::new();
for dirname in RUNTIME_ONLY_DIRS {
let Some(d) = root.open_dir_optional(dirname)? else {
continue;
};
d.walk(
&WalkConfiguration::default()
.noxdev()
.path_base(Path::new(dirname)),
|entry| -> std::io::Result<_> {
// Skip mount points (bind mounts, tmpfs, etc.) - these are
// container-runtime injected content like .containerenv
if entry.dir.is_mountpoint(entry.filename)? == Some(true) {
return Ok(ControlFlow::Continue(()));
}
let full_path = Utf8Path::new("/").join(entry.path.to_string_lossy().as_ref());
found_content.insert(full_path);
Ok(ControlFlow::Continue(()))
},
)?;
}
if found_content.is_empty() {
return lint_ok();
}
let header = "Found content in runtime-only directories (/run, /tmp)";
let items = found_content.iter().map(PathQuotedDisplay::new);
format_lint_err_from_items(config, header, items)
}
#[cfg(test)]
mod tests {
use std::sync::LazyLock;
@@ -1004,6 +1056,37 @@ mod tests {
Ok(())
}
#[test]
fn test_runtime_only_dirs() -> Result<()> {
let root = &fixture()?;
let config = &LintExecutionConfig::default();
// Empty directories should pass
root.create_dir_all("run")?;
root.create_dir_all("tmp")?;
check_runtime_only_dirs(root, config).unwrap().unwrap();
// Content in /run should fail
root.create_dir("run/some-mount-stub")?;
let Err(e) = check_runtime_only_dirs(root, config).unwrap() else {
unreachable!()
};
assert!(e.to_string().contains("/run/some-mount-stub"));
root.remove_dir("run/some-mount-stub")?;
check_runtime_only_dirs(root, config).unwrap().unwrap();
// Content in /tmp should fail
root.write("tmp/build-artifact", "some data")?;
let Err(e) = check_runtime_only_dirs(root, config).unwrap() else {
unreachable!()
};
assert!(e.to_string().contains("/tmp/build-artifact"));
root.remove_file("tmp/build-artifact")?;
check_runtime_only_dirs(root, config).unwrap().unwrap();
Ok(())
}
fn run_recursive_lint(
root: &Dir,
f: LintRecursiveFn,