Skip to main content

bootc_lib/
fsck.rs

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
9use std::fmt::Write as _;
10use std::future::Future;
11use std::num::NonZeroUsize;
12use std::pin::Pin;
13
14use bootc_utils::collect_until;
15use camino::Utf8PathBuf;
16use cap_std::fs::{Dir, MetadataExt as _};
17use cap_std_ext::cap_std;
18use cap_std_ext::dirext::CapStdExtDirExt;
19use composefs_ctl::composefs;
20use fn_error_context::context;
21use linkme::distributed_slice;
22use ostree_ext::ostree;
23use ostree_ext::ostree_prepareroot::Tristate;
24
25use crate::store::Storage;
26
27use std::os::fd::AsFd;
28
29/// A lint check has failed.
30#[derive(thiserror::Error, Debug)]
31pub(crate) struct FsckError(String);
32
33/// The outer error is for unexpected fatal runtime problems; the
34/// inner error is for the check failing in an expected way.
35pub(crate) type FsckResult = anyhow::Result<std::result::Result<(), FsckError>>;
36
37/// Everything is OK - we didn't encounter a runtime error, and
38/// the targeted check passed.
39pub(crate) fn fsck_ok() -> FsckResult {
40    Ok(Ok(()))
41}
42
43/// We successfully found a failure.
44pub(crate) fn fsck_err(msg: impl AsRef<str>) -> FsckResult {
45    Ok(Err(FsckError::new(msg)))
46}
47
48impl std::fmt::Display for FsckError {
49    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
50        f.write_str(&self.0)
51    }
52}
53
54impl FsckError {
55    fn new(msg: impl AsRef<str>) -> Self {
56        Self(msg.as_ref().to_owned())
57    }
58}
59
60pub(crate) type FsckFn = fn(&Storage) -> FsckResult;
61pub(crate) type AsyncFsckFn = fn(&Storage) -> Pin<Box<dyn Future<Output = FsckResult> + '_>>;
62#[derive(Debug)]
63pub(crate) enum FsckFnImpl {
64    Sync(FsckFn),
65    Async(AsyncFsckFn),
66}
67
68impl From<FsckFn> for FsckFnImpl {
69    fn from(value: FsckFn) -> Self {
70        Self::Sync(value)
71    }
72}
73
74impl From<AsyncFsckFn> for FsckFnImpl {
75    fn from(value: AsyncFsckFn) -> Self {
76        Self::Async(value)
77    }
78}
79
80#[derive(Debug)]
81pub(crate) struct FsckCheck {
82    name: &'static str,
83    ordering: u16,
84    f: FsckFnImpl,
85}
86
87#[distributed_slice]
88pub(crate) static FSCK_CHECKS: [FsckCheck];
89
90impl FsckCheck {
91    pub(crate) const fn new(name: &'static str, ordering: u16, f: FsckFnImpl) -> Self {
92        FsckCheck { name, ordering, f }
93    }
94}
95
96#[distributed_slice(FSCK_CHECKS)]
97static CHECK_RESOLVCONF: FsckCheck =
98    FsckCheck::new("etc-resolvconf", 5, FsckFnImpl::Sync(check_resolvconf));
99/// See <https://github.com/bootc-dev/bootc/pull/1096> and <https://github.com/containers/bootc/pull/1167>
100/// Basically verify that if /usr/etc/resolv.conf exists, it is not a zero-sized file that was
101/// probably injected by buildah and that bootc should have removed.
102///
103/// Note that this fsck check can fail for systems upgraded from old bootc right now, as
104/// we need the *new* bootc to fix it.
105///
106/// But at the current time fsck is an experimental feature that we should only be running
107/// in our CI.
108fn check_resolvconf(storage: &Storage) -> FsckResult {
109    let ostree = match storage.get_ostree() {
110        Ok(o) => o,
111        Err(_) => return fsck_ok(), // Not an ostree system (e.g. composefs-only)
112    };
113    // For now we only check the booted deployment.
114    if ostree.booted_deployment().is_none() {
115        return fsck_ok();
116    }
117    // Read usr/etc/resolv.conf directly.
118    let usr = Dir::open_ambient_dir("/usr", cap_std::ambient_authority())?;
119    let Some(meta) = usr.symlink_metadata_optional("etc/resolv.conf")? else {
120        return fsck_ok();
121    };
122    if meta.is_file() && meta.size() == 0 {
123        return fsck_err("Found usr/etc/resolv.conf as zero-sized file");
124    }
125    fsck_ok()
126}
127
128#[derive(Debug, Default)]
129struct ObjectsVerityState {
130    /// Count of objects with fsverity
131    enabled: u64,
132    /// Count of objects without fsverity
133    disabled: u64,
134    /// Objects which should have fsverity but do not
135    missing: Vec<String>,
136}
137
138/// Check the fsverity state of all regular files in this object directory.
139#[context("Computing verity state")]
140fn verity_state_of_objects(
141    d: &Dir,
142    prefix: &str,
143    expected: bool,
144) -> anyhow::Result<ObjectsVerityState> {
145    let mut enabled = 0;
146    let mut disabled = 0;
147    let mut missing = Vec::new();
148    for ent in d.entries()? {
149        let ent = ent?;
150        if !ent.file_type()?.is_file() {
151            continue;
152        }
153        let name = ent.file_name();
154        let name = name
155            .into_string()
156            .map(Utf8PathBuf::from)
157            .map_err(|_| anyhow::anyhow!("Invalid UTF-8"))?;
158        let Some("file") = name.extension() else {
159            continue;
160        };
161        let f = d.open(&name)?;
162        let r: Option<composefs::fsverity::Sha256HashValue> =
163            composefs::fsverity::measure_verity_opt(f.as_fd())?;
164        drop(f);
165        if r.is_some() {
166            enabled += 1;
167        } else {
168            disabled += 1;
169            if expected {
170                missing.push(format!("{prefix}{name}"));
171            }
172        }
173    }
174    let r = ObjectsVerityState {
175        enabled,
176        disabled,
177        missing,
178    };
179    Ok(r)
180}
181
182async fn verity_state_of_all_objects(
183    repo: &ostree::Repo,
184    expected: bool,
185) -> anyhow::Result<ObjectsVerityState> {
186    // Limit concurrency here
187    const MAX_CONCURRENT: usize = 3;
188
189    let repodir = Dir::reopen_dir(&repo.dfd_borrow())?;
190
191    // It's convenient here to reuse tokio's spawn_blocking as a threadpool basically.
192    let mut joinset = tokio::task::JoinSet::new();
193    let mut results = Vec::new();
194
195    for ent in repodir.read_dir("objects")? {
196        // Block here if the queue is full
197        while joinset.len() >= MAX_CONCURRENT {
198            results.push(joinset.join_next().await.unwrap()??);
199        }
200        let ent = ent?;
201        if !ent.file_type()?.is_dir() {
202            continue;
203        }
204        let name = ent.file_name();
205        let name = name
206            .into_string()
207            .map(Utf8PathBuf::from)
208            .map_err(|_| anyhow::anyhow!("Invalid UTF-8"))?;
209
210        let objdir = ent.open_dir()?;
211        joinset.spawn_blocking(move || verity_state_of_objects(&objdir, name.as_str(), expected));
212    }
213
214    // Drain the remaining tasks.
215    while let Some(output) = joinset.join_next().await {
216        results.push(output??);
217    }
218    // Fold the results.
219    let r = results
220        .into_iter()
221        .fold(ObjectsVerityState::default(), |mut acc, v| {
222            acc.enabled += v.enabled;
223            acc.disabled += v.disabled;
224            acc.missing.extend(v.missing);
225            acc
226        });
227    Ok(r)
228}
229
230#[distributed_slice(FSCK_CHECKS)]
231static CHECK_FSVERITY: FsckCheck =
232    FsckCheck::new("fsverity", 10, FsckFnImpl::Async(check_fsverity));
233fn check_fsverity(storage: &Storage) -> Pin<Box<dyn Future<Output = FsckResult> + '_>> {
234    Box::pin(check_fsverity_inner(storage))
235}
236
237async fn check_fsverity_inner(storage: &Storage) -> FsckResult {
238    let ostree = match storage.get_ostree() {
239        Ok(o) => o,
240        Err(_) => return fsck_ok(), // Not an ostree system (e.g. composefs-only)
241    };
242    let repo = &ostree.repo();
243    let verity_state = ostree_ext::fsverity::is_verity_enabled(repo)?;
244    tracing::debug!(
245        "verity: expected={:?} found={:?}",
246        verity_state.desired,
247        verity_state.enabled
248    );
249
250    let verity_found_state =
251        verity_state_of_all_objects(&ostree.repo(), verity_state.desired == Tristate::Enabled)
252            .await?;
253    let Some((missing, rest)) = collect_until(
254        verity_found_state.missing.iter(),
255        const { NonZeroUsize::new(5).unwrap() },
256    ) else {
257        return fsck_ok();
258    };
259    let mut err = String::from("fsverity enabled, but objects without fsverity:\n");
260    for obj in missing {
261        // SAFETY: Writing into a String
262        writeln!(err, "  {obj}").unwrap();
263    }
264    if rest > 0 {
265        // SAFETY: Writing into a String
266        writeln!(err, "  ...and {rest} more").unwrap();
267    }
268    fsck_err(err)
269}
270
271pub(crate) async fn fsck(storage: &Storage, mut output: impl std::io::Write) -> anyhow::Result<()> {
272    let mut checks = FSCK_CHECKS.static_slice().iter().collect::<Vec<_>>();
273    checks.sort_by(|a, b| a.ordering.cmp(&b.ordering));
274
275    let mut errors = false;
276    for check in checks.iter() {
277        let name = check.name;
278        let r = match check.f {
279            FsckFnImpl::Sync(f) => f(&storage),
280            FsckFnImpl::Async(f) => f(&storage).await,
281        };
282        match r {
283            Ok(Ok(())) => {
284                println!("ok: {name}");
285            }
286            Ok(Err(e)) => {
287                errors = true;
288                writeln!(output, "fsck error: {name}: {e}")?;
289            }
290            Err(e) => {
291                errors = true;
292                writeln!(output, "Unexpected runtime error in check {name}: {e}")?;
293            }
294        }
295    }
296    if errors {
297        anyhow::bail!("Encountered errors")
298    }
299
300    // Run an `ostree fsck` (yes, ostree exposes enough APIs
301    // that we could reimplement this in Rust, but eh)
302    // TODO: Fix https://github.com/bootc-dev/bootc/issues/1216 so we can
303    // do this.
304    // let st = Command::new("ostree")
305    //     .arg("fsck")
306    //     .stdin(std::process::Stdio::inherit())
307    //     .status()?;
308    // if !st.success() {
309    //     anyhow::bail!("ostree fsck failed");
310    // }
311
312    Ok(())
313}