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 fn_error_context::context;
20use linkme::distributed_slice;
21use ostree_ext::ostree_prepareroot::Tristate;
22use ostree_ext::{composefs, ostree};
23
24use crate::store::Storage;
25
26use std::os::fd::AsFd;
27
28#[derive(thiserror::Error, Debug)]
30struct FsckError(String);
31
32type FsckResult = anyhow::Result<std::result::Result<(), FsckError>>;
35
36fn fsck_ok() -> FsckResult {
39 Ok(Ok(()))
40}
41
42fn fsck_err(msg: impl AsRef<str>) -> FsckResult {
44 Ok(Err(FsckError::new(msg)))
45}
46
47impl std::fmt::Display for FsckError {
48 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
49 f.write_str(&self.0)
50 }
51}
52
53impl FsckError {
54 fn new(msg: impl AsRef<str>) -> Self {
55 Self(msg.as_ref().to_owned())
56 }
57}
58
59type FsckFn = fn(&Storage) -> FsckResult;
60type AsyncFsckFn = fn(&Storage) -> Pin<Box<dyn Future<Output = FsckResult> + '_>>;
61#[derive(Debug)]
62enum FsckFnImpl {
63 Sync(FsckFn),
64 Async(AsyncFsckFn),
65}
66
67impl From<FsckFn> for FsckFnImpl {
68 fn from(value: FsckFn) -> Self {
69 Self::Sync(value)
70 }
71}
72
73impl From<AsyncFsckFn> for FsckFnImpl {
74 fn from(value: AsyncFsckFn) -> Self {
75 Self::Async(value)
76 }
77}
78
79#[derive(Debug)]
80struct FsckCheck {
81 name: &'static str,
82 ordering: u16,
83 f: FsckFnImpl,
84}
85
86#[distributed_slice]
87pub(crate) static FSCK_CHECKS: [FsckCheck];
88
89impl FsckCheck {
90 pub(crate) const fn new(name: &'static str, ordering: u16, f: FsckFnImpl) -> Self {
91 FsckCheck { name, ordering, f }
92 }
93}
94
95#[distributed_slice(FSCK_CHECKS)]
96static CHECK_RESOLVCONF: FsckCheck =
97 FsckCheck::new("etc-resolvconf", 5, FsckFnImpl::Sync(check_resolvconf));
98fn check_resolvconf(storage: &Storage) -> FsckResult {
108 let ostree = storage.get_ostree()?;
109 if ostree.booted_deployment().is_none() {
111 return fsck_ok();
112 }
113 let usr = Dir::open_ambient_dir("/usr", cap_std::ambient_authority())?;
115 let Some(meta) = usr.symlink_metadata_optional("etc/resolv.conf")? else {
116 return fsck_ok();
117 };
118 if meta.is_file() && meta.size() == 0 {
119 return fsck_err("Found usr/etc/resolv.conf as zero-sized file");
120 }
121 fsck_ok()
122}
123
124#[derive(Debug, Default)]
125struct ObjectsVerityState {
126 enabled: u64,
128 disabled: u64,
130 missing: Vec<String>,
132}
133
134#[context("Computing verity state")]
136fn verity_state_of_objects(
137 d: &Dir,
138 prefix: &str,
139 expected: bool,
140) -> anyhow::Result<ObjectsVerityState> {
141 let mut enabled = 0;
142 let mut disabled = 0;
143 let mut missing = Vec::new();
144 for ent in d.entries()? {
145 let ent = ent?;
146 if !ent.file_type()?.is_file() {
147 continue;
148 }
149 let name = ent.file_name();
150 let name = name
151 .into_string()
152 .map(Utf8PathBuf::from)
153 .map_err(|_| anyhow::anyhow!("Invalid UTF-8"))?;
154 let Some("file") = name.extension() else {
155 continue;
156 };
157 let f = d.open(&name)?;
158 let r: Option<composefs::fsverity::Sha256HashValue> =
159 composefs::fsverity::measure_verity_opt(f.as_fd())?;
160 drop(f);
161 if r.is_some() {
162 enabled += 1;
163 } else {
164 disabled += 1;
165 if expected {
166 missing.push(format!("{prefix}{name}"));
167 }
168 }
169 }
170 let r = ObjectsVerityState {
171 enabled,
172 disabled,
173 missing,
174 };
175 Ok(r)
176}
177
178async fn verity_state_of_all_objects(
179 repo: &ostree::Repo,
180 expected: bool,
181) -> anyhow::Result<ObjectsVerityState> {
182 const MAX_CONCURRENT: usize = 3;
184
185 let repodir = Dir::reopen_dir(&repo.dfd_borrow())?;
186
187 let mut joinset = tokio::task::JoinSet::new();
189 let mut results = Vec::new();
190
191 for ent in repodir.read_dir("objects")? {
192 while joinset.len() >= MAX_CONCURRENT {
194 results.push(joinset.join_next().await.unwrap()??);
195 }
196 let ent = ent?;
197 if !ent.file_type()?.is_dir() {
198 continue;
199 }
200 let name = ent.file_name();
201 let name = name
202 .into_string()
203 .map(Utf8PathBuf::from)
204 .map_err(|_| anyhow::anyhow!("Invalid UTF-8"))?;
205
206 let objdir = ent.open_dir()?;
207 joinset.spawn_blocking(move || verity_state_of_objects(&objdir, name.as_str(), expected));
208 }
209
210 while let Some(output) = joinset.join_next().await {
212 results.push(output??);
213 }
214 let r = results
216 .into_iter()
217 .fold(ObjectsVerityState::default(), |mut acc, v| {
218 acc.enabled += v.enabled;
219 acc.disabled += v.disabled;
220 acc.missing.extend(v.missing);
221 acc
222 });
223 Ok(r)
224}
225
226#[distributed_slice(FSCK_CHECKS)]
227static CHECK_FSVERITY: FsckCheck =
228 FsckCheck::new("fsverity", 10, FsckFnImpl::Async(check_fsverity));
229fn check_fsverity(storage: &Storage) -> Pin<Box<dyn Future<Output = FsckResult> + '_>> {
230 Box::pin(check_fsverity_inner(storage))
231}
232
233async fn check_fsverity_inner(storage: &Storage) -> FsckResult {
234 let ostree = storage.get_ostree()?;
235 let repo = &ostree.repo();
236 let verity_state = ostree_ext::fsverity::is_verity_enabled(repo)?;
237 tracing::debug!(
238 "verity: expected={:?} found={:?}",
239 verity_state.desired,
240 verity_state.enabled
241 );
242
243 let verity_found_state =
244 verity_state_of_all_objects(&ostree.repo(), verity_state.desired == Tristate::Enabled)
245 .await?;
246 let Some((missing, rest)) = collect_until(
247 verity_found_state.missing.iter(),
248 const { NonZeroUsize::new(5).unwrap() },
249 ) else {
250 return fsck_ok();
251 };
252 let mut err = String::from("fsverity enabled, but objects without fsverity:\n");
253 for obj in missing {
254 writeln!(err, " {obj}").unwrap();
256 }
257 if rest > 0 {
258 writeln!(err, " ...and {rest} more").unwrap();
260 }
261 fsck_err(err)
262}
263
264pub(crate) async fn fsck(storage: &Storage, mut output: impl std::io::Write) -> anyhow::Result<()> {
265 let mut checks = FSCK_CHECKS.static_slice().iter().collect::<Vec<_>>();
266 checks.sort_by(|a, b| a.ordering.cmp(&b.ordering));
267
268 let mut errors = false;
269 for check in checks.iter() {
270 let name = check.name;
271 let r = match check.f {
272 FsckFnImpl::Sync(f) => f(&storage),
273 FsckFnImpl::Async(f) => f(&storage).await,
274 };
275 match r {
276 Ok(Ok(())) => {
277 println!("ok: {name}");
278 }
279 Ok(Err(e)) => {
280 errors = true;
281 writeln!(output, "fsck error: {name}: {e}")?;
282 }
283 Err(e) => {
284 errors = true;
285 writeln!(output, "Unexpected runtime error in check {name}: {e}")?;
286 }
287 }
288 }
289 if errors {
290 anyhow::bail!("Encountered errors")
291 }
292
293 Ok(())
306}