Skip to content

Commit 94c58c9

Browse files
committed
lints: Special case /run/systemd/resolve in nonempty-run-tmp
When we were reworking our lint flow to try to have an empty `/run` for enhanced composefs compatibility, I added a hack into our own build that dealt with `/run/systemd/resolve` which is injected by buildah. As of relatively recently buildah will clean that up, but we'd still warn about it. Since there's no real easy way for users to deal with this, just skip warning about it. Closes: #2050 Assisted-by: OpenCode (Claude claude-opus-4-6) Signed-off-by: Colin Walters <walters@verbum.org>
1 parent 86ba489 commit 94c58c9

2 files changed

Lines changed: 107 additions & 16 deletions

File tree

Dockerfile

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -216,9 +216,4 @@ fi
216216
EORUN
217217
# And finally, test our linting
218218
# lint: allow non-tmpfs - we want to detect leaked files in /run and /tmp
219-
RUN --network=none <<EORUN
220-
set -xeuo pipefail
221-
# workaround for https://github.com/containers/buildah/pull/6233
222-
rm -vrf /run/systemd
223-
bootc container lint --fatal-warnings
224-
EORUN
219+
RUN --network=none bootc container lint --fatal-warnings

crates/lib/src/lints.rs

Lines changed: 106 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -800,6 +800,14 @@ fn check_boot(root: &Dir, config: &LintExecutionConfig) -> LintResult {
800800
/// These are tmpfs at runtime and any content is build-time artifacts.
801801
const RUNTIME_ONLY_DIRS: &[&str] = &["run", "tmp"];
802802

803+
/// Files injected by container runtimes (podman/buildah) that should be
804+
/// ignored when linting. Podman bind-mounts stub-resolv.conf for DNS
805+
/// resolution, creating parent directories as a side effect.
806+
/// As of only recently, buildah will clean this up.
807+
/// See <https://github.com/containers/buildah/pull/6233>
808+
/// See <https://github.com/bootc-dev/bootc/issues/2050>
809+
const CONTAINER_RUNTIME_FILES: &[&str] = &["/run/systemd/resolve/stub-resolv.conf"];
810+
803811
#[distributed_slice(LINTS)]
804812
static LINT_RUNTIME_ONLY_DIRS: Lint = Lint::new_warning(
805813
"nonempty-run-tmp",
@@ -816,7 +824,11 @@ fn check_runtime_only_dirs(root: &Dir, config: &LintExecutionConfig) -> LintResu
816824
let mut found_content = BTreeSet::new();
817825

818826
for dirname in RUNTIME_ONLY_DIRS {
819-
let Some(d) = root.open_dir_optional(dirname)? else {
827+
// Use open_dir_noxdev so that if the directory is a mount point
828+
// (e.g. the user passed --mount=type=tmpfs,target=/run) we skip
829+
// it — content on a different filesystem is ephemeral and won't
830+
// end up in the image.
831+
let Some(d) = root.open_dir_noxdev(dirname)? else {
820832
continue;
821833
};
822834

@@ -837,6 +849,11 @@ fn check_runtime_only_dirs(root: &Dir, config: &LintExecutionConfig) -> LintResu
837849
)?;
838850
}
839851

852+
// Remove known container-runtime injected paths under /run
853+
// (e.g. stub-resolv.conf and its parent directories if they have
854+
// no other children).
855+
prune_known_run_paths(&mut found_content);
856+
840857
if found_content.is_empty() {
841858
return lint_ok();
842859
}
@@ -846,6 +863,38 @@ fn check_runtime_only_dirs(root: &Dir, config: &LintExecutionConfig) -> LintResu
846863
format_lint_err_from_items(config, header, items)
847864
}
848865

866+
/// Remove known container-runtime injected paths from `/run`.
867+
///
868+
/// Podman/buildah inject files like `/run/systemd/resolve/stub-resolv.conf`
869+
/// for DNS resolution, creating the parent directory tree as a side effect.
870+
/// The file itself is usually already filtered out (it's a bind mount detected
871+
/// by `is_mountpoint()`), but the parent directories `/run/systemd` and
872+
/// `/run/systemd/resolve` remain.
873+
///
874+
/// Remove the known file if present, then walk up removing each parent
875+
/// under `/run` that has no remaining children in the set.
876+
fn prune_known_run_paths(paths: &mut BTreeSet<Utf8PathBuf>) {
877+
let run_prefix = Utf8Path::new("/run");
878+
for known in CONTAINER_RUNTIME_FILES {
879+
let known = Utf8Path::new(known);
880+
paths.remove(known);
881+
// Walk up the parent directories, removing each one only if
882+
// nothing else in the set is underneath it.
883+
let mut dir = known.parent();
884+
while let Some(d) = dir {
885+
if !d.starts_with(run_prefix) || d == run_prefix {
886+
break;
887+
}
888+
let has_other_children = paths.iter().any(|p| p != d && p.starts_with(d));
889+
if has_other_children {
890+
break;
891+
}
892+
paths.remove(d);
893+
dir = d.parent();
894+
}
895+
}
896+
}
897+
849898
#[cfg(test)]
850899
mod tests {
851900
use std::sync::LazyLock;
@@ -1067,27 +1116,74 @@ mod tests {
10671116
let root = &fixture()?;
10681117
let config = &LintExecutionConfig::default();
10691118

1070-
// Empty directories should pass
10711119
root.create_dir_all("run")?;
10721120
root.create_dir_all("tmp")?;
1121+
1122+
// Simulate the exact scenario from https://github.com/bootc-dev/bootc/issues/2050:
1123+
// podman creates /run/systemd/resolve/stub-resolv.conf as a bind mount
1124+
// for DNS, leaving the directory tree behind. When /run/systemd/resolve
1125+
// only contains the known runtime file (or is empty because the file is
1126+
// a mount point that was filtered), the whole tree should be pruned.
1127+
root.create_dir_all("run/systemd/resolve")?;
10731128
check_runtime_only_dirs(root, config).unwrap().unwrap();
10741129

1075-
// Content in /run should fail
1076-
root.create_dir("run/some-mount-stub")?;
1130+
// Same but with the stub-resolv.conf file actually present
1131+
root.write("run/systemd/resolve/stub-resolv.conf", "data")?;
1132+
check_runtime_only_dirs(root, config).unwrap().unwrap();
1133+
root.remove_file("run/systemd/resolve/stub-resolv.conf")?;
1134+
1135+
// If there's *other* content under /run/systemd, we should still warn
1136+
// about it (the pruning only removes dirs that are solely parents of
1137+
// known runtime files).
1138+
root.write("run/systemd/resolve/something-else", "data")?;
10771139
let Err(e) = check_runtime_only_dirs(root, config).unwrap() else {
1078-
unreachable!()
1140+
unreachable!("expected warning for unknown file under /run/systemd")
10791141
};
1080-
assert!(e.to_string().contains("/run/some-mount-stub"));
1081-
root.remove_dir("run/some-mount-stub")?;
1082-
check_runtime_only_dirs(root, config).unwrap().unwrap();
1142+
let msg = e.to_string();
1143+
assert!(
1144+
msg.contains("/run/systemd/resolve/something-else"),
1145+
"should warn about the unknown file: {msg}"
1146+
);
1147+
// The parent dirs should still appear since they have real children
1148+
assert!(
1149+
msg.contains("/run/systemd"),
1150+
"parent dirs with real children should appear: {msg}"
1151+
);
1152+
root.remove_file("run/systemd/resolve/something-else")?;
10831153

1084-
// Content in /tmp should fail
1154+
// Unknown directories should still warn
1155+
root.create_dir("run/dnf")?;
1156+
let Err(e) = check_runtime_only_dirs(root, config).unwrap() else {
1157+
unreachable!("expected warning for /run/dnf")
1158+
};
1159+
let msg = e.to_string();
1160+
assert!(
1161+
msg.contains("/run/dnf"),
1162+
"should warn about /run/dnf: {msg}"
1163+
);
1164+
assert!(
1165+
!msg.contains("/run/systemd"),
1166+
"should not mention /run/systemd: {msg}"
1167+
);
1168+
root.remove_dir("run/dnf")?;
1169+
1170+
// Files in /run should warn
1171+
root.write("run/leaked-file", "data")?;
1172+
let Err(e) = check_runtime_only_dirs(root, config).unwrap() else {
1173+
unreachable!("expected warning for /run/leaked-file")
1174+
};
1175+
assert!(e.to_string().contains("/run/leaked-file"));
1176+
root.remove_file("run/leaked-file")?;
1177+
1178+
// Files in /tmp should warn
10851179
root.write("tmp/build-artifact", "some data")?;
10861180
let Err(e) = check_runtime_only_dirs(root, config).unwrap() else {
1087-
unreachable!()
1181+
unreachable!("expected warning for /tmp/build-artifact")
10881182
};
10891183
assert!(e.to_string().contains("/tmp/build-artifact"));
10901184
root.remove_file("tmp/build-artifact")?;
1185+
1186+
// Clean state should pass
10911187
check_runtime_only_dirs(root, config).unwrap().unwrap();
10921188

10931189
Ok(())

0 commit comments

Comments
 (0)