Skip to content

Commit 7145132

Browse files
committed
internals: Add new bootc internals fsck
Split this out of the fsverity PR. We obviously want a `fsck` command. This starts by doing just two checks: - A verification of `etc/resolv.conf`; this tests 98995f6 - Just run `ostree fsck` But obvious things we should be adding here are: - Verifying kargs - Verifying LBIs etc. Signed-off-by: Colin Walters <[email protected]>
1 parent 190085d commit 7145132

File tree

6 files changed

+182
-0
lines changed

6 files changed

+182
-0
lines changed

docs/src/SUMMARY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
# Experimental features
5656

5757
- [bootc image](experimental-bootc-image.md)
58+
- [fsck](experimental-fsck.md)
5859
- [--progress-fd](experimental-progress-fd.md)
5960

6061
# More information

docs/src/experimental-fsck.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# bootc internals fsck
2+
3+
Experimental features are subject to change or removal. Please
4+
do provide feedback on them.
5+
6+
## Using `bootc internals fsck`
7+
8+
This command expects a booted system, and performs consistency checks
9+
in a read-only fashion.

lib/src/cli.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -423,6 +423,8 @@ pub(crate) enum InternalsOpts {
423423
},
424424
#[clap(subcommand)]
425425
Fsverity(FsverityOpts),
426+
/// Perform consistency checking.
427+
Fsck,
426428
/// Perform cleanup actions
427429
Cleanup,
428430
/// Proxy frontend for the `ostree-ext` CLI.
@@ -1173,6 +1175,11 @@ async fn run_from_opt(opt: Opt) -> Result<()> {
11731175
Ok(())
11741176
}
11751177
},
1178+
InternalsOpts::Fsck => {
1179+
let sysroot = &get_storage().await?;
1180+
crate::fsck::fsck(&sysroot, std::io::stdout().lock()).await?;
1181+
Ok(())
1182+
}
11761183
InternalsOpts::FixupEtcFstab => crate::deploy::fixup_etc_fstab(&root),
11771184
InternalsOpts::PrintJsonSchema { of } => {
11781185
let schema = match of {

lib/src/fsck.rs

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
//! # Perform consistency checking.
2+
//!
3+
//! This is an internal module, backing the experimental `bootc internals fsck`
4+
//! command.
5+
6+
// Unfortunately needed here to work with linkme
7+
#![allow(unsafe_code)]
8+
9+
use std::future::Future;
10+
use std::pin::Pin;
11+
use std::process::Command;
12+
13+
use cap_std::fs::{Dir, MetadataExt as _};
14+
use cap_std_ext::cap_std;
15+
use cap_std_ext::dirext::CapStdExtDirExt;
16+
use linkme::distributed_slice;
17+
18+
use crate::store::Storage;
19+
20+
/// A lint check has failed.
21+
#[derive(thiserror::Error, Debug)]
22+
struct FsckError(String);
23+
24+
/// The outer error is for unexpected fatal runtime problems; the
25+
/// inner error is for the check failing in an expected way.
26+
type FsckResult = anyhow::Result<std::result::Result<(), FsckError>>;
27+
28+
/// Everything is OK - we didn't encounter a runtime error, and
29+
/// the targeted check passed.
30+
fn fsck_ok() -> FsckResult {
31+
Ok(Ok(()))
32+
}
33+
34+
/// We successfully found a failure.
35+
fn fsck_err(msg: impl AsRef<str>) -> FsckResult {
36+
Ok(Err(FsckError::new(msg)))
37+
}
38+
39+
impl std::fmt::Display for FsckError {
40+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
41+
f.write_str(&self.0)
42+
}
43+
}
44+
45+
impl FsckError {
46+
fn new(msg: impl AsRef<str>) -> Self {
47+
Self(msg.as_ref().to_owned())
48+
}
49+
}
50+
51+
type FsckFn = fn(&Storage) -> FsckResult;
52+
type AsyncFsckFn = fn(&Storage) -> Pin<Box<dyn Future<Output = FsckResult> + '_>>;
53+
#[derive(Debug)]
54+
enum FsckFnImpl {
55+
Sync(FsckFn),
56+
Async(AsyncFsckFn),
57+
}
58+
59+
impl From<FsckFn> for FsckFnImpl {
60+
fn from(value: FsckFn) -> Self {
61+
Self::Sync(value)
62+
}
63+
}
64+
65+
impl From<AsyncFsckFn> for FsckFnImpl {
66+
fn from(value: AsyncFsckFn) -> Self {
67+
Self::Async(value)
68+
}
69+
}
70+
71+
#[derive(Debug)]
72+
struct FsckCheck {
73+
name: &'static str,
74+
ordering: u16,
75+
f: FsckFnImpl,
76+
}
77+
78+
#[distributed_slice]
79+
pub(crate) static FSCK_CHECKS: [FsckCheck];
80+
81+
impl FsckCheck {
82+
pub(crate) const fn new(name: &'static str, ordering: u16, f: FsckFnImpl) -> Self {
83+
FsckCheck { name, ordering, f }
84+
}
85+
}
86+
87+
#[distributed_slice(FSCK_CHECKS)]
88+
static CHECK_RESOLVCONF: FsckCheck =
89+
FsckCheck::new("etc-resolvconf", 5, FsckFnImpl::Sync(check_resolvconf));
90+
/// See https://github.com/containers/bootc/pull/1096 and https://github.com/containers/bootc/pull/1167
91+
/// Basically verify that if /usr/etc/resolv.conf exists, it is not a zero-sized file that was
92+
/// probably injected by buildah and that bootc should have removed.
93+
///
94+
/// Note that this fsck check can fail for systems upgraded from old bootc right now, as
95+
/// we need the *new* bootc to fix it.
96+
///
97+
/// But at the current time fsck is an experimental feature that we should only be running
98+
/// in our CI.
99+
fn check_resolvconf(storage: &Storage) -> FsckResult {
100+
// For now we only check the booted deployment.
101+
if storage.booted_deployment().is_none() {
102+
return fsck_ok();
103+
}
104+
// Read usr/etc/resolv.conf directly.
105+
let usr = Dir::open_ambient_dir("/usr", cap_std::ambient_authority())?;
106+
let Some(meta) = usr.symlink_metadata_optional("etc/resolv.conf")? else {
107+
return fsck_ok();
108+
};
109+
if meta.is_file() && meta.size() == 0 {
110+
return fsck_err("Found usr/etc/resolv.conf as zero-sized file");
111+
}
112+
fsck_ok()
113+
}
114+
115+
pub(crate) async fn fsck(storage: &Storage, mut output: impl std::io::Write) -> anyhow::Result<()> {
116+
let mut checks = FSCK_CHECKS.static_slice().iter().collect::<Vec<_>>();
117+
checks.sort_by(|a, b| a.ordering.cmp(&b.ordering));
118+
119+
let mut errors = false;
120+
for check in checks.iter() {
121+
let name = check.name;
122+
let r = match check.f {
123+
FsckFnImpl::Sync(f) => f(&storage),
124+
FsckFnImpl::Async(f) => f(&storage).await,
125+
};
126+
match r {
127+
Ok(Ok(())) => {
128+
println!("ok: {name}");
129+
}
130+
Ok(Err(e)) => {
131+
errors = true;
132+
writeln!(output, "fsck error: {name}: {e}")?;
133+
}
134+
Err(e) => {
135+
errors = true;
136+
writeln!(output, "Unexpected runtime error in check {name}: {e}")?;
137+
}
138+
}
139+
}
140+
if errors {
141+
anyhow::bail!("Encountered errors")
142+
}
143+
144+
// Run an `ostree fsck` (yes, ostree exposes enough APIs
145+
// that we could reimplement this in Rust, but eh)
146+
let st = Command::new("ostree")
147+
.arg("fsck")
148+
.stdin(std::process::Stdio::inherit())
149+
.status()?;
150+
if !st.success() {
151+
anyhow::bail!("ostree fsck failed");
152+
}
153+
154+
Ok(())
155+
}

lib/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
mod boundimage;
88
pub mod cli;
99
pub(crate) mod deploy;
10+
pub(crate) mod fsck;
1011
pub(crate) mod generator;
1112
mod glyph;
1213
mod image;
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
use std assert
2+
use tap.nu
3+
4+
tap begin "Run fsck"
5+
6+
# That's it, just ensure we've run a fsck on our basic install.
7+
bootc internals fsck
8+
9+
tap ok

0 commit comments

Comments
 (0)