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;
31
32const BASEIMAGE_REF: &str = "usr/share/doc/bootc/baseimage/base";
34const API_DIRS: &[&str] = &["dev", "proc", "sys", "run", "tmp", "var"];
36
37const DEFAULT_TRUNCATED_OUTPUT: NonZeroUsize = const { NonZeroUsize::new(5).unwrap() };
39
40#[derive(thiserror::Error, Debug)]
42struct LintError(String);
43
44type LintResult = Result<std::result::Result<(), LintError>>;
47
48fn lint_ok() -> LintResult {
51 Ok(Ok(()))
52}
53
54fn lint_err(msg: impl AsRef<str>) -> LintResult {
56 Ok(Err(LintError::new(msg)))
57}
58
59impl std::fmt::Display for LintError {
60 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
61 f.write_str(&self.0)
62 }
63}
64
65impl LintError {
66 fn new(msg: impl AsRef<str>) -> Self {
67 Self(msg.as_ref().to_owned())
68 }
69}
70
71#[derive(Debug, Default)]
72struct LintExecutionConfig {
73 no_truncate: bool,
74}
75
76type LintFn = fn(&Dir, config: &LintExecutionConfig) -> LintResult;
77type LintRecursiveResult = LintResult;
78type LintRecursiveFn = fn(&WalkComponent, config: &LintExecutionConfig) -> LintRecursiveResult;
79#[derive(Debug)]
82enum LintFnTy {
83 Regular(LintFn),
85 Recursive(LintRecursiveFn),
87}
88#[distributed_slice]
89pub(crate) static LINTS: [Lint];
90
91#[derive(Debug, Serialize)]
93#[serde(rename_all = "kebab-case")]
94enum LintType {
95 Fatal,
98 Warning,
100}
101
102#[derive(Debug, Copy, Clone)]
103pub(crate) enum WarningDisposition {
104 AllowWarnings,
105 FatalWarnings,
106}
107
108#[derive(Debug, Copy, Clone, Serialize, PartialEq, Eq)]
109pub(crate) enum RootType {
110 Running,
111 Alternative,
112}
113
114#[derive(Debug, Serialize)]
115#[serde(rename_all = "kebab-case")]
116struct Lint {
117 name: &'static str,
118 #[serde(rename = "type")]
119 ty: LintType,
120 #[serde(skip)]
121 f: LintFnTy,
122 description: &'static str,
123 #[serde(skip_serializing_if = "Option::is_none")]
125 root_type: Option<RootType>,
126}
127
128impl PartialEq for Lint {
130 fn eq(&self, other: &Self) -> bool {
131 self.name == other.name
132 }
133}
134impl Eq for Lint {}
135
136impl std::hash::Hash for Lint {
137 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
138 self.name.hash(state);
139 }
140}
141
142impl PartialOrd for Lint {
143 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
144 Some(self.cmp(other))
145 }
146}
147impl Ord for Lint {
148 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
149 self.name.cmp(other.name)
150 }
151}
152
153impl Lint {
154 pub(crate) const fn new_fatal(
155 name: &'static str,
156 description: &'static str,
157 f: LintFn,
158 ) -> Self {
159 Lint {
160 name,
161 ty: LintType::Fatal,
162 f: LintFnTy::Regular(f),
163 description,
164 root_type: None,
165 }
166 }
167
168 pub(crate) const fn new_warning(
169 name: &'static str,
170 description: &'static str,
171 f: LintFn,
172 ) -> Self {
173 Lint {
174 name,
175 ty: LintType::Warning,
176 f: LintFnTy::Regular(f),
177 description,
178 root_type: None,
179 }
180 }
181
182 const fn set_root_type(mut self, v: RootType) -> Self {
183 self.root_type = Some(v);
184 self
185 }
186}
187
188pub(crate) fn lint_list(output: impl std::io::Write) -> Result<()> {
189 serde_yaml::to_writer(output, &*LINTS)?;
191 Ok(())
192}
193
194#[derive(Debug)]
195struct LintExecutionResult {
196 warnings: usize,
197 passed: usize,
198 skipped: usize,
199 fatal: usize,
200}
201
202fn format_items<T>(
204 config: &LintExecutionConfig,
205 header: &str,
206 items: impl Iterator<Item = T>,
207 o: &mut String,
208) -> Result<()>
209where
210 T: Display,
211{
212 let mut items = items.into_iter();
213 if config.no_truncate {
214 let Some(first) = items.next() else {
215 return Ok(());
216 };
217 writeln!(o, "{header}:")?;
218 writeln!(o, " {first}")?;
219 for item in items {
220 writeln!(o, " {item}")?;
221 }
222 return Ok(());
223 } else {
224 let Some((samples, rest)) = bootc_utils::collect_until(items, DEFAULT_TRUNCATED_OUTPUT)
225 else {
226 return Ok(());
227 };
228 writeln!(o, "{header}:")?;
229 for item in samples {
230 writeln!(o, " {item}")?;
231 }
232 if rest > 0 {
233 writeln!(o, " ...and {rest} more")?;
234 }
235 }
236 Ok(())
237}
238
239fn format_lint_err_from_items<T>(
243 config: &LintExecutionConfig,
244 header: &str,
245 items: impl Iterator<Item = T>,
246) -> LintResult
247where
248 T: Display,
249{
250 let mut msg = String::new();
251 format_items(config, header, items, &mut msg).unwrap();
253 lint_err(msg)
254}
255
256fn lint_inner<'skip>(
257 root: &Dir,
258 root_type: RootType,
259 config: &LintExecutionConfig,
260 skip: impl IntoIterator<Item = &'skip str>,
261 mut output: impl std::io::Write,
262) -> Result<LintExecutionResult> {
263 let mut fatal = 0usize;
264 let mut warnings = 0usize;
265 let mut passed = 0usize;
266 let skip: std::collections::HashSet<_> = skip.into_iter().collect();
267 let (mut applicable_lints, skipped_lints): (Vec<_>, Vec<_>) = LINTS.iter().partition(|lint| {
268 if skip.contains(lint.name) {
269 return false;
270 }
271 if let Some(lint_root_type) = lint.root_type {
272 if lint_root_type != root_type {
273 return false;
274 }
275 }
276 true
277 });
278 let skipped = skipped_lints.len();
280 applicable_lints.sort_by(|a, b| a.name.cmp(b.name));
282 let (nonrec_lints, recursive_lints): (Vec<_>, Vec<_>) = applicable_lints
284 .into_iter()
285 .partition(|lint| matches!(lint.f, LintFnTy::Regular(_)));
286 let mut results = Vec::new();
287 for lint in nonrec_lints {
288 let f = match lint.f {
289 LintFnTy::Regular(f) => f,
290 LintFnTy::Recursive(_) => unreachable!(),
291 };
292 results.push((lint, f(&root, &config)));
293 }
294
295 let mut recursive_lints = BTreeSet::from_iter(recursive_lints);
296 let mut recursive_errors = BTreeMap::new();
297 root.walk(
298 &WalkConfiguration::default()
299 .noxdev()
300 .path_base(Path::new("/")),
301 |e| -> std::io::Result<_> {
302 if recursive_lints.is_empty() {
304 return Ok(ControlFlow::Break(()));
305 }
306 let mut this_iteration_errors = Vec::new();
309 for &lint in recursive_lints.iter() {
311 let f = match &lint.f {
312 LintFnTy::Regular(_) => unreachable!(),
314 LintFnTy::Recursive(f) => f,
315 };
316 match f(e, &config) {
318 Ok(Ok(())) => {}
319 o => this_iteration_errors.push((lint, o)),
320 }
321 }
322 for (lint, err) in this_iteration_errors {
325 recursive_lints.remove(lint);
326 recursive_errors.insert(lint, err);
327 }
328 Ok(ControlFlow::Continue(()))
329 },
330 )?;
331 results.extend(recursive_errors);
333 results.extend(recursive_lints.into_iter().map(|lint| (lint, lint_ok())));
335 for (lint, r) in results {
336 let name = lint.name;
337 let r = match r {
338 Ok(r) => r,
339 Err(e) => anyhow::bail!("Unexpected runtime error running lint {name}: {e}"),
340 };
341
342 if let Err(e) = r {
343 match lint.ty {
344 LintType::Fatal => {
345 writeln!(output, "Failed lint: {name}: {e}")?;
346 fatal += 1;
347 }
348 LintType::Warning => {
349 writeln!(output, "Lint warning: {name}: {e}")?;
350 warnings += 1;
351 }
352 }
353 } else {
354 tracing::debug!("OK {name} (type={:?})", lint.ty);
356 passed += 1;
357 }
358 }
359
360 Ok(LintExecutionResult {
361 passed,
362 skipped,
363 warnings,
364 fatal,
365 })
366}
367
368#[context("Linting")]
369pub(crate) fn lint<'skip>(
370 root: &Dir,
371 warning_disposition: WarningDisposition,
372 root_type: RootType,
373 skip: impl IntoIterator<Item = &'skip str>,
374 mut output: impl std::io::Write,
375 no_truncate: bool,
376) -> Result<()> {
377 let config = LintExecutionConfig { no_truncate };
378 let r = lint_inner(root, root_type, &config, skip, &mut output)?;
379 writeln!(output, "Checks passed: {}", r.passed)?;
380 if r.skipped > 0 {
381 writeln!(output, "Checks skipped: {}", r.skipped)?;
382 }
383 let fatal = if matches!(warning_disposition, WarningDisposition::FatalWarnings) {
384 r.fatal + r.warnings
385 } else {
386 r.fatal
387 };
388 if r.warnings > 0 {
389 writeln!(output, "Warnings: {}", r.warnings)?;
390 }
391 if fatal > 0 {
392 anyhow::bail!("Checks failed: {}", fatal)
393 }
394 Ok(())
395}
396
397#[distributed_slice(LINTS)]
400static LINT_VAR_RUN: Lint = Lint::new_fatal(
401 "var-run",
402 "Check for /var/run being a physical directory; this is always a bug.",
403 check_var_run,
404);
405fn check_var_run(root: &Dir, _config: &LintExecutionConfig) -> LintResult {
406 if let Some(meta) = root.symlink_metadata_optional("var/run")? {
407 if !meta.is_symlink() {
408 return lint_err("Not a symlink: var/run");
409 }
410 }
411 lint_ok()
412}
413
414#[distributed_slice(LINTS)]
415static LINT_BUILDAH_INJECTED: Lint = Lint::new_warning(
416 "buildah-injected",
417 indoc::indoc! { "
418 Check for an invalid /etc/hostname or /etc/resolv.conf that may have been injected by
419 a container build system." },
420 check_buildah_injected,
421)
422.set_root_type(RootType::Alternative);
425fn check_buildah_injected(root: &Dir, _config: &LintExecutionConfig) -> LintResult {
426 const RUNTIME_INJECTED: &[&str] = &["etc/hostname", "etc/resolv.conf"];
427 for ent in RUNTIME_INJECTED {
428 if let Some(meta) = root.symlink_metadata_optional(ent)? {
429 if meta.is_file() && meta.size() == 0 {
430 return lint_err(format!("/{ent} is an empty file; this may have been synthesized by a container runtime."));
431 }
432 }
433 }
434 lint_ok()
435}
436
437#[distributed_slice(LINTS)]
438static LINT_ETC_USRUSETC: Lint = Lint::new_fatal(
439 "etc-usretc",
440 indoc! { r#"
441Verify that only one of /etc or /usr/etc exist. You should only have /etc
442in a container image. It will cause undefined behavior to have both /etc
443and /usr/etc.
444"# },
445 check_usretc,
446);
447fn check_usretc(root: &Dir, _config: &LintExecutionConfig) -> LintResult {
448 let etc_exists = root.symlink_metadata_optional("etc")?.is_some();
449 if !etc_exists {
451 return lint_ok();
452 }
453 if root.symlink_metadata_optional("usr/etc")?.is_some() {
455 return lint_err(
456 "Found /usr/etc - this is a bootc implementation detail and not supported to use in containers"
457 );
458 }
459 lint_ok()
460}
461
462#[distributed_slice(LINTS)]
464static LINT_KARGS: Lint = Lint::new_fatal(
465 "bootc-kargs",
466 "Verify syntax of /usr/lib/bootc/kargs.d.",
467 check_parse_kargs,
468);
469fn check_parse_kargs(root: &Dir, _config: &LintExecutionConfig) -> LintResult {
470 let args = crate::bootc_kargs::get_kargs_in_root(root, ARCH)?;
471 tracing::debug!("found kargs: {args:?}");
472 lint_ok()
473}
474
475#[distributed_slice(LINTS)]
476static LINT_KERNEL: Lint = Lint::new_fatal(
477 "kernel",
478 indoc! { r#"
479 Check for multiple kernels, i.e. multiple directories of the form /usr/lib/modules/$kver.
480 Only one kernel is supported in an image.
481 "# },
482 check_kernel,
483);
484fn check_kernel(root: &Dir, _config: &LintExecutionConfig) -> LintResult {
485 let result = ostree_ext::bootabletree::find_kernel_dir_fs(&root)?;
486 tracing::debug!("Found kernel: {:?}", result);
487 lint_ok()
488}
489
490#[distributed_slice(LINTS)]
492static LINT_UTF8: Lint = Lint {
493 name: "utf8",
494 description: indoc! { r#"
495Check for non-UTF8 filenames. Currently, the ostree backend of bootc only supports
496UTF-8 filenames. Non-UTF8 filenames will cause a fatal error.
497"#},
498 ty: LintType::Fatal,
499 root_type: None,
500 f: LintFnTy::Recursive(check_utf8),
501};
502fn check_utf8(e: &WalkComponent, _config: &LintExecutionConfig) -> LintRecursiveResult {
503 let path = e.path;
504 let filename = e.filename;
505 let dirname = path.parent().unwrap_or(Path::new("/"));
506 if filename.to_str().is_none() {
507 return lint_err(format!(
509 "{}: Found non-utf8 filename {filename:?}",
510 PathQuotedDisplay::new(&dirname)
511 ));
512 };
513
514 if e.file_type.is_symlink() {
515 let target = e.dir.read_link_contents(filename)?;
516 if target.to_str().is_none() {
517 return lint_err(format!(
518 "{}: Found non-utf8 symlink target",
519 PathQuotedDisplay::new(&path)
520 ));
521 }
522 }
523 lint_ok()
524}
525
526fn check_prepareroot_composefs_norecurse(dir: &Dir) -> LintResult {
527 let path = ostree_ext::ostree_prepareroot::CONF_PATH;
528 let Some(config) = ostree_prepareroot::load_config_from_root(dir)? else {
529 return lint_err(format!("{path} is not present to enable composefs"));
530 };
531 if !ostree_prepareroot::overlayfs_enabled_in_config(&config)? {
532 return lint_err(format!("{path} does not have composefs enabled"));
533 }
534 lint_ok()
535}
536
537#[distributed_slice(LINTS)]
538static LINT_API_DIRS: Lint = Lint::new_fatal(
539 "api-base-directories",
540 indoc! { r#"
541Verify that expected base API directories exist. For more information
542on these, see <https://systemd.io/API_FILE_SYSTEMS/>.
543
544Note that in addition, bootc requires that `/var` exist as a directory.
545"#},
546 check_api_dirs,
547);
548fn check_api_dirs(root: &Dir, _config: &LintExecutionConfig) -> LintResult {
549 for d in API_DIRS {
550 let Some(meta) = root.symlink_metadata_optional(d)? else {
551 return lint_err(format!("Missing API filesystem base directory: /{d}"));
552 };
553 if !meta.is_dir() {
554 return lint_err(format!(
555 "Expected directory for API filesystem base directory: /{d}"
556 ));
557 }
558 }
559 lint_ok()
560}
561
562#[distributed_slice(LINTS)]
563static LINT_COMPOSEFS: Lint = Lint::new_warning(
564 "baseimage-composefs",
565 indoc! { r#"
566Check that composefs is enabled for ostree. More in
567<https://ostreedev.github.io/ostree/composefs/>.
568"#},
569 check_composefs,
570);
571fn check_composefs(dir: &Dir, _config: &LintExecutionConfig) -> LintResult {
572 if let Err(e) = check_prepareroot_composefs_norecurse(dir)? {
573 return Ok(Err(e));
574 }
575 if let Some(dir) = dir.open_dir_optional(BASEIMAGE_REF)? {
578 if let Err(e) = check_prepareroot_composefs_norecurse(&dir)? {
579 return Ok(Err(e));
580 }
581 }
582 lint_ok()
583}
584
585fn check_baseimage_root_norecurse(dir: &Dir, _config: &LintExecutionConfig) -> LintResult {
587 let meta = dir.symlink_metadata_optional("sysroot")?;
589 match meta {
590 Some(meta) if !meta.is_dir() => return lint_err("Expected a directory for /sysroot"),
591 None => return lint_err("Missing /sysroot"),
592 _ => {}
593 }
594
595 let Some(meta) = dir.symlink_metadata_optional("ostree")? else {
597 return lint_err("Missing ostree -> sysroot/ostree link");
598 };
599 if !meta.is_symlink() {
600 return lint_err("/ostree should be a symlink");
601 }
602 let link = dir.read_link_contents("ostree")?;
603 let expected = "sysroot/ostree";
604 if link.as_os_str().as_bytes() != expected.as_bytes() {
605 return lint_err(format!("Expected /ostree -> {expected}, not {link:?}"));
606 }
607
608 lint_ok()
609}
610
611#[distributed_slice(LINTS)]
613static LINT_BASEIMAGE_ROOT: Lint = Lint::new_fatal(
614 "baseimage-root",
615 indoc! { r#"
616Check that expected files are present in the root of the filesystem; such
617as /sysroot and a composefs configuration for ostree. More in
618<https://bootc-dev.github.io/bootc/bootc-images.html#standard-image-content>.
619"#},
620 check_baseimage_root,
621);
622fn check_baseimage_root(dir: &Dir, config: &LintExecutionConfig) -> LintResult {
623 if let Err(e) = check_baseimage_root_norecurse(dir, config)? {
624 return Ok(Err(e));
625 }
626 if let Some(dir) = dir.open_dir_optional(BASEIMAGE_REF)? {
629 if let Err(e) = check_baseimage_root_norecurse(&dir, config)? {
630 return Ok(Err(e));
631 }
632 }
633 lint_ok()
634}
635
636fn collect_nonempty_regfiles(
637 root: &Dir,
638 path: &Utf8Path,
639 out: &mut BTreeSet<Utf8PathBuf>,
640) -> Result<()> {
641 for entry in root.entries_utf8()? {
642 let entry = entry?;
643 let ty = entry.file_type()?;
644 let path = path.join(entry.file_name()?);
645 if ty.is_file() {
646 let meta = entry.metadata()?;
647 if meta.size() > 0 {
648 out.insert(path);
649 }
650 } else if ty.is_dir() {
651 let d = entry.open_dir()?;
652 collect_nonempty_regfiles(d.as_cap_std(), &path, out)?;
653 }
654 }
655 Ok(())
656}
657
658#[distributed_slice(LINTS)]
659static LINT_VARLOG: Lint = Lint::new_warning(
660 "var-log",
661 indoc! { r#"
662Check for non-empty regular files in `/var/log`. It is often undesired
663to ship log files in container images. Log files in general are usually
664per-machine state in `/var`. Additionally, log files often include
665timestamps, causing unreproducible container images, and may contain
666sensitive build system information.
667"#},
668 check_varlog,
669);
670fn check_varlog(root: &Dir, config: &LintExecutionConfig) -> LintResult {
671 let Some(d) = root.open_dir_optional("var/log")? else {
672 return lint_ok();
673 };
674 let mut nonempty_regfiles = BTreeSet::new();
675 collect_nonempty_regfiles(&d, "/var/log".into(), &mut nonempty_regfiles)?;
676
677 if nonempty_regfiles.is_empty() {
678 return lint_ok();
679 }
680
681 let header = "Found non-empty logfiles";
682 let items = nonempty_regfiles.iter().map(PathQuotedDisplay::new);
683 format_lint_err_from_items(config, header, items)
684}
685
686#[distributed_slice(LINTS)]
687static LINT_VAR_TMPFILES: Lint = Lint::new_warning(
688 "var-tmpfiles",
689 indoc! { r#"
690Check for content in /var that does not have corresponding systemd tmpfiles.d entries.
691This can cause a problem across upgrades because content in /var from the container
692image will only be applied on the initial provisioning.
693
694Instead, it's recommended to have /var effectively empty in the container image,
695and use systemd tmpfiles.d to generate empty directories and compatibility symbolic links
696as part of each boot.
697"#},
698 check_var_tmpfiles,
699)
700.set_root_type(RootType::Running);
701
702fn check_var_tmpfiles(_root: &Dir, config: &LintExecutionConfig) -> LintResult {
703 let r = bootc_tmpfiles::find_missing_tmpfiles_current_root()?;
704 if r.tmpfiles.is_empty() && r.unsupported.is_empty() {
705 return lint_ok();
706 }
707 let mut msg = String::new();
708 let header = "Found content in /var missing systemd tmpfiles.d entries";
709 format_items(config, header, r.tmpfiles.iter().map(|v| v as &_), &mut msg)?;
710 let header = "Found non-directory/non-symlink files in /var";
711 let items = r.unsupported.iter().map(PathQuotedDisplay::new);
712 format_items(config, header, items, &mut msg)?;
713 lint_err(msg)
714}
715
716#[distributed_slice(LINTS)]
717static LINT_SYSUSERS: Lint = Lint::new_warning(
718 "sysusers",
719 indoc! { r#"
720Check for users in /etc/passwd and groups in /etc/group that do not have corresponding
721systemd sysusers.d entries in /usr/lib/sysusers.d.
722This can cause a problem across upgrades because if /etc is not transient and is locally
723modified (commonly due to local user additions), then the contents of /etc/passwd in the new container
724image may not be visible.
725
726Using systemd-sysusers to allocate users and groups will ensure that these are allocated
727on system startup alongside other users.
728
729More on this topic in <https://bootc-dev.github.io/bootc/building/users-and-groups.html>
730"# },
731 check_sysusers,
732);
733fn check_sysusers(rootfs: &Dir, config: &LintExecutionConfig) -> LintResult {
734 let r = bootc_sysusers::analyze(rootfs)?;
735 if r.is_empty() {
736 return lint_ok();
737 }
738 let mut msg = String::new();
739 let header = "Found /etc/passwd entry without corresponding systemd sysusers.d";
740 let items = r.missing_users.iter().map(|v| v as &dyn std::fmt::Display);
741 format_items(config, header, items, &mut msg)?;
742 let header = "Found /etc/group entry without corresponding systemd sysusers.d";
743 format_items(config, header, r.missing_groups.into_iter(), &mut msg)?;
744 lint_err(msg)
745}
746
747#[distributed_slice(LINTS)]
748static LINT_NONEMPTY_BOOT: Lint = Lint::new_warning(
749 "nonempty-boot",
750 indoc! { r#"
751The `/boot` directory should be present, but empty. The kernel
752content should be in /usr/lib/modules instead in the container image.
753Any content here in the container image will be masked at runtime.
754"#},
755 check_boot,
756);
757fn check_boot(root: &Dir, config: &LintExecutionConfig) -> LintResult {
758 let Some(d) = root.open_dir_optional("boot")? else {
759 return lint_err("Missing /boot directory");
760 };
761
762 let entries: Result<BTreeSet<_>, _> = d
764 .entries()?
765 .into_iter()
766 .map(|v| {
767 let v = v?;
768 anyhow::Ok(v.file_name())
769 })
770 .collect();
771 let mut entries = entries?;
772 {
773 let efidir = Utf8Path::new(EFI_LINUX)
775 .parent()
776 .map(|b| b.as_std_path())
777 .unwrap();
778 entries.remove(efidir.as_os_str());
779 }
780 if entries.is_empty() {
781 return lint_ok();
782 }
783
784 let header = "Found non-empty /boot";
785 let items = entries.iter().map(PathQuotedDisplay::new);
786 format_lint_err_from_items(config, header, items)
787}
788
789#[cfg(test)]
790mod tests {
791 use std::sync::LazyLock;
792
793 use super::*;
794
795 static ALTROOT_LINTS: LazyLock<usize> = LazyLock::new(|| {
796 LINTS
797 .iter()
798 .filter(|lint| lint.root_type != Some(RootType::Running))
799 .count()
800 });
801
802 fn fixture() -> Result<cap_std_ext::cap_tempfile::TempDir> {
803 let tempdir = cap_std_ext::cap_tempfile::tempdir(cap_std::ambient_authority())?;
805 Ok(tempdir)
806 }
807
808 fn passing_fixture() -> Result<cap_std_ext::cap_tempfile::TempDir> {
809 let root = cap_std_ext::cap_tempfile::tempdir(cap_std::ambient_authority())?;
811 for d in API_DIRS {
812 root.create_dir(d)?;
813 }
814 root.create_dir_all("usr/lib/modules/5.7.2")?;
815 root.write("usr/lib/modules/5.7.2/vmlinuz", "vmlinuz")?;
816
817 root.create_dir("boot")?;
818 root.create_dir("sysroot")?;
819 root.symlink_contents("sysroot/ostree", "ostree")?;
820
821 const PREPAREROOT_PATH: &str = "usr/lib/ostree/prepare-root.conf";
822 const PREPAREROOT: &str =
823 include_str!("../../../baseimage/base/usr/lib/ostree/prepare-root.conf");
824 root.create_dir_all(Utf8Path::new(PREPAREROOT_PATH).parent().unwrap())?;
825 root.atomic_write(PREPAREROOT_PATH, PREPAREROOT)?;
826
827 Ok(root)
828 }
829
830 #[test]
831 fn test_var_run() -> Result<()> {
832 let root = &fixture()?;
833 let config = &LintExecutionConfig::default();
834 check_var_run(root, config).unwrap().unwrap();
836 root.create_dir_all("var/run/foo")?;
837 assert!(check_var_run(root, config).unwrap().is_err());
838 root.remove_dir_all("var/run")?;
839 check_var_run(root, config).unwrap().unwrap();
841 Ok(())
842 }
843
844 #[test]
845 fn test_api() -> Result<()> {
846 let root = &passing_fixture()?;
847 let config = &LintExecutionConfig::default();
848 check_api_dirs(root, config).unwrap().unwrap();
850 root.remove_dir("var")?;
851 assert!(check_api_dirs(root, config).unwrap().is_err());
852 root.write("var", "a file for var")?;
853 assert!(check_api_dirs(root, config).unwrap().is_err());
854 Ok(())
855 }
856
857 #[test]
858 fn test_lint_main() -> Result<()> {
859 let root = &passing_fixture()?;
860 let config = &LintExecutionConfig::default();
861 let mut out = Vec::new();
862 let warnings = WarningDisposition::FatalWarnings;
863 let root_type = RootType::Alternative;
864 lint(root, warnings, root_type, [], &mut out, config.no_truncate).unwrap();
865 root.create_dir_all("var/run/foo")?;
866 let mut out = Vec::new();
867 assert!(lint(root, warnings, root_type, [], &mut out, config.no_truncate).is_err());
868 Ok(())
869 }
870
871 #[test]
872 fn test_lint_inner() -> Result<()> {
873 let root = &passing_fixture()?;
874 let config = &LintExecutionConfig::default();
875
876 let mut out = Vec::new();
878 let root_type = RootType::Alternative;
879 let r = lint_inner(root, root_type, config, [], &mut out).unwrap();
880 let running_only_lints = LINTS.len().checked_sub(*ALTROOT_LINTS).unwrap();
881 assert_eq!(r.warnings, 0);
882 assert_eq!(r.fatal, 0);
883 assert_eq!(r.skipped, running_only_lints);
884 assert_eq!(r.passed, *ALTROOT_LINTS);
885
886 let r = lint_inner(root, root_type, config, ["var-log"], &mut out).unwrap();
887 root.create_dir_all("var/log/dnf")?;
889 root.write("var/log/dnf/dnf.log", b"dummy dnf log")?;
890 assert_eq!(r.passed, ALTROOT_LINTS.checked_sub(1).unwrap());
891 assert_eq!(r.fatal, 0);
892 assert_eq!(r.skipped, running_only_lints + 1);
893 assert_eq!(r.warnings, 0);
894
895 let mut out = Vec::new();
897 let r = lint_inner(root, root_type, config, [], &mut out).unwrap();
898 assert_eq!(r.passed, ALTROOT_LINTS.checked_sub(1).unwrap());
899 assert_eq!(r.fatal, 0);
900 assert_eq!(r.skipped, running_only_lints);
901 assert_eq!(r.warnings, 1);
902 Ok(())
903 }
904
905 #[test]
906 fn test_kernel_lint() -> Result<()> {
907 let root = &fixture()?;
908 let config = &LintExecutionConfig::default();
909 check_kernel(root, config).unwrap().unwrap();
911 root.create_dir_all("usr/lib/modules/5.7.2")?;
912 root.write("usr/lib/modules/5.7.2/vmlinuz", "old vmlinuz")?;
913 root.create_dir_all("usr/lib/modules/6.3.1")?;
914 root.write("usr/lib/modules/6.3.1/vmlinuz", "new vmlinuz")?;
915 assert!(check_kernel(root, config).is_err());
916 root.remove_dir_all("usr/lib/modules/5.7.2")?;
917 check_kernel(root, config).unwrap().unwrap();
919 Ok(())
920 }
921
922 #[test]
923 fn test_kargs() -> Result<()> {
924 let root = &fixture()?;
925 let config = &LintExecutionConfig::default();
926 check_parse_kargs(root, config).unwrap().unwrap();
927 root.create_dir_all("usr/lib/bootc")?;
928 root.write("usr/lib/bootc/kargs.d", "not a directory")?;
929 assert!(check_parse_kargs(root, config).is_err());
930 Ok(())
931 }
932
933 #[test]
934 fn test_usr_etc() -> Result<()> {
935 let root = &fixture()?;
936 let config = &LintExecutionConfig::default();
937 check_usretc(root, config).unwrap().unwrap();
939 root.create_dir_all("etc")?;
940 root.create_dir_all("usr/etc")?;
941 assert!(check_usretc(root, config).unwrap().is_err());
942 root.remove_dir_all("etc")?;
943 check_usretc(root, config).unwrap().unwrap();
945 Ok(())
946 }
947
948 #[test]
949 fn test_varlog() -> Result<()> {
950 let root = &fixture()?;
951 let config = &LintExecutionConfig::default();
952 check_varlog(root, config).unwrap().unwrap();
953 root.create_dir_all("var/log")?;
954 check_varlog(root, config).unwrap().unwrap();
955 root.symlink_contents("../../usr/share/doc/systemd/README.logs", "var/log/README")?;
956 check_varlog(root, config).unwrap().unwrap();
957
958 root.atomic_write("var/log/somefile.log", "log contents")?;
959 let Err(e) = check_varlog(root, config).unwrap() else {
960 unreachable!()
961 };
962 similar_asserts::assert_eq!(
963 e.to_string(),
964 "Found non-empty logfiles:\n /var/log/somefile.log\n"
965 );
966 root.create_dir_all("var/log/someproject")?;
967 root.atomic_write("var/log/someproject/audit.log", "audit log")?;
968 root.atomic_write("var/log/someproject/info.log", "info")?;
969 let Err(e) = check_varlog(root, config).unwrap() else {
970 unreachable!()
971 };
972 similar_asserts::assert_eq!(
973 e.to_string(),
974 indoc! { r#"
975 Found non-empty logfiles:
976 /var/log/somefile.log
977 /var/log/someproject/audit.log
978 /var/log/someproject/info.log
979 "# }
980 );
981
982 Ok(())
983 }
984
985 #[test]
986 fn test_boot() -> Result<()> {
987 let root = &passing_fixture()?;
988 let config = &LintExecutionConfig::default();
989 check_boot(&root, config).unwrap().unwrap();
990
991 root.create_dir_all("EFI/Linux")?;
993 root.write("EFI/Linux/foo.efi", b"some dummy efi")?;
994 check_boot(&root, config).unwrap().unwrap();
995
996 root.create_dir("boot/somesubdir")?;
997 let Err(e) = check_boot(&root, config).unwrap() else {
998 unreachable!()
999 };
1000 assert!(e.to_string().contains("somesubdir"));
1001
1002 Ok(())
1003 }
1004
1005 fn run_recursive_lint(
1006 root: &Dir,
1007 f: LintRecursiveFn,
1008 config: &LintExecutionConfig,
1009 ) -> LintResult {
1010 let mut result = lint_ok();
1012 root.walk(
1013 &WalkConfiguration::default()
1014 .noxdev()
1015 .path_base(Path::new("/")),
1016 |e| -> Result<_> {
1017 let r = f(e, config)?;
1018 match r {
1019 Ok(()) => Ok(ControlFlow::Continue(())),
1020 Err(e) => {
1021 result = Ok(Err(e));
1022 Ok(ControlFlow::Break(()))
1023 }
1024 }
1025 },
1026 )?;
1027 result
1028 }
1029
1030 #[test]
1031 fn test_non_utf8() {
1032 use std::{ffi::OsStr, os::unix::ffi::OsStrExt};
1033
1034 let root = &fixture().unwrap();
1035 let config = &LintExecutionConfig::default();
1036
1037 root.create_dir("subdir").unwrap();
1039 root.symlink("self", "self").unwrap();
1041 root.symlink("..", "subdir/parent").unwrap();
1043 root.symlink("does-not-exist", "broken").unwrap();
1045 root.symlink("../../x", "escape").unwrap();
1047 run_recursive_lint(root, check_utf8, config)
1049 .unwrap()
1050 .unwrap();
1051
1052 let baddir = OsStr::from_bytes(b"subdir/2/bad\xffdir");
1054 root.create_dir("subdir/2").unwrap();
1055 root.create_dir(baddir).unwrap();
1056 let Err(err) = run_recursive_lint(root, check_utf8, config).unwrap() else {
1057 unreachable!("Didn't fail");
1058 };
1059 assert_eq!(
1060 err.to_string(),
1061 r#"/subdir/2: Found non-utf8 filename "bad\xFFdir""#
1062 );
1063 root.remove_dir(baddir).unwrap(); run_recursive_lint(root, check_utf8, config)
1065 .unwrap()
1066 .unwrap(); let badfile = OsStr::from_bytes(b"regular\xff");
1070 root.write(badfile, b"Hello, world!\n").unwrap();
1071 let Err(err) = run_recursive_lint(root, check_utf8, config).unwrap() else {
1072 unreachable!("Didn't fail");
1073 };
1074 assert_eq!(
1075 err.to_string(),
1076 r#"/: Found non-utf8 filename "regular\xFF""#
1077 );
1078 root.remove_file(badfile).unwrap(); run_recursive_lint(root, check_utf8, config)
1080 .unwrap()
1081 .unwrap(); root.symlink(badfile, "subdir/good-name").unwrap();
1085 let Err(err) = run_recursive_lint(root, check_utf8, config).unwrap() else {
1086 unreachable!("Didn't fail");
1087 };
1088 assert_eq!(
1089 err.to_string(),
1090 r#"/subdir/good-name: Found non-utf8 symlink target"#
1091 );
1092 root.remove_file("subdir/good-name").unwrap(); run_recursive_lint(root, check_utf8, config)
1094 .unwrap()
1095 .unwrap(); root.symlink(badfile, badfile).unwrap();
1100 let Err(err) = run_recursive_lint(root, check_utf8, config).unwrap() else {
1101 unreachable!("Didn't fail");
1102 };
1103 assert_eq!(
1104 err.to_string(),
1105 r#"/: Found non-utf8 filename "regular\xFF""#
1106 );
1107 root.remove_file(badfile).unwrap(); run_recursive_lint(root, check_utf8, config)
1109 .unwrap()
1110 .unwrap(); }
1112
1113 #[test]
1114 fn test_baseimage_root() -> Result<()> {
1115 let td = fixture()?;
1116 let config = &LintExecutionConfig::default();
1117
1118 assert!(check_baseimage_root(&td, config).unwrap().is_err());
1120
1121 drop(td);
1122 let td = passing_fixture()?;
1123 check_baseimage_root(&td, config).unwrap().unwrap();
1124 Ok(())
1125 }
1126
1127 #[test]
1128 fn test_composefs() -> Result<()> {
1129 let td = fixture()?;
1130 let config = &LintExecutionConfig::default();
1131
1132 assert!(check_composefs(&td, config).unwrap().is_err());
1134
1135 drop(td);
1136 let td = passing_fixture()?;
1137 check_composefs(&td, config).unwrap().unwrap();
1139
1140 td.write(
1141 "usr/lib/ostree/prepare-root.conf",
1142 b"[composefs]\nenabled = false",
1143 )?;
1144 assert!(check_composefs(&td, config).unwrap().is_err());
1146
1147 Ok(())
1148 }
1149
1150 #[test]
1151 fn test_buildah_injected() -> Result<()> {
1152 let td = fixture()?;
1153 let config = &LintExecutionConfig::default();
1154 td.create_dir("etc")?;
1155 assert!(check_buildah_injected(&td, config).unwrap().is_ok());
1156 td.write("etc/hostname", b"")?;
1157 assert!(check_buildah_injected(&td, config).unwrap().is_err());
1158 td.write("etc/hostname", b"some static hostname")?;
1159 assert!(check_buildah_injected(&td, config).unwrap().is_ok());
1160 Ok(())
1161 }
1162
1163 #[test]
1164 fn test_list() {
1165 let mut r = Vec::new();
1166 lint_list(&mut r).unwrap();
1167 let lints: Vec<serde_yaml::Value> = serde_yaml::from_slice(&r).unwrap();
1168 assert_eq!(lints.len(), LINTS.len());
1169 }
1170
1171 #[test]
1172 fn test_format_items_no_truncate() -> Result<()> {
1173 let config = LintExecutionConfig { no_truncate: true };
1174 let header = "Test Header";
1175 let mut output_str = String::new();
1176
1177 let items_empty: Vec<String> = vec![];
1179 format_items(&config, header, items_empty.iter(), &mut output_str)?;
1180 assert_eq!(output_str, "");
1181 output_str.clear();
1182
1183 let items_one = ["item1"];
1185 format_items(&config, header, items_one.iter(), &mut output_str)?;
1186 assert_eq!(output_str, "Test Header:\n item1\n");
1187 output_str.clear();
1188
1189 let items_multiple = (1..=3).map(|v| format!("item{v}")).collect::<Vec<_>>();
1191 format_items(&config, header, items_multiple.iter(), &mut output_str)?;
1192 assert_eq!(output_str, "Test Header:\n item1\n item2\n item3\n");
1193 output_str.clear();
1194
1195 let items_multiple = (1..=8).map(|v| format!("item{v}")).collect::<Vec<_>>();
1197 format_items(&config, header, items_multiple.iter(), &mut output_str)?;
1198 assert_eq!(output_str, "Test Header:\n item1\n item2\n item3\n item4\n item5\n item6\n item7\n item8\n");
1199 output_str.clear();
1200
1201 Ok(())
1202 }
1203
1204 #[test]
1205 fn test_format_items_truncate() -> Result<()> {
1206 let config = LintExecutionConfig::default();
1207 let header = "Test Header";
1208 let mut output_str = String::new();
1209
1210 let items_empty: Vec<String> = vec![];
1212 format_items(&config, header, items_empty.iter(), &mut output_str)?;
1213 assert_eq!(output_str, "");
1214 output_str.clear();
1215
1216 let items_few = ["item1", "item2"];
1218 format_items(&config, header, items_few.iter(), &mut output_str)?;
1219 assert_eq!(output_str, "Test Header:\n item1\n item2\n");
1220 output_str.clear();
1221
1222 let items_exact: Vec<_> = (0..DEFAULT_TRUNCATED_OUTPUT.get())
1224 .map(|i| format!("item{}", i + 1))
1225 .collect();
1226 format_items(&config, header, items_exact.iter(), &mut output_str)?;
1227 let mut expected_output = String::from("Test Header:\n");
1228 for i in 0..DEFAULT_TRUNCATED_OUTPUT.get() {
1229 writeln!(expected_output, " item{}", i + 1)?;
1230 }
1231 assert_eq!(output_str, expected_output);
1232 output_str.clear();
1233
1234 let items_many: Vec<_> = (0..(DEFAULT_TRUNCATED_OUTPUT.get() + 2))
1236 .map(|i| format!("item{}", i + 1))
1237 .collect();
1238 format_items(&config, header, items_many.iter(), &mut output_str)?;
1239 let mut expected_output = String::from("Test Header:\n");
1240 for i in 0..DEFAULT_TRUNCATED_OUTPUT.get() {
1241 writeln!(expected_output, " item{}", i + 1)?;
1242 }
1243 writeln!(expected_output, " ...and 2 more")?;
1244 assert_eq!(output_str, expected_output);
1245 output_str.clear();
1246
1247 let items_one_more: Vec<_> = (0..(DEFAULT_TRUNCATED_OUTPUT.get() + 1))
1249 .map(|i| format!("item{}", i + 1))
1250 .collect();
1251 format_items(&config, header, items_one_more.iter(), &mut output_str)?;
1252 let mut expected_output = String::from("Test Header:\n");
1253 for i in 0..DEFAULT_TRUNCATED_OUTPUT.get() {
1254 writeln!(expected_output, " item{}", i + 1)?;
1255 }
1256 writeln!(expected_output, " ...and 1 more")?;
1257 assert_eq!(output_str, expected_output);
1258 output_str.clear();
1259
1260 Ok(())
1261 }
1262
1263 #[test]
1264 fn test_format_items_display_impl() -> Result<()> {
1265 let config = LintExecutionConfig::default();
1266 let header = "Numbers";
1267 let mut output_str = String::new();
1268
1269 let items_numbers = [1, 2, 3];
1270 format_items(&config, header, items_numbers.iter(), &mut output_str)?;
1271 similar_asserts::assert_eq!(output_str, "Numbers:\n 1\n 2\n 3\n");
1272
1273 Ok(())
1274 }
1275}