1#![allow(unsafe_code)]
7
8use std::collections::{BTreeMap, BTreeSet};
9use std::env::consts::ARCH;
10use std::fmt::{Display, Write as WriteFmt};
11use std::num::NonZeroUsize;
12use std::ops::ControlFlow;
13use std::os::unix::ffi::OsStrExt;
14use std::path::Path;
15
16use anyhow::Result;
17use bootc_utils::PathQuotedDisplay;
18use camino::{Utf8Path, Utf8PathBuf};
19use cap_std::fs::Dir;
20use cap_std_ext::cap_std;
21use cap_std_ext::cap_std::fs::MetadataExt;
22use cap_std_ext::dirext::WalkConfiguration;
23use cap_std_ext::dirext::{CapStdExtDirExt as _, WalkComponent};
24use fn_error_context::context;
25use indoc::indoc;
26use linkme::distributed_slice;
27use ostree_ext::ostree_prepareroot;
28use serde::Serialize;
29
30use crate::bootc_composefs::boot::EFI_LINUX;
31use crate::bootc_composefs::image::is_composefs_native;
32
33fn walk_configuration() -> WalkConfiguration<'static> {
40 WalkConfiguration::default().noxdev()
41}
42
43const BASEIMAGE_REF: &str = "usr/share/doc/bootc/baseimage/base";
45const API_DIRS: &[&str] = &["dev", "proc", "sys", "run", "tmp", "var"];
47
48const DEFAULT_TRUNCATED_OUTPUT: NonZeroUsize = const { NonZeroUsize::new(5).unwrap() };
50
51#[derive(thiserror::Error, Debug)]
53struct LintError(String);
54
55type LintResult = Result<std::result::Result<(), LintError>>;
58
59fn lint_ok() -> LintResult {
62 Ok(Ok(()))
63}
64
65fn lint_err(msg: impl AsRef<str>) -> LintResult {
67 Ok(Err(LintError::new(msg)))
68}
69
70impl std::fmt::Display for LintError {
71 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
72 f.write_str(&self.0)
73 }
74}
75
76impl LintError {
77 fn new(msg: impl AsRef<str>) -> Self {
78 Self(msg.as_ref().to_owned())
79 }
80}
81
82#[derive(Debug, Default)]
83struct LintExecutionConfig {
84 no_truncate: bool,
85}
86
87type LintFn = fn(&Dir, config: &LintExecutionConfig) -> LintResult;
88type LintRecursiveResult = LintResult;
89type LintRecursiveFn = fn(&WalkComponent, config: &LintExecutionConfig) -> LintRecursiveResult;
90#[derive(Debug)]
93enum LintFnTy {
94 Regular(LintFn),
96 Recursive(LintRecursiveFn),
98}
99#[distributed_slice]
100pub(crate) static LINTS: [Lint];
101
102#[derive(Debug, Serialize)]
104#[serde(rename_all = "kebab-case")]
105enum LintType {
106 Fatal,
109 Warning,
111}
112
113#[derive(Debug, Copy, Clone)]
114pub(crate) enum WarningDisposition {
115 AllowWarnings,
116 FatalWarnings,
117}
118
119#[derive(Debug, Copy, Clone, Serialize, PartialEq, Eq)]
120pub(crate) enum RootType {
121 Running,
122 Alternative,
123}
124
125#[derive(Debug, Serialize)]
126#[serde(rename_all = "kebab-case")]
127struct Lint {
128 name: &'static str,
129 #[serde(rename = "type")]
130 ty: LintType,
131 #[serde(skip)]
132 f: LintFnTy,
133 description: &'static str,
134 #[serde(skip_serializing_if = "Option::is_none")]
136 root_type: Option<RootType>,
137}
138
139impl PartialEq for Lint {
141 fn eq(&self, other: &Self) -> bool {
142 self.name == other.name
143 }
144}
145impl Eq for Lint {}
146
147impl std::hash::Hash for Lint {
148 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
149 self.name.hash(state);
150 }
151}
152
153impl PartialOrd for Lint {
154 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
155 Some(self.cmp(other))
156 }
157}
158impl Ord for Lint {
159 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
160 self.name.cmp(other.name)
161 }
162}
163
164impl Lint {
165 pub(crate) const fn new_fatal(
166 name: &'static str,
167 description: &'static str,
168 f: LintFn,
169 ) -> Self {
170 Lint {
171 name,
172 ty: LintType::Fatal,
173 f: LintFnTy::Regular(f),
174 description,
175 root_type: None,
176 }
177 }
178
179 pub(crate) const fn new_warning(
180 name: &'static str,
181 description: &'static str,
182 f: LintFn,
183 ) -> Self {
184 Lint {
185 name,
186 ty: LintType::Warning,
187 f: LintFnTy::Regular(f),
188 description,
189 root_type: None,
190 }
191 }
192
193 const fn set_root_type(mut self, v: RootType) -> Self {
194 self.root_type = Some(v);
195 self
196 }
197}
198
199pub(crate) fn lint_list(output: impl std::io::Write) -> Result<()> {
200 serde_yaml::to_writer(output, &*LINTS)?;
202 Ok(())
203}
204
205#[derive(Debug)]
206struct LintExecutionResult {
207 warnings: usize,
208 passed: usize,
209 skipped: usize,
210 fatal: usize,
211}
212
213fn format_items<T>(
215 config: &LintExecutionConfig,
216 header: &str,
217 items: impl Iterator<Item = T>,
218 o: &mut String,
219) -> Result<()>
220where
221 T: Display,
222{
223 let mut items = items.into_iter();
224 if config.no_truncate {
225 let Some(first) = items.next() else {
226 return Ok(());
227 };
228 writeln!(o, "{header}:")?;
229 writeln!(o, " {first}")?;
230 for item in items {
231 writeln!(o, " {item}")?;
232 }
233 return Ok(());
234 } else {
235 let Some((samples, rest)) = bootc_utils::collect_until(items, DEFAULT_TRUNCATED_OUTPUT)
236 else {
237 return Ok(());
238 };
239 writeln!(o, "{header}:")?;
240 for item in samples {
241 writeln!(o, " {item}")?;
242 }
243 if rest > 0 {
244 writeln!(o, " ...and {rest} more")?;
245 }
246 }
247 Ok(())
248}
249
250fn format_lint_err_from_items<T>(
254 config: &LintExecutionConfig,
255 header: &str,
256 items: impl Iterator<Item = T>,
257) -> LintResult
258where
259 T: Display,
260{
261 let mut msg = String::new();
262 format_items(config, header, items, &mut msg).unwrap();
264 lint_err(msg)
265}
266
267fn lint_inner<'skip>(
268 root: &Dir,
269 root_type: RootType,
270 config: &LintExecutionConfig,
271 skip: impl IntoIterator<Item = &'skip str>,
272 mut output: impl std::io::Write,
273) -> Result<LintExecutionResult> {
274 let mut fatal = 0usize;
275 let mut warnings = 0usize;
276 let mut passed = 0usize;
277 let skip: std::collections::HashSet<_> = skip.into_iter().collect();
278 let (mut applicable_lints, skipped_lints): (Vec<_>, Vec<_>) = LINTS.iter().partition(|lint| {
279 if skip.contains(lint.name) {
280 return false;
281 }
282 if let Some(lint_root_type) = lint.root_type {
283 if lint_root_type != root_type {
284 return false;
285 }
286 }
287 true
288 });
289 let skipped = skipped_lints.len();
291 applicable_lints.sort_by(|a, b| a.name.cmp(b.name));
293 let (nonrec_lints, recursive_lints): (Vec<_>, Vec<_>) = applicable_lints
295 .into_iter()
296 .partition(|lint| matches!(lint.f, LintFnTy::Regular(_)));
297 let mut results = Vec::new();
298 for lint in nonrec_lints {
299 let f = match lint.f {
300 LintFnTy::Regular(f) => f,
301 LintFnTy::Recursive(_) => unreachable!(),
302 };
303 results.push((lint, f(&root, &config)));
304 }
305
306 let mut recursive_lints = BTreeSet::from_iter(recursive_lints);
307 let mut recursive_errors = BTreeMap::new();
308 root.walk(
309 &walk_configuration().path_base(Path::new("/")),
310 |e| -> std::io::Result<_> {
311 if recursive_lints.is_empty() {
313 return Ok(ControlFlow::Break(()));
314 }
315 let mut this_iteration_errors = Vec::new();
318 for &lint in recursive_lints.iter() {
320 let f = match &lint.f {
321 LintFnTy::Regular(_) => unreachable!(),
323 LintFnTy::Recursive(f) => f,
324 };
325 match f(e, &config) {
327 Ok(Ok(())) => {}
328 o => this_iteration_errors.push((lint, o)),
329 }
330 }
331 for (lint, err) in this_iteration_errors {
334 recursive_lints.remove(lint);
335 recursive_errors.insert(lint, err);
336 }
337 Ok(ControlFlow::Continue(()))
338 },
339 )?;
340 results.extend(recursive_errors);
342 results.extend(recursive_lints.into_iter().map(|lint| (lint, lint_ok())));
344 for (lint, r) in results {
345 let name = lint.name;
346 let r = match r {
347 Ok(r) => r,
348 Err(e) => {
349 return Err(e.context(format!("Unexpected runtime error running lint {name}")));
350 }
351 };
352
353 if let Err(e) = r {
354 match lint.ty {
355 LintType::Fatal => {
356 writeln!(output, "Failed lint: {name}: {e}")?;
357 fatal += 1;
358 }
359 LintType::Warning => {
360 writeln!(output, "Lint warning: {name}: {e}")?;
361 warnings += 1;
362 }
363 }
364 } else {
365 tracing::debug!("OK {name} (type={:?})", lint.ty);
367 passed += 1;
368 }
369 }
370
371 Ok(LintExecutionResult {
372 passed,
373 skipped,
374 warnings,
375 fatal,
376 })
377}
378
379#[context("Linting")]
380pub(crate) fn lint<'skip>(
381 root: &Dir,
382 warning_disposition: WarningDisposition,
383 root_type: RootType,
384 skip: impl IntoIterator<Item = &'skip str>,
385 mut output: impl std::io::Write,
386 no_truncate: bool,
387) -> Result<()> {
388 let config = LintExecutionConfig { no_truncate };
389 let r = lint_inner(root, root_type, &config, skip, &mut output)?;
390 writeln!(output, "Checks passed: {}", r.passed)?;
391 if r.skipped > 0 {
392 writeln!(output, "Checks skipped: {}", r.skipped)?;
393 }
394 let fatal = if matches!(warning_disposition, WarningDisposition::FatalWarnings) {
395 r.fatal + r.warnings
396 } else {
397 r.fatal
398 };
399 if r.warnings > 0 {
400 writeln!(output, "Warnings: {}", r.warnings)?;
401 }
402 if fatal > 0 {
403 anyhow::bail!("Checks failed: {}", fatal)
404 }
405 Ok(())
406}
407
408#[distributed_slice(LINTS)]
411static LINT_VAR_RUN: Lint = Lint::new_fatal(
412 "var-run",
413 "Check for /var/run being a physical directory; this is always a bug.",
414 check_var_run,
415);
416fn check_var_run(root: &Dir, _config: &LintExecutionConfig) -> LintResult {
417 if let Some(meta) = root.symlink_metadata_optional("var/run")? {
418 if !meta.is_symlink() {
419 return lint_err("Not a symlink: var/run");
420 }
421 }
422 lint_ok()
423}
424
425#[distributed_slice(LINTS)]
426static LINT_BUILDAH_INJECTED: Lint = Lint::new_warning(
427 "buildah-injected",
428 indoc::indoc! { "
429 Check for an invalid /etc/hostname or /etc/resolv.conf that may have been injected by
430 a container build system." },
431 check_buildah_injected,
432)
433.set_root_type(RootType::Alternative);
436fn check_buildah_injected(root: &Dir, _config: &LintExecutionConfig) -> LintResult {
437 const RUNTIME_INJECTED: &[&str] = &["etc/hostname", "etc/resolv.conf"];
438 for ent in RUNTIME_INJECTED {
439 if let Some(meta) = root.symlink_metadata_optional(ent)? {
440 if meta.is_file() && meta.size() == 0 {
441 return lint_err(format!(
442 "/{ent} is an empty file; this may have been synthesized by a container runtime."
443 ));
444 }
445 }
446 }
447 lint_ok()
448}
449
450#[distributed_slice(LINTS)]
451static LINT_ETC_USRUSETC: Lint = Lint::new_fatal(
452 "etc-usretc",
453 indoc! { r#"
454Verify that only one of /etc or /usr/etc exist. You should only have /etc
455in a container image. It will cause undefined behavior to have both /etc
456and /usr/etc.
457"# },
458 check_usretc,
459);
460fn check_usretc(root: &Dir, _config: &LintExecutionConfig) -> LintResult {
461 let etc_exists = root.symlink_metadata_optional("etc")?.is_some();
462 if !etc_exists {
464 return lint_ok();
465 }
466 if root.symlink_metadata_optional("usr/etc")?.is_some() {
468 return lint_err(
469 "Found /usr/etc - this is a bootc implementation detail and not supported to use in containers",
470 );
471 }
472 lint_ok()
473}
474
475#[distributed_slice(LINTS)]
477static LINT_KARGS: Lint = Lint::new_fatal(
478 "bootc-kargs",
479 "Verify syntax of /usr/lib/bootc/kargs.d.",
480 check_parse_kargs,
481);
482fn check_parse_kargs(root: &Dir, _config: &LintExecutionConfig) -> LintResult {
483 let args = crate::bootc_kargs::get_kargs_in_root(root, ARCH)?;
484 tracing::debug!("found kargs: {args:?}");
485 lint_ok()
486}
487
488#[distributed_slice(LINTS)]
489static LINT_KERNEL: Lint = Lint::new_fatal(
490 "kernel",
491 indoc! { r#"
492 Check for multiple kernels, i.e. multiple directories of the form /usr/lib/modules/$kver.
493 Only one kernel is supported in an image.
494 "# },
495 check_kernel,
496);
497fn check_kernel(root: &Dir, _config: &LintExecutionConfig) -> LintResult {
498 let result = ostree_ext::bootabletree::find_kernel_dir_fs(&root)?;
499 tracing::debug!("Found kernel: {:?}", result);
500 lint_ok()
501}
502
503#[distributed_slice(LINTS)]
505static LINT_UTF8: Lint = Lint {
506 name: "utf8",
507 description: indoc! { r#"
508Check for non-UTF8 filenames. Currently, the ostree backend of bootc only supports
509UTF-8 filenames. Non-UTF8 filenames will cause a fatal error.
510"#},
511 ty: LintType::Fatal,
512 root_type: None,
513 f: LintFnTy::Recursive(check_utf8),
514};
515fn check_utf8(e: &WalkComponent, _config: &LintExecutionConfig) -> LintRecursiveResult {
516 let path = e.path;
517 let filename = e.filename;
518 let dirname = path.parent().unwrap_or(Path::new("/"));
519 if filename.to_str().is_none() {
520 return lint_err(format!(
522 "{}: Found non-utf8 filename {filename:?}",
523 PathQuotedDisplay::new(&dirname)
524 ));
525 };
526
527 if e.file_type.is_symlink() {
528 let target = e.dir.read_link_contents(filename)?;
529 if target.to_str().is_none() {
530 return lint_err(format!(
531 "{}: Found non-utf8 symlink target",
532 PathQuotedDisplay::new(&path)
533 ));
534 }
535 }
536 lint_ok()
537}
538
539fn check_prepareroot_composefs_norecurse(dir: &Dir) -> LintResult {
540 let path = ostree_ext::ostree_prepareroot::CONF_PATH;
541 let Some(config) = ostree_prepareroot::load_config_from_root(dir)? else {
542 return lint_err(format!("{path} is not present to enable composefs"));
543 };
544 if !ostree_prepareroot::overlayfs_enabled_in_config(&config)? {
545 return lint_err(format!("{path} does not have composefs enabled"));
546 }
547 lint_ok()
548}
549
550#[distributed_slice(LINTS)]
551static LINT_API_DIRS: Lint = Lint::new_fatal(
552 "api-base-directories",
553 indoc! { r#"
554Verify that expected base API directories exist. For more information
555on these, see <https://systemd.io/API_FILE_SYSTEMS/>.
556
557Note that in addition, bootc requires that `/var` exist as a directory.
558"#},
559 check_api_dirs,
560);
561fn check_api_dirs(root: &Dir, _config: &LintExecutionConfig) -> LintResult {
562 for d in API_DIRS {
563 let Some(meta) = root.symlink_metadata_optional(d)? else {
564 return lint_err(format!("Missing API filesystem base directory: /{d}"));
565 };
566 if !meta.is_dir() {
567 return lint_err(format!(
568 "Expected directory for API filesystem base directory: /{d}"
569 ));
570 }
571 }
572 lint_ok()
573}
574
575#[distributed_slice(LINTS)]
576static LINT_COMPOSEFS: Lint = Lint::new_warning(
577 "baseimage-composefs",
578 indoc! { r#"
579Check that composefs is enabled for ostree. Skipped for composefs-native
580images, i.e. those that ship /usr/lib/composefs/setup-root-conf.toml. More in
581<https://ostreedev.github.io/ostree/composefs/>.
582"#},
583 check_composefs,
584);
585fn check_composefs(dir: &Dir, _config: &LintExecutionConfig) -> LintResult {
586 if is_composefs_native(dir)? {
588 return lint_ok();
589 }
590 if let Err(e) = check_prepareroot_composefs_norecurse(dir)? {
591 return Ok(Err(e));
592 }
593 if let Some(dir) = dir.open_dir_optional(BASEIMAGE_REF)? {
596 if let Err(e) = check_prepareroot_composefs_norecurse(&dir)? {
597 return Ok(Err(e));
598 }
599 }
600 lint_ok()
601}
602
603fn check_baseimage_root_norecurse(dir: &Dir, _config: &LintExecutionConfig) -> LintResult {
605 let meta = dir.symlink_metadata_optional("sysroot")?;
607 match meta {
608 Some(meta) if !meta.is_dir() => return lint_err("Expected a directory for /sysroot"),
609 None => return lint_err("Missing /sysroot"),
610 _ => {}
611 }
612
613 let Some(meta) = dir.symlink_metadata_optional("ostree")? else {
615 if is_composefs_native(dir)? {
617 return lint_ok();
618 }
619 return lint_err("Missing ostree -> sysroot/ostree link");
620 };
621 if !meta.is_symlink() {
622 return lint_err("/ostree should be a symlink");
623 }
624 let link = dir.read_link_contents("ostree")?;
625 let expected = "sysroot/ostree";
626 if link.as_os_str().as_bytes() != expected.as_bytes() {
627 return lint_err(format!("Expected /ostree -> {expected}, not {link:?}"));
628 }
629
630 lint_ok()
631}
632
633#[distributed_slice(LINTS)]
635static LINT_BASEIMAGE_ROOT: Lint = Lint::new_fatal(
636 "baseimage-root",
637 indoc! { r#"
638Check that expected files are present in the root of the filesystem; such
639as /sysroot and a composefs configuration for ostree. The /ostree symlink
640is not required for composefs-native images, i.e. those that ship
641/usr/lib/composefs/setup-root-conf.toml. More in
642<https://bootc.dev/bootc/bootc-images.html#standard-image-content>.
643"#},
644 check_baseimage_root,
645);
646fn check_baseimage_root(dir: &Dir, config: &LintExecutionConfig) -> LintResult {
647 if let Err(e) = check_baseimage_root_norecurse(dir, config)? {
648 return Ok(Err(e));
649 }
650 if let Some(dir) = dir.open_dir_optional(BASEIMAGE_REF)? {
653 if let Err(e) = check_baseimage_root_norecurse(&dir, config)? {
654 return Ok(Err(e));
655 }
656 }
657 lint_ok()
658}
659
660fn collect_nonempty_regfiles(
661 root: &Dir,
662 path: &Utf8Path,
663 out: &mut BTreeSet<Utf8PathBuf>,
664) -> Result<()> {
665 for entry in root.entries_utf8()? {
666 let entry = entry?;
667 let ty = entry.file_type()?;
668 let path = path.join(entry.file_name()?);
669 if ty.is_file() {
670 let meta = entry.metadata()?;
671 if meta.size() > 0 {
672 out.insert(path);
673 }
674 } else if ty.is_dir() {
675 let d = entry.open_dir()?;
676 collect_nonempty_regfiles(d.as_cap_std(), &path, out)?;
677 }
678 }
679 Ok(())
680}
681
682#[distributed_slice(LINTS)]
683static LINT_VARLOG: Lint = Lint::new_warning(
684 "var-log",
685 indoc! { r#"
686Check for non-empty regular files in `/var/log`. It is often undesired
687to ship log files in container images. Log files in general are usually
688per-machine state in `/var`. Additionally, log files often include
689timestamps, causing unreproducible container images, and may contain
690sensitive build system information.
691"#},
692 check_varlog,
693);
694fn check_varlog(root: &Dir, config: &LintExecutionConfig) -> LintResult {
695 let Some(d) = root.open_dir_optional("var/log")? else {
696 return lint_ok();
697 };
698 let mut nonempty_regfiles = BTreeSet::new();
699 collect_nonempty_regfiles(&d, "/var/log".into(), &mut nonempty_regfiles)?;
700
701 if nonempty_regfiles.is_empty() {
702 return lint_ok();
703 }
704
705 let header = "Found non-empty logfiles";
706 let items = nonempty_regfiles.iter().map(PathQuotedDisplay::new);
707 format_lint_err_from_items(config, header, items)
708}
709
710#[distributed_slice(LINTS)]
711static LINT_VAR_TMPFILES: Lint = Lint::new_warning(
712 "var-tmpfiles",
713 indoc! { r#"
714Check for content in /var that does not have corresponding systemd tmpfiles.d entries.
715This can cause a problem across upgrades because content in /var from the container
716image will only be applied on the initial provisioning.
717
718Instead, it's recommended to have /var effectively empty in the container image,
719and use systemd tmpfiles.d to generate empty directories and compatibility symbolic links
720as part of each boot.
721"#},
722 check_var_tmpfiles,
723)
724.set_root_type(RootType::Running);
725
726fn check_var_tmpfiles(_root: &Dir, config: &LintExecutionConfig) -> LintResult {
727 let r = bootc_tmpfiles::find_missing_tmpfiles_current_root()?;
728 if r.tmpfiles.is_empty() && r.unsupported.is_empty() {
729 return lint_ok();
730 }
731 let mut msg = String::new();
732 let header = "Found content in /var missing systemd tmpfiles.d entries";
733 format_items(config, header, r.tmpfiles.iter().map(|v| v as &_), &mut msg)?;
734 let header = "Found non-directory/non-symlink files in /var";
735 let items = r.unsupported.iter().map(PathQuotedDisplay::new);
736 format_items(config, header, items, &mut msg)?;
737 lint_err(msg)
738}
739
740#[distributed_slice(LINTS)]
741static LINT_SYSUSERS: Lint = Lint::new_warning(
742 "sysusers",
743 indoc! { r#"
744Check for users in /etc/passwd and groups in /etc/group that do not have corresponding
745systemd sysusers.d entries in /usr/lib/sysusers.d.
746This can cause a problem across upgrades because if /etc is not transient and is locally
747modified (commonly due to local user additions), then the contents of /etc/passwd in the new container
748image may not be visible.
749
750Using systemd-sysusers to allocate users and groups will ensure that these are allocated
751on system startup alongside other users.
752
753More on this topic in <https://bootc.dev/bootc/building/users-and-groups.html>
754"# },
755 check_sysusers,
756);
757fn check_sysusers(rootfs: &Dir, config: &LintExecutionConfig) -> LintResult {
758 let r = bootc_sysusers::analyze(rootfs)?;
759 if r.is_empty() {
760 return lint_ok();
761 }
762 let mut msg = String::new();
763 let header = "Found /etc/passwd entry without corresponding systemd sysusers.d";
764 let items = r.missing_users.iter().map(|v| v as &dyn std::fmt::Display);
765 format_items(config, header, items, &mut msg)?;
766 let header = "Found /etc/group entry without corresponding systemd sysusers.d";
767 format_items(config, header, r.missing_groups.into_iter(), &mut msg)?;
768 lint_err(msg)
769}
770
771#[distributed_slice(LINTS)]
772static LINT_NONEMPTY_BOOT: Lint = Lint::new_warning(
773 "nonempty-boot",
774 indoc! { r#"
775The `/boot` directory should be present, but empty. The kernel
776content should be in /usr/lib/modules instead in the container image.
777Any content here in the container image will be masked at runtime.
778"#},
779 check_boot,
780);
781fn check_boot(root: &Dir, config: &LintExecutionConfig) -> LintResult {
782 let Some(d) = root.open_dir_optional(crate::install::BOOT)? else {
783 return lint_err("Missing /boot directory");
784 };
785
786 let entries: Result<BTreeSet<_>, _> = d
788 .entries()?
789 .into_iter()
790 .map(|v| {
791 let v = v?;
792 anyhow::Ok(v.file_name())
793 })
794 .collect();
795 let mut entries = entries?;
796 {
797 let efidir = Utf8Path::new(EFI_LINUX)
799 .parent()
800 .map(|b| b.as_std_path())
801 .unwrap();
802 entries.remove(efidir.as_os_str());
803 }
804 if entries.is_empty() {
805 return lint_ok();
806 }
807
808 let header = "Found non-empty /boot";
809 let items = entries.iter().map(PathQuotedDisplay::new);
810 format_lint_err_from_items(config, header, items)
811}
812
813const RUNTIME_ONLY_DIRS: &[&str] = &["run", "tmp"];
816
817const CONTAINER_RUNTIME_FILES: &[&str] = &["/run/systemd/resolve/stub-resolv.conf"];
824
825#[distributed_slice(LINTS)]
826static LINT_RUNTIME_ONLY_DIRS: Lint = Lint::new_warning(
827 "nonempty-run-tmp",
828 indoc! { r#"
829The `/run` and `/tmp` directories should be empty in container images.
830These directories are normally mounted as `tmpfs` at runtime
831(masking any content in the underlying image) and any content here is typically build-time
832artifacts that serve no purpose in the final image.
833"#},
834 check_runtime_only_dirs,
835);
836
837fn check_runtime_only_dirs(root: &Dir, config: &LintExecutionConfig) -> LintResult {
838 let mut found_content = BTreeSet::new();
839
840 for dirname in RUNTIME_ONLY_DIRS {
841 let Some(d) = root.open_dir_noxdev(dirname)? else {
846 continue;
847 };
848
849 d.walk(
850 &walk_configuration().path_base(Path::new(dirname)),
851 |entry| -> std::io::Result<_> {
852 if entry.dir.is_mountpoint(entry.filename)? == Some(true) {
855 return Ok(ControlFlow::Continue(()));
856 }
857
858 let full_path = Utf8Path::new("/").join(entry.path.to_string_lossy().as_ref());
859 found_content.insert(full_path);
860
861 Ok(ControlFlow::Continue(()))
862 },
863 )?;
864 }
865
866 prune_known_run_paths(&mut found_content);
870
871 if found_content.is_empty() {
872 return lint_ok();
873 }
874
875 let header = "Found content in runtime-only directories (/run, /tmp)";
876 let items = found_content.iter().map(PathQuotedDisplay::new);
877 format_lint_err_from_items(config, header, items)
878}
879
880fn prune_known_run_paths(paths: &mut BTreeSet<Utf8PathBuf>) {
891 let run_prefix = Utf8Path::new("/run");
892 for known in CONTAINER_RUNTIME_FILES {
893 let known = Utf8Path::new(known);
894 paths.remove(known);
895 let mut dir = known.parent();
898 while let Some(d) = dir {
899 if !d.starts_with(run_prefix) || d == run_prefix {
900 break;
901 }
902 let has_other_children = paths.iter().any(|p| p != d && p.starts_with(d));
903 if has_other_children {
904 break;
905 }
906 paths.remove(d);
907 dir = d.parent();
908 }
909 }
910}
911
912const BINARY_DEPS: &str = env!("BOOTC_BINARY_DEPS");
915
916fn resolve_runtime_bins<'a>(podman: &'a str, skopeo: &'a str) -> Vec<&'a str> {
922 let mut bins: Vec<&str> = BINARY_DEPS
923 .split(',')
924 .map(|b| match b {
925 "podman" => podman,
926 "skopeo" => skopeo,
927 other => other,
928 })
929 .collect();
930 bins.sort_unstable();
931 bins.dedup();
932 bins
933}
934
935fn resolved_runtime_bins() -> Vec<&'static str> {
936 resolve_runtime_bins(bootc_utils::podman_bin(), bootc_utils::skopeo_bin())
937}
938
939#[distributed_slice(LINTS)]
940static LINT_RUNTIME_DEPS: Lint = Lint::new_warning(
941 "runtime-deps",
942 "Check that required runtime dependencies are present in the image.",
943 check_runtime_deps,
944);
945fn check_runtime_deps(root: &Dir, _config: &LintExecutionConfig) -> LintResult {
946 let mut missing = Vec::new();
947 for bin in resolved_runtime_bins() {
948 if !crate::utils::have_executable_in_root(root, bin)? {
949 missing.push(bin);
950 }
951 }
952 if missing.is_empty() {
953 return lint_ok();
954 }
955 lint_err(format!(
956 "Missing required runtime dependencies: {}",
957 missing.join(", ")
958 ))
959}
960
961#[cfg(test)]
962mod tests {
963 use std::sync::LazyLock;
964
965 use super::*;
966 use crate::bootc_composefs::image::setup_root_conf_path;
967
968 static ALTROOT_LINTS: LazyLock<usize> = LazyLock::new(|| {
969 LINTS
970 .iter()
971 .filter(|lint| lint.root_type != Some(RootType::Running))
972 .count()
973 });
974
975 fn fixture() -> Result<cap_std_ext::cap_tempfile::TempDir> {
976 let tempdir = cap_std_ext::cap_tempfile::tempdir(cap_std::ambient_authority())?;
978 Ok(tempdir)
979 }
980
981 fn passing_fixture() -> Result<cap_std_ext::cap_tempfile::TempDir> {
982 let root = cap_std_ext::cap_tempfile::tempdir(cap_std::ambient_authority())?;
984 for d in API_DIRS {
985 root.create_dir(d)?;
986 }
987 root.create_dir_all("usr/lib/modules/5.7.2")?;
988 root.write("usr/lib/modules/5.7.2/vmlinuz", "vmlinuz")?;
989
990 root.create_dir("boot")?;
991 root.create_dir("sysroot")?;
992 root.symlink_contents("sysroot/ostree", "ostree")?;
993
994 const PREPAREROOT_PATH: &str = "usr/lib/ostree/prepare-root.conf";
995 const PREPAREROOT: &str =
996 include_str!("../../../baseimage/base/usr/lib/ostree/prepare-root.conf");
997 root.create_dir_all(Utf8Path::new(PREPAREROOT_PATH).parent().unwrap())?;
998 root.atomic_write(PREPAREROOT_PATH, PREPAREROOT)?;
999
1000 for bin in resolved_runtime_bins() {
1001 add_runtime_bin(&root, bin)?;
1002 }
1003
1004 Ok(root)
1005 }
1006
1007 fn add_runtime_bin(root: &Dir, bin: &str) -> Result<()> {
1008 root.create_dir_all("usr/bin")?;
1009 root.write(format!("usr/bin/{bin}"), "")?;
1010 Ok(())
1011 }
1012
1013 #[test]
1014 fn test_var_run() -> Result<()> {
1015 let root = &fixture()?;
1016 let config = &LintExecutionConfig::default();
1017 check_var_run(root, config).unwrap().unwrap();
1019 root.create_dir_all("var/run/foo")?;
1020 assert!(check_var_run(root, config).unwrap().is_err());
1021 root.remove_dir_all("var/run")?;
1022 check_var_run(root, config).unwrap().unwrap();
1024 Ok(())
1025 }
1026
1027 #[test]
1028 fn test_api() -> Result<()> {
1029 let root = &passing_fixture()?;
1030 let config = &LintExecutionConfig::default();
1031 check_api_dirs(root, config).unwrap().unwrap();
1033 root.remove_dir("var")?;
1034 assert!(check_api_dirs(root, config).unwrap().is_err());
1035 root.write("var", "a file for var")?;
1036 assert!(check_api_dirs(root, config).unwrap().is_err());
1037 Ok(())
1038 }
1039
1040 #[test]
1041 fn test_lint_main() -> Result<()> {
1042 let root = &passing_fixture()?;
1043 let config = &LintExecutionConfig::default();
1044 let mut out = Vec::new();
1045 let warnings = WarningDisposition::FatalWarnings;
1046 let root_type = RootType::Alternative;
1047 lint(root, warnings, root_type, [], &mut out, config.no_truncate).unwrap();
1048 root.create_dir_all("var/run/foo")?;
1049 let mut out = Vec::new();
1050 assert!(lint(root, warnings, root_type, [], &mut out, config.no_truncate).is_err());
1051 Ok(())
1052 }
1053
1054 #[test]
1055 fn test_lint_inner() -> Result<()> {
1056 let root = &passing_fixture()?;
1057 let config = &LintExecutionConfig::default();
1058
1059 let mut out = Vec::new();
1061 let root_type = RootType::Alternative;
1062 let r = lint_inner(root, root_type, config, [], &mut out).unwrap();
1063 let running_only_lints = LINTS.len().checked_sub(*ALTROOT_LINTS).unwrap();
1064 assert_eq!(r.warnings, 0);
1065 assert_eq!(r.fatal, 0);
1066 assert_eq!(r.skipped, running_only_lints);
1067 assert_eq!(r.passed, *ALTROOT_LINTS);
1068
1069 let r = lint_inner(root, root_type, config, ["var-log"], &mut out).unwrap();
1070 root.create_dir_all("var/log/dnf")?;
1072 root.write("var/log/dnf/dnf.log", b"dummy dnf log")?;
1073 assert_eq!(r.passed, ALTROOT_LINTS.checked_sub(1).unwrap());
1074 assert_eq!(r.fatal, 0);
1075 assert_eq!(r.skipped, running_only_lints + 1);
1076 assert_eq!(r.warnings, 0);
1077
1078 let mut out = Vec::new();
1080 let r = lint_inner(root, root_type, config, [], &mut out).unwrap();
1081 assert_eq!(r.passed, ALTROOT_LINTS.checked_sub(1).unwrap());
1082 assert_eq!(r.fatal, 0);
1083 assert_eq!(r.skipped, running_only_lints);
1084 assert_eq!(r.warnings, 1);
1085 Ok(())
1086 }
1087
1088 #[test]
1089 fn test_kernel_lint() -> Result<()> {
1090 let root = &fixture()?;
1091 let config = &LintExecutionConfig::default();
1092 check_kernel(root, config).unwrap().unwrap();
1094 root.create_dir_all("usr/lib/modules/5.7.2")?;
1095 root.write("usr/lib/modules/5.7.2/vmlinuz", "old vmlinuz")?;
1096 root.create_dir_all("usr/lib/modules/6.3.1")?;
1097 root.write("usr/lib/modules/6.3.1/vmlinuz", "new vmlinuz")?;
1098 assert!(check_kernel(root, config).is_err());
1099 root.remove_dir_all("usr/lib/modules/5.7.2")?;
1100 check_kernel(root, config).unwrap().unwrap();
1102 Ok(())
1103 }
1104
1105 #[test]
1106 fn test_kargs() -> Result<()> {
1107 let root = &fixture()?;
1108 let config = &LintExecutionConfig::default();
1109 check_parse_kargs(root, config).unwrap().unwrap();
1110 root.create_dir_all("usr/lib/bootc")?;
1111 root.write("usr/lib/bootc/kargs.d", "not a directory")?;
1112 assert!(check_parse_kargs(root, config).is_err());
1113 Ok(())
1114 }
1115
1116 #[test]
1117 fn test_usr_etc() -> Result<()> {
1118 let root = &fixture()?;
1119 let config = &LintExecutionConfig::default();
1120 check_usretc(root, config).unwrap().unwrap();
1122 root.create_dir_all("etc")?;
1123 root.create_dir_all("usr/etc")?;
1124 assert!(check_usretc(root, config).unwrap().is_err());
1125 root.remove_dir_all("etc")?;
1126 check_usretc(root, config).unwrap().unwrap();
1128 Ok(())
1129 }
1130
1131 #[test]
1132 fn test_varlog() -> Result<()> {
1133 let root = &fixture()?;
1134 let config = &LintExecutionConfig::default();
1135 check_varlog(root, config).unwrap().unwrap();
1136 root.create_dir_all("var/log")?;
1137 check_varlog(root, config).unwrap().unwrap();
1138 root.symlink_contents("../../usr/share/doc/systemd/README.logs", "var/log/README")?;
1139 check_varlog(root, config).unwrap().unwrap();
1140
1141 root.atomic_write("var/log/somefile.log", "log contents")?;
1142 let Err(e) = check_varlog(root, config).unwrap() else {
1143 unreachable!()
1144 };
1145 similar_asserts::assert_eq!(
1146 e.to_string(),
1147 "Found non-empty logfiles:\n /var/log/somefile.log\n"
1148 );
1149 root.create_dir_all("var/log/someproject")?;
1150 root.atomic_write("var/log/someproject/audit.log", "audit log")?;
1151 root.atomic_write("var/log/someproject/info.log", "info")?;
1152 let Err(e) = check_varlog(root, config).unwrap() else {
1153 unreachable!()
1154 };
1155 similar_asserts::assert_eq!(
1156 e.to_string(),
1157 indoc! { r#"
1158 Found non-empty logfiles:
1159 /var/log/somefile.log
1160 /var/log/someproject/audit.log
1161 /var/log/someproject/info.log
1162 "# }
1163 );
1164
1165 Ok(())
1166 }
1167
1168 #[test]
1169 fn test_boot() -> Result<()> {
1170 let root = &passing_fixture()?;
1171 let config = &LintExecutionConfig::default();
1172 check_boot(&root, config).unwrap().unwrap();
1173
1174 root.create_dir_all("EFI/Linux")?;
1176 root.write("EFI/Linux/foo.efi", b"some dummy efi")?;
1177 check_boot(&root, config).unwrap().unwrap();
1178
1179 root.create_dir("boot/somesubdir")?;
1180 let Err(e) = check_boot(&root, config).unwrap() else {
1181 unreachable!()
1182 };
1183 assert!(e.to_string().contains("somesubdir"));
1184
1185 Ok(())
1186 }
1187
1188 #[test]
1189 fn test_runtime_only_dirs() -> Result<()> {
1190 let root = &fixture()?;
1191 let config = &LintExecutionConfig::default();
1192
1193 root.create_dir_all("run")?;
1194 root.create_dir_all("tmp")?;
1195
1196 root.create_dir_all("run/systemd/resolve")?;
1202 check_runtime_only_dirs(root, config).unwrap().unwrap();
1203
1204 root.write("run/systemd/resolve/stub-resolv.conf", "data")?;
1206 check_runtime_only_dirs(root, config).unwrap().unwrap();
1207 root.remove_file("run/systemd/resolve/stub-resolv.conf")?;
1208
1209 root.write("run/systemd/resolve/something-else", "data")?;
1213 let Err(e) = check_runtime_only_dirs(root, config).unwrap() else {
1214 unreachable!("expected warning for unknown file under /run/systemd")
1215 };
1216 let msg = e.to_string();
1217 assert!(
1218 msg.contains("/run/systemd/resolve/something-else"),
1219 "should warn about the unknown file: {msg}"
1220 );
1221 assert!(
1223 msg.contains("/run/systemd"),
1224 "parent dirs with real children should appear: {msg}"
1225 );
1226 root.remove_file("run/systemd/resolve/something-else")?;
1227
1228 root.create_dir("run/dnf")?;
1230 let Err(e) = check_runtime_only_dirs(root, config).unwrap() else {
1231 unreachable!("expected warning for /run/dnf")
1232 };
1233 let msg = e.to_string();
1234 assert!(
1235 msg.contains("/run/dnf"),
1236 "should warn about /run/dnf: {msg}"
1237 );
1238 assert!(
1239 !msg.contains("/run/systemd"),
1240 "should not mention /run/systemd: {msg}"
1241 );
1242 root.remove_dir("run/dnf")?;
1243
1244 root.write("run/leaked-file", "data")?;
1246 let Err(e) = check_runtime_only_dirs(root, config).unwrap() else {
1247 unreachable!("expected warning for /run/leaked-file")
1248 };
1249 assert!(e.to_string().contains("/run/leaked-file"));
1250 root.remove_file("run/leaked-file")?;
1251
1252 root.write("tmp/build-artifact", "some data")?;
1254 let Err(e) = check_runtime_only_dirs(root, config).unwrap() else {
1255 unreachable!("expected warning for /tmp/build-artifact")
1256 };
1257 assert!(e.to_string().contains("/tmp/build-artifact"));
1258 root.remove_file("tmp/build-artifact")?;
1259
1260 check_runtime_only_dirs(root, config).unwrap().unwrap();
1262
1263 Ok(())
1264 }
1265
1266 fn run_recursive_lint(
1267 root: &Dir,
1268 f: LintRecursiveFn,
1269 config: &LintExecutionConfig,
1270 ) -> LintResult {
1271 let mut result = lint_ok();
1273 root.walk(
1274 &walk_configuration().path_base(Path::new("/")),
1275 |e| -> Result<_> {
1276 let r = f(e, config)?;
1277 match r {
1278 Ok(()) => Ok(ControlFlow::Continue(())),
1279 Err(e) => {
1280 result = Ok(Err(e));
1281 Ok(ControlFlow::Break(()))
1282 }
1283 }
1284 },
1285 )?;
1286 result
1287 }
1288
1289 #[test]
1290 fn test_non_utf8() {
1291 use std::{ffi::OsStr, os::unix::ffi::OsStrExt};
1292
1293 let root = &fixture().unwrap();
1294 let config = &LintExecutionConfig::default();
1295
1296 root.create_dir("subdir").unwrap();
1298 root.symlink("self", "self").unwrap();
1300 root.symlink("..", "subdir/parent").unwrap();
1302 root.symlink("does-not-exist", "broken").unwrap();
1304 root.symlink("../../x", "escape").unwrap();
1306 run_recursive_lint(root, check_utf8, config)
1308 .unwrap()
1309 .unwrap();
1310
1311 let baddir = OsStr::from_bytes(b"subdir/2/bad\xffdir");
1313 root.create_dir("subdir/2").unwrap();
1314 root.create_dir(baddir).unwrap();
1315 let Err(err) = run_recursive_lint(root, check_utf8, config).unwrap() else {
1316 unreachable!("Didn't fail");
1317 };
1318 assert_eq!(
1319 err.to_string(),
1320 r#"/subdir/2: Found non-utf8 filename "bad\xFFdir""#
1321 );
1322 root.remove_dir(baddir).unwrap(); run_recursive_lint(root, check_utf8, config)
1324 .unwrap()
1325 .unwrap(); let badfile = OsStr::from_bytes(b"regular\xff");
1329 root.write(badfile, b"Hello, world!\n").unwrap();
1330 let Err(err) = run_recursive_lint(root, check_utf8, config).unwrap() else {
1331 unreachable!("Didn't fail");
1332 };
1333 assert_eq!(
1334 err.to_string(),
1335 r#"/: Found non-utf8 filename "regular\xFF""#
1336 );
1337 root.remove_file(badfile).unwrap(); run_recursive_lint(root, check_utf8, config)
1339 .unwrap()
1340 .unwrap(); root.symlink(badfile, "subdir/good-name").unwrap();
1344 let Err(err) = run_recursive_lint(root, check_utf8, config).unwrap() else {
1345 unreachable!("Didn't fail");
1346 };
1347 assert_eq!(
1348 err.to_string(),
1349 r#"/subdir/good-name: Found non-utf8 symlink target"#
1350 );
1351 root.remove_file("subdir/good-name").unwrap(); run_recursive_lint(root, check_utf8, config)
1353 .unwrap()
1354 .unwrap(); root.symlink(badfile, badfile).unwrap();
1359 let Err(err) = run_recursive_lint(root, check_utf8, config).unwrap() else {
1360 unreachable!("Didn't fail");
1361 };
1362 assert_eq!(
1363 err.to_string(),
1364 r#"/: Found non-utf8 filename "regular\xFF""#
1365 );
1366 root.remove_file(badfile).unwrap(); run_recursive_lint(root, check_utf8, config)
1368 .unwrap()
1369 .unwrap(); }
1371
1372 #[test]
1373 fn test_baseimage_root() -> Result<()> {
1374 let td = fixture()?;
1375 let config = &LintExecutionConfig::default();
1376
1377 assert!(check_baseimage_root(&td, config).unwrap().is_err());
1379
1380 drop(td);
1381 let td = passing_fixture()?;
1382 check_baseimage_root(&td, config).unwrap().unwrap();
1383
1384 td.remove_file("ostree")?;
1386 assert!(check_baseimage_root(&td, config).unwrap().is_err());
1387 let conf = Utf8Path::new(setup_root_conf_path());
1388 td.create_dir_all(conf.parent().unwrap())?;
1389 td.write(conf, "")?;
1390 check_baseimage_root(&td, config).unwrap().unwrap();
1391 td.create_dir("ostree")?;
1393 assert!(check_baseimage_root(&td, config).unwrap().is_err());
1394 td.remove_dir("ostree")?;
1395 td.remove_dir("sysroot")?;
1397 assert!(check_baseimage_root(&td, config).unwrap().is_err());
1398 Ok(())
1399 }
1400
1401 #[test]
1402 fn test_composefs() -> Result<()> {
1403 let td = fixture()?;
1404 let config = &LintExecutionConfig::default();
1405
1406 assert!(check_composefs(&td, config).unwrap().is_err());
1408
1409 drop(td);
1410 let td = passing_fixture()?;
1411 check_composefs(&td, config).unwrap().unwrap();
1413
1414 td.write(
1415 "usr/lib/ostree/prepare-root.conf",
1416 b"[composefs]\nenabled = false",
1417 )?;
1418 assert!(check_composefs(&td, config).unwrap().is_err());
1420
1421 let conf = Utf8Path::new(setup_root_conf_path());
1424 td.create_dir_all(conf.parent().unwrap())?;
1425 td.write(conf, "")?;
1426 check_composefs(&td, config).unwrap().unwrap();
1427
1428 Ok(())
1429 }
1430
1431 #[test]
1432 fn test_buildah_injected() -> Result<()> {
1433 let td = fixture()?;
1434 let config = &LintExecutionConfig::default();
1435 td.create_dir("etc")?;
1436 assert!(check_buildah_injected(&td, config).unwrap().is_ok());
1437 td.write("etc/hostname", b"")?;
1438 assert!(check_buildah_injected(&td, config).unwrap().is_err());
1439 td.write("etc/hostname", b"some static hostname")?;
1440 assert!(check_buildah_injected(&td, config).unwrap().is_ok());
1441 Ok(())
1442 }
1443
1444 #[test]
1445 fn test_list() {
1446 let mut r = Vec::new();
1447 lint_list(&mut r).unwrap();
1448 let lints: Vec<serde_yaml::Value> = serde_yaml::from_slice(&r).unwrap();
1449 assert_eq!(lints.len(), LINTS.len());
1450 }
1451
1452 #[test]
1453 fn test_format_items_no_truncate() -> Result<()> {
1454 let config = LintExecutionConfig { no_truncate: true };
1455 let header = "Test Header";
1456 let mut output_str = String::new();
1457
1458 let items_empty: Vec<String> = vec![];
1460 format_items(&config, header, items_empty.iter(), &mut output_str)?;
1461 assert_eq!(output_str, "");
1462 output_str.clear();
1463
1464 let items_one = ["item1"];
1466 format_items(&config, header, items_one.iter(), &mut output_str)?;
1467 assert_eq!(output_str, "Test Header:\n item1\n");
1468 output_str.clear();
1469
1470 let items_multiple = (1..=3).map(|v| format!("item{v}")).collect::<Vec<_>>();
1472 format_items(&config, header, items_multiple.iter(), &mut output_str)?;
1473 assert_eq!(output_str, "Test Header:\n item1\n item2\n item3\n");
1474 output_str.clear();
1475
1476 let items_multiple = (1..=8).map(|v| format!("item{v}")).collect::<Vec<_>>();
1478 format_items(&config, header, items_multiple.iter(), &mut output_str)?;
1479 assert_eq!(
1480 output_str,
1481 "Test Header:\n item1\n item2\n item3\n item4\n item5\n item6\n item7\n item8\n"
1482 );
1483 output_str.clear();
1484
1485 Ok(())
1486 }
1487
1488 #[test]
1489 fn test_format_items_truncate() -> Result<()> {
1490 let config = LintExecutionConfig::default();
1491 let header = "Test Header";
1492 let mut output_str = String::new();
1493
1494 let items_empty: Vec<String> = vec![];
1496 format_items(&config, header, items_empty.iter(), &mut output_str)?;
1497 assert_eq!(output_str, "");
1498 output_str.clear();
1499
1500 let items_few = ["item1", "item2"];
1502 format_items(&config, header, items_few.iter(), &mut output_str)?;
1503 assert_eq!(output_str, "Test Header:\n item1\n item2\n");
1504 output_str.clear();
1505
1506 let items_exact: Vec<_> = (0..DEFAULT_TRUNCATED_OUTPUT.get())
1508 .map(|i| format!("item{}", i + 1))
1509 .collect();
1510 format_items(&config, header, items_exact.iter(), &mut output_str)?;
1511 let mut expected_output = String::from("Test Header:\n");
1512 for i in 0..DEFAULT_TRUNCATED_OUTPUT.get() {
1513 writeln!(expected_output, " item{}", i + 1)?;
1514 }
1515 assert_eq!(output_str, expected_output);
1516 output_str.clear();
1517
1518 let items_many: Vec<_> = (0..(DEFAULT_TRUNCATED_OUTPUT.get() + 2))
1520 .map(|i| format!("item{}", i + 1))
1521 .collect();
1522 format_items(&config, header, items_many.iter(), &mut output_str)?;
1523 let mut expected_output = String::from("Test Header:\n");
1524 for i in 0..DEFAULT_TRUNCATED_OUTPUT.get() {
1525 writeln!(expected_output, " item{}", i + 1)?;
1526 }
1527 writeln!(expected_output, " ...and 2 more")?;
1528 assert_eq!(output_str, expected_output);
1529 output_str.clear();
1530
1531 let items_one_more: Vec<_> = (0..(DEFAULT_TRUNCATED_OUTPUT.get() + 1))
1533 .map(|i| format!("item{}", i + 1))
1534 .collect();
1535 format_items(&config, header, items_one_more.iter(), &mut output_str)?;
1536 let mut expected_output = String::from("Test Header:\n");
1537 for i in 0..DEFAULT_TRUNCATED_OUTPUT.get() {
1538 writeln!(expected_output, " item{}", i + 1)?;
1539 }
1540 writeln!(expected_output, " ...and 1 more")?;
1541 assert_eq!(output_str, expected_output);
1542 output_str.clear();
1543
1544 Ok(())
1545 }
1546
1547 #[test]
1548 fn test_format_items_display_impl() -> Result<()> {
1549 let config = LintExecutionConfig::default();
1550 let header = "Numbers";
1551 let mut output_str = String::new();
1552
1553 let items_numbers = [1, 2, 3];
1554 format_items(&config, header, items_numbers.iter(), &mut output_str)?;
1555 similar_asserts::assert_eq!(output_str, "Numbers:\n 1\n 2\n 3\n");
1556
1557 Ok(())
1558 }
1559
1560 #[test]
1561 fn test_runtime_deps() -> Result<()> {
1562 let root = &fixture()?;
1563 let config = &LintExecutionConfig::default();
1564
1565 let bins = resolved_runtime_bins();
1566 let err = check_runtime_deps(root, config)?.unwrap_err();
1567 assert_eq!(
1568 err.to_string(),
1569 format!("Missing required runtime dependencies: {}", bins.join(", "))
1570 );
1571
1572 for bin in &bins {
1573 add_runtime_bin(root, bin)?;
1574 }
1575 check_runtime_deps(root, config)??;
1576
1577 let podman = bootc_utils::podman_bin();
1578 root.remove_file(format!("usr/bin/{podman}"))?;
1579 let err = check_runtime_deps(root, config)?.unwrap_err();
1580 assert_eq!(
1581 err.to_string(),
1582 format!("Missing required runtime dependencies: {podman}")
1583 );
1584
1585 assert_eq!(
1586 resolve_runtime_bins("dtool", "dtool"),
1587 ["chcon", "dtool", "ostree", "setpriv", "systemctl", "zstd"]
1588 );
1589 Ok(())
1590 }
1591
1592 #[test]
1593 fn test_runtime_deps_escape() -> Result<()> {
1594 let root = &fixture()?;
1595 root.create_dir("usr")?;
1596 root.symlink_contents("../../var/usrbin", "usr/bin")?;
1599
1600 let config = &LintExecutionConfig::default();
1601 let mut out = Vec::new();
1602 let err = lint_inner(
1603 root,
1604 RootType::Alternative,
1605 config,
1606 LINTS
1607 .iter()
1608 .filter(|lint| lint.name != "runtime-deps")
1609 .map(|lint| lint.name),
1610 &mut out,
1611 )
1612 .expect_err("lookup failure must remain a runtime error");
1613
1614 let bin = resolved_runtime_bins()
1615 .into_iter()
1616 .next()
1617 .expect("runtime dependency");
1618 let attempted = format!("usr/bin/{bin}");
1619 assert_eq!(
1620 err.to_string(),
1621 "Unexpected runtime error running lint runtime-deps"
1622 );
1623 assert!(
1624 err.chain()
1625 .any(|source| source.to_string().contains(&attempted)),
1626 "{err:?}"
1627 );
1628 assert!(
1629 err.chain().any(|source| {
1630 source
1631 .downcast_ref::<std::io::Error>()
1632 .is_some_and(|io_err| {
1633 io_err
1634 .to_string()
1635 .contains("a path led outside of the filesystem")
1636 })
1637 }),
1638 "{err:?}"
1639 );
1640 Ok(())
1641 }
1642}