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 cfsctl::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)]
31struct FsckError(String);
32
33type FsckResult = anyhow::Result<std::result::Result<(), FsckError>>;
36
37fn fsck_ok() -> FsckResult {
40 Ok(Ok(()))
41}
42
43fn 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
60type FsckFn = fn(&Storage) -> FsckResult;
61type AsyncFsckFn = fn(&Storage) -> Pin<Box<dyn Future<Output = FsckResult> + '_>>;
62#[derive(Debug)]
63enum 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)]
81struct 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 = storage.get_ostree()?;
110 if ostree.booted_deployment().is_none() {
112 return fsck_ok();
113 }
114 let usr = Dir::open_ambient_dir("/usr", cap_std::ambient_authority())?;
116 let Some(meta) = usr.symlink_metadata_optional("etc/resolv.conf")? else {
117 return fsck_ok();
118 };
119 if meta.is_file() && meta.size() == 0 {
120 return fsck_err("Found usr/etc/resolv.conf as zero-sized file");
121 }
122 fsck_ok()
123}
124
125#[derive(Debug, Default)]
126struct ObjectsVerityState {
127 enabled: u64,
129 disabled: u64,
131 missing: Vec<String>,
133}
134
135#[context("Computing verity state")]
137fn verity_state_of_objects(
138 d: &Dir,
139 prefix: &str,
140 expected: bool,
141) -> anyhow::Result<ObjectsVerityState> {
142 let mut enabled = 0;
143 let mut disabled = 0;
144 let mut missing = Vec::new();
145 for ent in d.entries()? {
146 let ent = ent?;
147 if !ent.file_type()?.is_file() {
148 continue;
149 }
150 let name = ent.file_name();
151 let name = name
152 .into_string()
153 .map(Utf8PathBuf::from)
154 .map_err(|_| anyhow::anyhow!("Invalid UTF-8"))?;
155 let Some("file") = name.extension() else {
156 continue;
157 };
158 let f = d.open(&name)?;
159 let r: Option<composefs::fsverity::Sha256HashValue> =
160 composefs::fsverity::measure_verity_opt(f.as_fd())?;
161 drop(f);
162 if r.is_some() {
163 enabled += 1;
164 } else {
165 disabled += 1;
166 if expected {
167 missing.push(format!("{prefix}{name}"));
168 }
169 }
170 }
171 let r = ObjectsVerityState {
172 enabled,
173 disabled,
174 missing,
175 };
176 Ok(r)
177}
178
179async fn verity_state_of_all_objects(
180 repo: &ostree::Repo,
181 expected: bool,
182) -> anyhow::Result<ObjectsVerityState> {
183 const MAX_CONCURRENT: usize = 3;
185
186 let repodir = Dir::reopen_dir(&repo.dfd_borrow())?;
187
188 let mut joinset = tokio::task::JoinSet::new();
190 let mut results = Vec::new();
191
192 for ent in repodir.read_dir("objects")? {
193 while joinset.len() >= MAX_CONCURRENT {
195 results.push(joinset.join_next().await.unwrap()??);
196 }
197 let ent = ent?;
198 if !ent.file_type()?.is_dir() {
199 continue;
200 }
201 let name = ent.file_name();
202 let name = name
203 .into_string()
204 .map(Utf8PathBuf::from)
205 .map_err(|_| anyhow::anyhow!("Invalid UTF-8"))?;
206
207 let objdir = ent.open_dir()?;
208 joinset.spawn_blocking(move || verity_state_of_objects(&objdir, name.as_str(), expected));
209 }
210
211 while let Some(output) = joinset.join_next().await {
213 results.push(output??);
214 }
215 let r = results
217 .into_iter()
218 .fold(ObjectsVerityState::default(), |mut acc, v| {
219 acc.enabled += v.enabled;
220 acc.disabled += v.disabled;
221 acc.missing.extend(v.missing);
222 acc
223 });
224 Ok(r)
225}
226
227#[distributed_slice(FSCK_CHECKS)]
228static CHECK_FSVERITY: FsckCheck =
229 FsckCheck::new("fsverity", 10, FsckFnImpl::Async(check_fsverity));
230fn check_fsverity(storage: &Storage) -> Pin<Box<dyn Future<Output = FsckResult> + '_>> {
231 Box::pin(check_fsverity_inner(storage))
232}
233
234async fn check_fsverity_inner(storage: &Storage) -> FsckResult {
235 let ostree = storage.get_ostree()?;
236 let repo = &ostree.repo();
237 let verity_state = ostree_ext::fsverity::is_verity_enabled(repo)?;
238 tracing::debug!(
239 "verity: expected={:?} found={:?}",
240 verity_state.desired,
241 verity_state.enabled
242 );
243
244 let verity_found_state =
245 verity_state_of_all_objects(&ostree.repo(), verity_state.desired == Tristate::Enabled)
246 .await?;
247 let Some((missing, rest)) = collect_until(
248 verity_found_state.missing.iter(),
249 const { NonZeroUsize::new(5).unwrap() },
250 ) else {
251 return fsck_ok();
252 };
253 let mut err = String::from("fsverity enabled, but objects without fsverity:\n");
254 for obj in missing {
255 writeln!(err, " {obj}").unwrap();
257 }
258 if rest > 0 {
259 writeln!(err, " ...and {rest} more").unwrap();
261 }
262 fsck_err(err)
263}
264
265pub(crate) async fn fsck(storage: &Storage, mut output: impl std::io::Write) -> anyhow::Result<()> {
266 let mut checks = FSCK_CHECKS.static_slice().iter().collect::<Vec<_>>();
267 checks.sort_by(|a, b| a.ordering.cmp(&b.ordering));
268
269 let mut errors = false;
270 for check in checks.iter() {
271 let name = check.name;
272 let r = match check.f {
273 FsckFnImpl::Sync(f) => f(&storage),
274 FsckFnImpl::Async(f) => f(&storage).await,
275 };
276 match r {
277 Ok(Ok(())) => {
278 println!("ok: {name}");
279 }
280 Ok(Err(e)) => {
281 errors = true;
282 writeln!(output, "fsck error: {name}: {e}")?;
283 }
284 Err(e) => {
285 errors = true;
286 writeln!(output, "Unexpected runtime error in check {name}: {e}")?;
287 }
288 }
289 }
290 if errors {
291 anyhow::bail!("Encountered errors")
292 }
293
294 Ok(())
307}