|
6 | 6 | // Unfortunately needed here to work with linkme
|
7 | 7 | #![allow(unsafe_code)]
|
8 | 8 |
|
| 9 | +use std::fmt::Write as _; |
9 | 10 | use std::future::Future;
|
10 | 11 | use std::pin::Pin;
|
11 | 12 | use std::process::Command;
|
12 | 13 |
|
| 14 | +use bootc_utils::iterator_split_nonempty_rest_count; |
| 15 | +use camino::Utf8PathBuf; |
13 | 16 | use cap_std::fs::{Dir, MetadataExt as _};
|
14 | 17 | use cap_std_ext::cap_std;
|
15 | 18 | use cap_std_ext::dirext::CapStdExtDirExt;
|
| 19 | +use fn_error_context::context; |
16 | 20 | use linkme::distributed_slice;
|
| 21 | +use ostree_ext::ostree_prepareroot::Tristate; |
| 22 | +use ostree_ext::{composefs, ostree}; |
| 23 | +use serde::{Deserialize, Serialize}; |
17 | 24 |
|
18 | 25 | use crate::store::Storage;
|
19 | 26 |
|
| 27 | +use std::os::fd::AsFd; |
| 28 | + |
20 | 29 | /// A lint check has failed.
|
21 | 30 | #[derive(thiserror::Error, Debug)]
|
22 | 31 | struct FsckError(String);
|
@@ -112,6 +121,153 @@ fn check_resolvconf(storage: &Storage) -> FsckResult {
|
112 | 121 | fsck_ok()
|
113 | 122 | }
|
114 | 123 |
|
| 124 | +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)] |
| 125 | +#[serde(rename_all = "kebab-case")] |
| 126 | +pub(crate) enum VerityState { |
| 127 | + Enabled, |
| 128 | + Disabled, |
| 129 | + Inconsistent((u64, u64)), |
| 130 | +} |
| 131 | + |
| 132 | +#[derive(Debug, Default)] |
| 133 | +struct ObjectsVerityState { |
| 134 | + /// Count of objects with fsverity |
| 135 | + enabled: u64, |
| 136 | + /// Count of objects without fsverity |
| 137 | + disabled: u64, |
| 138 | + /// Objects which should have fsverity but do not |
| 139 | + missing: Vec<String>, |
| 140 | +} |
| 141 | + |
| 142 | +/// Check the fsverity state of all regular files in this object directory. |
| 143 | +#[context("Computing verity state")] |
| 144 | +fn verity_state_of_objects( |
| 145 | + d: &Dir, |
| 146 | + prefix: &str, |
| 147 | + expected: bool, |
| 148 | +) -> anyhow::Result<ObjectsVerityState> { |
| 149 | + let mut enabled = 0; |
| 150 | + let mut disabled = 0; |
| 151 | + let mut missing = Vec::new(); |
| 152 | + for ent in d.entries()? { |
| 153 | + let ent = ent?; |
| 154 | + if !ent.file_type()?.is_file() { |
| 155 | + continue; |
| 156 | + } |
| 157 | + let name = ent.file_name(); |
| 158 | + let name = name |
| 159 | + .into_string() |
| 160 | + .map(Utf8PathBuf::from) |
| 161 | + .map_err(|_| anyhow::anyhow!("Invalid UTF-8"))?; |
| 162 | + let Some("file") = name.extension() else { |
| 163 | + continue; |
| 164 | + }; |
| 165 | + let f = d.open(&name)?; |
| 166 | + let r: Option<composefs::fsverity::Sha256HashValue> = |
| 167 | + composefs::fsverity::ioctl::fs_ioc_measure_verity(f.as_fd())?; |
| 168 | + drop(f); |
| 169 | + if r.is_some() { |
| 170 | + enabled += 1; |
| 171 | + } else { |
| 172 | + disabled += 1; |
| 173 | + if expected { |
| 174 | + missing.push(format!("{prefix}{name}")); |
| 175 | + } |
| 176 | + } |
| 177 | + } |
| 178 | + let r = ObjectsVerityState { |
| 179 | + enabled, |
| 180 | + disabled, |
| 181 | + missing, |
| 182 | + }; |
| 183 | + Ok(r) |
| 184 | +} |
| 185 | + |
| 186 | +async fn verity_state_of_all_objects( |
| 187 | + repo: &ostree::Repo, |
| 188 | + expected: bool, |
| 189 | +) -> anyhow::Result<ObjectsVerityState> { |
| 190 | + // Limit concurrency here |
| 191 | + const MAX_CONCURRENT: usize = 3; |
| 192 | + |
| 193 | + let repodir = Dir::reopen_dir(&repo.dfd_borrow())?; |
| 194 | + |
| 195 | + // It's convenient here to reuse tokio's spawn_blocking as a threadpool basically. |
| 196 | + let mut joinset = tokio::task::JoinSet::new(); |
| 197 | + let mut results = Vec::new(); |
| 198 | + |
| 199 | + for ent in repodir.read_dir("objects")? { |
| 200 | + // Block here if the queue is full |
| 201 | + while joinset.len() >= MAX_CONCURRENT { |
| 202 | + results.push(joinset.join_next().await.unwrap()??); |
| 203 | + } |
| 204 | + let ent = ent?; |
| 205 | + if !ent.file_type()?.is_dir() { |
| 206 | + continue; |
| 207 | + } |
| 208 | + let name = ent.file_name(); |
| 209 | + let name = name |
| 210 | + .into_string() |
| 211 | + .map(Utf8PathBuf::from) |
| 212 | + .map_err(|_| anyhow::anyhow!("Invalid UTF-8"))?; |
| 213 | + |
| 214 | + let objdir = ent.open_dir()?; |
| 215 | + let expected = expected.clone(); |
| 216 | + joinset.spawn_blocking(move || verity_state_of_objects(&objdir, name.as_str(), expected)); |
| 217 | + } |
| 218 | + |
| 219 | + // Drain the remaining tasks. |
| 220 | + while let Some(output) = joinset.join_next().await { |
| 221 | + results.push(output??); |
| 222 | + } |
| 223 | + // Fold the results. |
| 224 | + let r = results |
| 225 | + .into_iter() |
| 226 | + .fold(ObjectsVerityState::default(), |mut acc, v| { |
| 227 | + acc.enabled += v.enabled; |
| 228 | + acc.disabled += v.disabled; |
| 229 | + acc.missing.extend(v.missing); |
| 230 | + acc |
| 231 | + }); |
| 232 | + Ok(r) |
| 233 | +} |
| 234 | + |
| 235 | +#[distributed_slice(FSCK_CHECKS)] |
| 236 | +static CHECK_FSVERITY: FsckCheck = |
| 237 | + FsckCheck::new("fsverity", 10, FsckFnImpl::Async(check_fsverity)); |
| 238 | +fn check_fsverity(storage: &Storage) -> Pin<Box<dyn Future<Output = FsckResult> + '_>> { |
| 239 | + Box::pin(check_fsverity_inner(storage)) |
| 240 | +} |
| 241 | + |
| 242 | +async fn check_fsverity_inner(storage: &Storage) -> FsckResult { |
| 243 | + let repo = &storage.repo(); |
| 244 | + let verity_state = ostree_ext::fsverity::is_verity_enabled(repo)?; |
| 245 | + tracing::debug!( |
| 246 | + "verity: expected={:?} found={:?}", |
| 247 | + verity_state.desired, |
| 248 | + verity_state.enabled |
| 249 | + ); |
| 250 | + |
| 251 | + let verity_found_state = |
| 252 | + verity_state_of_all_objects(&storage.repo(), verity_state.desired == Tristate::Enabled) |
| 253 | + .await?; |
| 254 | + let Some((missing, rest)) = |
| 255 | + iterator_split_nonempty_rest_count(verity_found_state.missing.iter(), 5) |
| 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 | + |
115 | 271 | pub(crate) async fn fsck(storage: &Storage, mut output: impl std::io::Write) -> anyhow::Result<()> {
|
116 | 272 | let mut checks = FSCK_CHECKS.static_slice().iter().collect::<Vec<_>>();
|
117 | 273 | checks.sort_by(|a, b| a.ordering.cmp(&b.ordering));
|
|
0 commit comments