1#![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#[derive(thiserror::Error, Debug)]
31pub(crate) struct FsckError(String);
32
33pub(crate) type FsckResult = anyhow::Result<std::result::Result<(), FsckError>>;
36
37pub(crate) fn fsck_ok() -> FsckResult {
40 Ok(Ok(()))
41}
42
43pub(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));
99fn check_resolvconf(storage: &Storage) -> FsckResult {
109 let ostree = match storage.get_ostree() {
110 Ok(o) => o,
111 Err(_) => return fsck_ok(), };
113 if ostree.booted_deployment().is_none() {
115 return fsck_ok();
116 }
117 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 enabled: u64,
132 disabled: u64,
134 missing: Vec<String>,
136}
137
138#[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 const MAX_CONCURRENT: usize = 3;
188
189 let repodir = Dir::reopen_dir(&repo.dfd_borrow())?;
190
191 let mut joinset = tokio::task::JoinSet::new();
193 let mut results = Vec::new();
194
195 for ent in repodir.read_dir("objects")? {
196 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 while let Some(output) = joinset.join_next().await {
216 results.push(output??);
217 }
218 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(), };
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 writeln!(err, " {obj}").unwrap();
263 }
264 if rest > 0 {
265 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 Ok(())
313}