1#![allow(unsafe_code)]
8
9use std::future::Future;
10use std::num::NonZeroUsize;
11use std::pin::Pin;
12
13use bootc_utils::collect_until;
14use camino::Utf8PathBuf;
15use cap_std::fs::{Dir, MetadataExt as _};
16use cap_std_ext::cap_std;
17use cap_std_ext::dirext::CapStdExtDirExt;
18use composefs_ctl::composefs;
19use fn_error_context::context;
20use linkme::distributed_slice;
21use ostree_ext::ostree;
22use ostree_ext::ostree_prepareroot::Tristate;
23
24use crate::store::Storage;
25
26use std::os::fd::AsFd;
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub(crate) enum CheckFindingCode {
31 ResolvconfZeroSized,
33 ObjectWithoutFsverity,
35}
36
37impl std::fmt::Display for CheckFindingCode {
38 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
39 std::fmt::Debug::fmt(self, f)
40 }
41}
42
43#[derive(Debug, PartialEq, Eq)]
45pub(crate) struct CheckFinding {
46 code: CheckFindingCode,
47 subject: String,
49 detail: String,
50}
51
52impl CheckFinding {
53 fn new(code: CheckFindingCode, subject: impl Into<String>, detail: impl Into<String>) -> Self {
54 Self {
55 code,
56 subject: subject.into(),
57 detail: detail.into(),
58 }
59 }
60}
61
62impl std::fmt::Display for CheckFinding {
63 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
64 write!(f, "{}: {}: {}", self.code, self.subject, self.detail)
65 }
66}
67
68pub(crate) type FsckResult = anyhow::Result<Vec<CheckFinding>>;
73
74const MAX_PRINTED_FINDINGS: NonZeroUsize = NonZeroUsize::new(5).unwrap();
76
77pub(crate) type FsckFn = fn(&Storage) -> FsckResult;
78pub(crate) type AsyncFsckFn = fn(&Storage) -> Pin<Box<dyn Future<Output = FsckResult> + '_>>;
79#[derive(Debug)]
80pub(crate) enum FsckFnImpl {
81 Sync(FsckFn),
82 Async(AsyncFsckFn),
83}
84
85impl From<FsckFn> for FsckFnImpl {
86 fn from(value: FsckFn) -> Self {
87 Self::Sync(value)
88 }
89}
90
91impl From<AsyncFsckFn> for FsckFnImpl {
92 fn from(value: AsyncFsckFn) -> Self {
93 Self::Async(value)
94 }
95}
96
97#[derive(Debug)]
98pub(crate) struct FsckCheck {
99 name: &'static str,
100 ordering: u16,
101 f: FsckFnImpl,
102}
103
104#[distributed_slice]
105pub(crate) static FSCK_CHECKS: [FsckCheck];
106
107impl FsckCheck {
108 pub(crate) const fn new(name: &'static str, ordering: u16, f: FsckFnImpl) -> Self {
109 FsckCheck { name, ordering, f }
110 }
111}
112
113#[distributed_slice(FSCK_CHECKS)]
114static CHECK_RESOLVCONF: FsckCheck =
115 FsckCheck::new("etc-resolvconf", 5, FsckFnImpl::Sync(check_resolvconf));
116fn check_resolvconf(storage: &Storage) -> FsckResult {
126 let ostree = match storage.get_ostree() {
127 Ok(o) => o,
128 Err(_) => return Ok(Vec::new()), };
130 if ostree.booted_deployment().is_none() {
132 return Ok(Vec::new());
133 }
134 let usr = Dir::open_ambient_dir("/usr", cap_std::ambient_authority())?;
135 check_resolvconf_in(&usr)
136}
137
138const RESOLVCONF_SUBJECT: &str = "usr/etc/resolv.conf";
140
141fn check_resolvconf_in(usr: &Dir) -> FsckResult {
143 let Some(meta) = usr.symlink_metadata_optional("etc/resolv.conf")? else {
145 return Ok(Vec::new());
146 };
147 if meta.is_file() && meta.size() == 0 {
148 return Ok(vec![CheckFinding::new(
149 CheckFindingCode::ResolvconfZeroSized,
150 RESOLVCONF_SUBJECT,
151 "zero-sized file",
152 )]);
153 }
154 Ok(Vec::new())
155}
156
157#[derive(Debug, Default)]
158struct ObjectsVerityState {
159 enabled: u64,
161 disabled: u64,
163 missing: Vec<String>,
165}
166
167#[context("Computing verity state")]
169fn verity_state_of_objects(
170 d: &Dir,
171 prefix: &str,
172 expected: bool,
173) -> anyhow::Result<ObjectsVerityState> {
174 let mut enabled = 0;
175 let mut disabled = 0;
176 let mut missing = Vec::new();
177 for ent in d.entries()? {
178 let ent = ent?;
179 if !ent.file_type()?.is_file() {
180 continue;
181 }
182 let name = ent.file_name();
183 let name = name
184 .into_string()
185 .map(Utf8PathBuf::from)
186 .map_err(|_| anyhow::anyhow!("Invalid UTF-8"))?;
187 let Some("file") = name.extension() else {
188 continue;
189 };
190 let f = d.open(&name)?;
191 let r: Option<composefs::fsverity::Sha256HashValue> =
192 composefs::fsverity::measure_verity_opt(f.as_fd())?;
193 drop(f);
194 if r.is_some() {
195 enabled += 1;
196 } else {
197 disabled += 1;
198 if expected {
199 missing.push(format!("{prefix}{name}"));
200 }
201 }
202 }
203 let r = ObjectsVerityState {
204 enabled,
205 disabled,
206 missing,
207 };
208 Ok(r)
209}
210
211async fn verity_state_of_all_objects(
212 repo: &ostree::Repo,
213 expected: bool,
214) -> anyhow::Result<ObjectsVerityState> {
215 const MAX_CONCURRENT: usize = 3;
217
218 let repodir = Dir::reopen_dir(&repo.dfd_borrow())?;
219
220 let mut joinset = tokio::task::JoinSet::new();
222 let mut results = Vec::new();
223
224 for ent in repodir.read_dir("objects")? {
225 while joinset.len() >= MAX_CONCURRENT {
227 results.push(joinset.join_next().await.unwrap()??);
228 }
229 let ent = ent?;
230 if !ent.file_type()?.is_dir() {
231 continue;
232 }
233 let name = ent.file_name();
234 let name = name
235 .into_string()
236 .map(Utf8PathBuf::from)
237 .map_err(|_| anyhow::anyhow!("Invalid UTF-8"))?;
238
239 let objdir = ent.open_dir()?;
240 joinset.spawn_blocking(move || verity_state_of_objects(&objdir, name.as_str(), expected));
241 }
242
243 while let Some(output) = joinset.join_next().await {
245 results.push(output??);
246 }
247 let r = results
249 .into_iter()
250 .fold(ObjectsVerityState::default(), |mut acc, v| {
251 acc.enabled += v.enabled;
252 acc.disabled += v.disabled;
253 acc.missing.extend(v.missing);
254 acc
255 });
256 Ok(r)
257}
258
259#[distributed_slice(FSCK_CHECKS)]
260static CHECK_FSVERITY: FsckCheck =
261 FsckCheck::new("fsverity", 10, FsckFnImpl::Async(check_fsverity));
262fn check_fsverity(storage: &Storage) -> Pin<Box<dyn Future<Output = FsckResult> + '_>> {
263 Box::pin(check_fsverity_inner(storage))
264}
265
266async fn check_fsverity_inner(storage: &Storage) -> FsckResult {
267 let ostree = match storage.get_ostree() {
268 Ok(o) => o,
269 Err(_) => return Ok(Vec::new()), };
271 let repo = &ostree.repo();
272 let verity_state = ostree_ext::fsverity::is_verity_enabled(repo)?;
273 tracing::debug!(
274 "verity: expected={:?} found={:?}",
275 verity_state.desired,
276 verity_state.enabled
277 );
278
279 let verity_found_state =
280 verity_state_of_all_objects(&ostree.repo(), verity_state.desired == Tristate::Enabled)
281 .await?;
282 let findings = verity_found_state
283 .missing
284 .into_iter()
285 .map(|obj| {
286 CheckFinding::new(
287 CheckFindingCode::ObjectWithoutFsverity,
288 obj,
289 "missing fsverity, which is enabled for the repository",
290 )
291 })
292 .collect();
293 Ok(findings)
294}
295
296fn print_check_result(
298 name: &str,
299 result: &FsckResult,
300 mut output: impl std::io::Write,
301) -> std::io::Result<bool> {
302 let findings = match result {
303 Ok(findings) => findings,
304 Err(e) => {
305 writeln!(output, "Unexpected runtime error in check {name}: {e:#}")?;
306 return Ok(false);
307 }
308 };
309 let Some((shown, rest)) = collect_until(findings.iter(), MAX_PRINTED_FINDINGS) else {
310 writeln!(output, "ok: {name}")?;
311 return Ok(true);
312 };
313 for finding in shown {
314 writeln!(output, "fsck error: {name}: {finding}")?;
315 }
316 if rest > 0 {
317 writeln!(output, "fsck error: {name}: ...and {rest} more")?;
318 }
319 Ok(false)
320}
321
322pub(crate) async fn fsck(storage: &Storage, mut output: impl std::io::Write) -> anyhow::Result<()> {
323 let mut checks = FSCK_CHECKS.static_slice().iter().collect::<Vec<_>>();
324 checks.sort_by(|a, b| a.ordering.cmp(&b.ordering));
325
326 let mut errors = false;
327 for check in checks.iter() {
328 let name = check.name;
329 let r = match check.f {
330 FsckFnImpl::Sync(f) => f(&storage),
331 FsckFnImpl::Async(f) => f(&storage).await,
332 };
333 if !print_check_result(name, &r, &mut output)? {
334 errors = true;
335 }
336 }
337 if errors {
338 anyhow::bail!("Encountered errors")
339 }
340
341 Ok(())
354}
355
356#[cfg(test)]
357mod tests {
358 use super::*;
359 use cap_std_ext::cap_tempfile;
360
361 #[test]
362 fn test_check_resolvconf() -> anyhow::Result<()> {
363 let zero_sized = || {
364 vec![CheckFinding::new(
365 CheckFindingCode::ResolvconfZeroSized,
366 RESOLVCONF_SUBJECT,
367 "zero-sized file",
368 )]
369 };
370 type Setup = fn(&Dir) -> std::io::Result<()>;
372 let cases: &[(&str, Setup, Vec<CheckFinding>)] = &[
373 ("missing", |_| Ok(()), Vec::new()),
374 (
375 "zero-sized",
376 |d| d.write("etc/resolv.conf", ""),
377 zero_sized(),
378 ),
379 (
380 "nonempty",
381 |d| d.write("etc/resolv.conf", "nameserver 192.0.2.1\n"),
382 Vec::new(),
383 ),
384 (
385 "symlink",
386 |d| d.symlink("../run/resolv.conf", "etc/resolv.conf"),
387 Vec::new(),
388 ),
389 ("directory", |d| d.create_dir("etc/resolv.conf"), Vec::new()),
390 ];
391 for (name, setup, expected) in cases {
392 let usr = cap_tempfile::tempdir(cap_std::ambient_authority())?;
393 usr.create_dir("etc")?;
394 setup(&usr)?;
395 assert_eq!(&check_resolvconf_in(&usr)?, expected, "case {name}");
396 }
397 Ok(())
398 }
399
400 #[test]
401 fn test_print_check_result() {
402 let objects = |n: usize| -> FsckResult {
403 Ok((0..n)
404 .map(|i| {
405 CheckFinding::new(
406 CheckFindingCode::ObjectWithoutFsverity,
407 format!("{i:02}.file"),
408 "detail",
409 )
410 })
411 .collect())
412 };
413 let cases: &[(&str, FsckResult, bool, &str)] = &[
414 ("no findings", Ok(Vec::new()), true, "ok: c\n"),
415 (
416 "one finding",
417 objects(1),
418 false,
419 "fsck error: c: ObjectWithoutFsverity: 00.file: detail\n",
420 ),
421 (
422 "at the limit",
423 objects(5),
424 false,
425 indoc::indoc! {"
426 fsck error: c: ObjectWithoutFsverity: 00.file: detail
427 fsck error: c: ObjectWithoutFsverity: 01.file: detail
428 fsck error: c: ObjectWithoutFsverity: 02.file: detail
429 fsck error: c: ObjectWithoutFsverity: 03.file: detail
430 fsck error: c: ObjectWithoutFsverity: 04.file: detail
431 "},
432 ),
433 (
434 "over the limit",
435 objects(8),
436 false,
437 indoc::indoc! {"
438 fsck error: c: ObjectWithoutFsverity: 00.file: detail
439 fsck error: c: ObjectWithoutFsverity: 01.file: detail
440 fsck error: c: ObjectWithoutFsverity: 02.file: detail
441 fsck error: c: ObjectWithoutFsverity: 03.file: detail
442 fsck error: c: ObjectWithoutFsverity: 04.file: detail
443 fsck error: c: ...and 3 more
444 "},
445 ),
446 (
447 "runtime error",
448 Err(anyhow::anyhow!("inner").context("outer")),
449 false,
450 "Unexpected runtime error in check c: outer: inner\n",
451 ),
452 ];
453 for (name, result, passed, expected) in cases {
454 let mut output = Vec::new();
455 let r = print_check_result("c", result, &mut output).unwrap();
456 assert_eq!(r, *passed, "case {name}");
457 assert_eq!(String::from_utf8(output).unwrap(), *expected, "case {name}");
458 }
459 }
460}