@@ -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.
801801const 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 ) ]
804812static 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) ]
850899mod 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