OXYGENHUB
◆ v0.4.6 oxygen / crates / oxy-core / src / repo / fsck.rs
19.5 KiB · Rust · last changed Grants let certified keys act for an author (keyless step 5a) 18 days ago source blame history raw
1
//! Repository integrity check (spec §21.1): `oxy fsck`.
2
//!
3
//! Reads everything and changes nothing. Four passes:
4
//!
5
//! 1. every stored object: header, length, hash, and that its body decodes
6
//!    as its type says;
7
//! 2. the operation log: every operation reachable from the head is
8
//!    present and decodes, and clocks never run backwards;
9
//! 3. references: the current view's bookmarks, tags, remote bookmarks,
10
//!    workspaces and policy, then the dependency closure of everything
11
//!    they name, with each change's author identity, blobs and chunks,
12
//!    and each change's signatures or seals;
13
//! 4. derived data: every bookmark's state validates, every checkpoint
14
//!    restores exactly the state it names, and reference and tag
15
//!    attestations carry good signatures.
16
//!
17
//! Missing blobs are warnings, not errors, in a partial clone
18
//! (`filter.max-blob` or a sparse filter), and changes beyond a shallow
19
//! boundary are expected to be absent.
20
21
use super::*;
22
use std::collections::{HashSet, VecDeque};
23
24
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
25
pub enum Severity {
26
    /// Worth knowing; the repository is still sound.
27
    Warning,
28
    /// Data is missing, corrupt or fails verification.
29
    Error,
30
}
31
32
#[derive(Clone, Debug, PartialEq, Eq)]
33
pub struct Problem {
34
    pub severity: Severity,
35
    /// The object the problem is about, when there is one.
36
    pub object: Option<ObjectRef>,
37
    pub what: String,
38
}
39
40
#[derive(Clone, Debug, Default, PartialEq, Eq)]
41
pub struct FsckReport {
42
    pub objects: usize,
43
    pub ops: usize,
44
    /// Changes checked, working-copy changes included.
45
    pub changes: usize,
46
    /// Committed changes authenticated by their own signature or by a seal.
47
    pub signed: usize,
48
    pub unsigned: usize,
49
    pub checkpoints: usize,
50
    pub attestations: usize,
51
    pub problems: Vec<Problem>,
52
}
53
54
impl FsckReport {
55
    pub fn errors(&self) -> usize {
56
        self.problems
57
            .iter()
58
            .filter(|p| p.severity == Severity::Error)
59
            .count()
60
    }
61
62
    pub fn warnings(&self) -> usize {
63
        self.problems
64
            .iter()
65
            .filter(|p| p.severity == Severity::Warning)
66
            .count()
67
    }
68
69
    fn error(&mut self, object: Option<ObjectRef>, what: impl Into<String>) {
70
        self.problems.push(Problem {
71
            severity: Severity::Error,
72
            object,
73
            what: what.into(),
74
        });
75
    }
76
77
    fn warn(&mut self, object: Option<ObjectRef>, what: impl Into<String>) {
78
        self.problems.push(Problem {
79
            severity: Severity::Warning,
80
            object,
81
            what: what.into(),
82
        });
83
    }
84
}
85
86
/// Whether `body` decodes as an object of type `ty`.
87
fn decodes(ty: ObjectType, body: &[u8]) -> std::result::Result<(), String> {
88
    let r = match ty {
89
        ObjectType::Chunk => Ok(()),
90
        ObjectType::Blob => Blob::decode(body).map(|_| ()),
91
        // Structure only: the text blobs are checked with the change graph.
92
        ObjectType::Change => {
93
            Change::decode_with(body, &mut |_, _, _| Ok(crate::lines::Lines::default())).map(|_| ())
94
        }
95
        ObjectType::Checkpoint => crate::objects::Checkpoint::decode(body).map(|_| ()),
96
        ObjectType::Op => match Op::decode(body) {
97
            Ok(_) => Ok(()),
98
            Err(e) => Op::decode_format1(body).map(|_| ()).map_err(|_| e),
99
        },
100
        ObjectType::Attest => Attest::decode(body).map(|_| ()),
101
        ObjectType::Identity => Identity::decode(body).map(|_| ()),
102
        ObjectType::Policy => crate::policy::Policy::decode(body).map(|_| ()),
103
        ObjectType::Receipt => crate::receipt::Receipt::decode(body).map(|_| ()),
104
        ObjectType::Certificate => crate::cert::Certificate::decode(body).map(|_| ()),
105
        ObjectType::Grant => crate::cert::Grant::decode(body).map(|_| ()),
106
        ObjectType::State => crate::objects::StateObject::decode(body).map(|_| ()),
107
    };
108
    r.map_err(|e| e.to_string())
109
}
110
111
impl Repo {
112
    /// Check the whole repository; see the module documentation.
113
    pub fn fsck(&self) -> Result<FsckReport> {
114
        let mut rep = FsckReport::default();
115
        let bad = {
116
            let _t = crate::timing::phase("fsck: objects");
117
            self.fsck_objects(&mut rep)?
118
        };
119
        {
120
            let _t = crate::timing::phase("fsck: operations");
121
            self.fsck_ops(&mut rep, &bad);
122
        }
123
        {
124
            let _t = crate::timing::phase("fsck: references");
125
            self.fsck_references(&mut rep, &bad)?;
126
        }
127
        {
128
            let _t = crate::timing::phase("fsck: states and checkpoints");
129
            self.fsck_derived(&mut rep)?;
130
        }
131
        rep.problems.sort_by_key(|p| std::cmp::Reverse(p.severity));
132
        Ok(rep)
133
    }
134
135
    /// Pass 1. Returns the objects that failed, so later passes report
136
    /// them once rather than at every reference.
137
    fn fsck_objects(&self, rep: &mut FsckReport) -> Result<HashSet<ObjectRef>> {
138
        use rayon::prelude::*;
139
        let mut bad = HashSet::new();
140
        // A chunk at a time, read (and verified) and decoded on every core.
141
        for refs in self.store.list()?.chunks(8192) {
142
            let checked: Vec<std::result::Result<(), String>> = self
143
                .store
144
                .get_many(refs)
145
                .into_par_iter()
146
                .map(|got| match got {
147
                    Ok((ty, body)) => decodes(ty, &body)
148
                        .map_err(|e| format!("{} object does not decode: {e}", ty.name())),
149
                    Err(e) => Err(format!("unreadable object: {e}")),
150
                })
151
                .collect();
152
            for (r, c) in refs.iter().zip(checked) {
153
                rep.objects += 1;
154
                if let Err(e) = c {
155
                    rep.error(Some(*r), e);
156
                    bad.insert(*r);
157
                }
158
            }
159
        }
160
        Ok(bad)
161
    }
162
163
    /// Pass 2.
164
    fn fsck_ops(&self, rep: &mut FsckReport, bad: &HashSet<ObjectRef>) {
165
        let head = match self.read_head() {
166
            Ok((h, _)) => h,
167
            Err(RepoError::Msg(m)) if m.starts_with("corrupt") => {
168
                rep.error(None, ".oxy/ops/head does not hold an object reference");
169
                return;
170
            }
171
            Err(e) => {
172
                rep.error(None, format!("cannot read .oxy/ops/head: {e}"));
173
                return;
174
            }
175
        };
176
        let mut seen = HashSet::new();
177
        let mut queue: VecDeque<(ObjectRef, Option<(ObjectRef, u64)>)> =
178
            VecDeque::from([(head, None)]);
179
        while let Some((r, child)) = queue.pop_front() {
180
            if !seen.insert(r) {
181
                continue;
182
            }
183
            if bad.contains(&r) {
184
                continue;
185
            }
186
            let body = match self.store.get_typed(&r, ObjectType::Op) {
187
                Ok(b) => b,
188
                Err(StoreError::NotFound(_)) => {
189
                    let by = child
190
                        .map(|(c, _)| format!(", the parent of operation {}", c.short(12)))
191
                        .unwrap_or_default();
192
                    rep.error(
193
                        Some(r),
194
                        format!("operation missing{by}; the log is cut here"),
195
                    );
196
                    continue;
197
                }
198
                Err(e) => {
199
                    rep.error(Some(r), format!("operation unreadable: {e}"));
200
                    continue;
201
                }
202
            };
203
            rep.ops += 1;
204
            let (parents, wall_ms) = match Op::decode(&body) {
205
                Ok(op) => (op.parents, op.hlc.wall_ms),
206
                Err(_) => match Op::decode_format1(&body) {
207
                    Ok(op) => (op.parents, op.hlc.wall_ms),
208
                    Err(e) => {
209
                        rep.error(Some(r), format!("operation does not decode: {e}"));
210
                        continue;
211
                    }
212
                },
213
            };
214
            if let Some((c, child_ms)) = child {
215
                if wall_ms > child_ms {
216
                    rep.warn(
217
                        Some(c),
218
                        format!(
219
                            "operation is timed before its parent {} (a clock was wrong)",
220
                            r.short(12)
221
                        ),
222
                    );
223
                }
224
            }
225
            queue.extend(parents.into_iter().map(|p| (p, Some((r, wall_ms)))));
226
        }
227
    }
228
229
    /// Pass 3.
230
    fn fsck_references(&self, rep: &mut FsckReport, bad: &HashSet<ObjectRef>) -> Result<()> {
231
        let view = match self.view() {
232
            Ok(v) => v,
233
            Err(e) => {
234
                rep.error(None, format!("cannot read the current view: {e}"));
235
                return Ok(());
236
            }
237
        };
238
        let boundary = self.boundary();
239
        let partial = self.config.max_blob.is_some() || !self.config.sparse_paths.is_empty();
240
241
        let mut roots: Vec<(ChangeHash, String)> = Vec::new();
242
        let mut states: Vec<(ObjectRef, String)> = Vec::new();
243
        states.extend(
244
            view.bookmarks
245
                .iter()
246
                .map(|(n, s)| (*s, format!("bookmark {n}"))),
247
        );
248
        states.extend(view.tags.iter().map(|(n, s)| (*s, format!("tag {n}"))));
249
        for (remote, bms) in &view.remotes {
250
            states.extend(bms.iter().map(|(n, s)| (*s, format!("{remote}/{n}"))));
251
        }
252
        for (st, what) in states {
253
            match self.store.get_state(&st) {
254
                Ok(s) => roots.extend(s.heads.into_iter().map(|h| (h, what.clone()))),
255
                Err(e) if !bad.contains(&st) => {
256
                    rep.error(Some(st), format!("state of {what} unusable: {e}"))
257
                }
258
                Err(_) => {}
259
            }
260
        }
261
        for (ws, h) in &view.workspaces {
262
            roots.push((*h, format!("workspace {}", ws.to_hex())));
263
        }
264
        if let Some(p) = view.policy {
265
            match self.store.get_typed(&p, ObjectType::Policy) {
266
                Ok(bytes) => {
267
                    if let Ok(policy) = crate::policy::Policy::decode(&bytes) {
268
                        for id in &policy.identities {
269
                            if !self.store.has(&id.hash()) {
270
                                rep.warn(
271
                                    Some(id.hash()),
272
                                    "an identity the policy names is not stored",
273
                                );
274
                            }
275
                        }
276
                    }
277
                }
278
                Err(e) if !bad.contains(&p) => {
279
                    rep.error(Some(p), format!("the policy is unusable: {e}"))
280
                }
281
                Err(_) => {}
282
            }
283
        }
284
285
        // Working-copy changes are never signed; they are checked but not tallied.
286
        let working: HashSet<ChangeHash> = view.workspaces.values().copied().collect();
287
        let mut seen: HashSet<ChangeHash> = HashSet::new();
288
        let mut blobs_seen: HashSet<ObjectRef> = HashSet::new();
289
        let mut stack = roots;
290
        while let Some((h, needed_by)) = stack.pop() {
291
            if !seen.insert(h) {
292
                continue;
293
            }
294
            // The skeleton: the text is checked as the blobs it is stored
295
            // in, not decoded (the objects pass checked every object).
296
            let (c, texts) = match self.store.get_change_lazy(&h) {
297
                Ok(c) => c,
298
                Err(StoreError::NotFound(_)) if boundary.contains(&h) => continue,
299
                Err(StoreError::NotFound(_)) => {
300
                    rep.error(Some(h), format!("change missing, needed by {needed_by}"));
301
                    continue;
302
                }
303
                Err(e) => {
304
                    if !bad.contains(&h) {
305
                        rep.error(Some(h), format!("change unusable: {e}"));
306
                    }
307
                    continue;
308
                }
309
            };
310
            rep.changes += 1;
311
            let label = format!("change {}", h.short(12));
312
            stack.extend(c.deps.iter().map(|d| (*d, label.clone())));
313
            if !c.author.is_zero() && !self.store.has(&c.author) {
314
                rep.warn(
315
                    Some(h),
316
                    format!("author identity {} is not stored", c.author.short(12)),
317
                );
318
            }
319
            for t in texts {
320
                if let crate::objects::TextRef::Blob(b, _, _) = t {
321
                    if blobs_seen.insert(b) {
322
                        // A change cannot be read without its text: never partial.
323
                        self.fsck_blob(rep, b, &label, false, bad);
324
                    }
325
                }
326
            }
327
            for e in &c.edits {
328
                let Edit::Replace { old, new, .. } = e else {
329
                    continue;
330
                };
331
                for b in [old, new].into_iter().flatten() {
332
                    if !blobs_seen.insert(*b) {
333
                        continue;
334
                    }
335
                    self.fsck_blob(rep, *b, &label, partial, bad);
336
                }
337
            }
338
            if working.contains(&h) {
339
                continue;
340
            }
341
            match self.authenticate_change(&h) {
342
                Ok(SignatureStatus::Valid { .. }) => rep.signed += 1,
343
                Ok(SignatureStatus::Unsigned) => rep.unsigned += 1,
344
                Ok(SignatureStatus::Invalid(why)) => {
345
                    rep.error(Some(h), format!("bad signature: {why}"))
346
                }
347
                Err(e) => rep.error(Some(h), format!("cannot check signatures: {e}")),
348
            }
349
        }
350
        Ok(())
351
    }
352
353
    fn fsck_blob(
354
        &self,
355
        rep: &mut FsckReport,
356
        b: ObjectRef,
357
        used_by: &str,
358
        partial: bool,
359
        bad: &HashSet<ObjectRef>,
360
    ) {
361
        match self.store.get_typed(&b, ObjectType::Blob) {
362
            Ok(bytes) => {
363
                let Ok(blob) = Blob::decode(&bytes) else {
364
                    return;
365
                };
366
                for ch in blob.chunks {
367
                    if !self.store.has(&ch) {
368
                        rep.error(
369
                            Some(ch),
370
                            format!("chunk missing from blob {} ({used_by})", b.short(12)),
371
                        );
372
                    }
373
                }
374
            }
375
            Err(StoreError::NotFound(_)) if partial => rep.warn(
376
                Some(b),
377
                format!("blob not fetched ({used_by}); expected in a partial clone"),
378
            ),
379
            Err(StoreError::NotFound(_)) => rep.error(Some(b), format!("blob missing ({used_by})")),
380
            Err(e) if !bad.contains(&b) => {
381
                rep.error(Some(b), format!("blob unusable ({used_by}): {e}"))
382
            }
383
            Err(_) => {}
384
        }
385
    }
386
387
    /// Whether `heads` form a valid state: its history loads and every file
388
    /// graph builds. Graphs are built a batch at a time and dropped, so the
389
    /// check needs the memory of a batch, not of the whole tree.
390
    fn check_state(&self, heads: &[ChangeHash]) -> Result<()> {
391
        let mut st = self.load_state_history(heads)?;
392
        let nodes = st.unbuilt_nodes();
393
        for batch in nodes.chunks(2048) {
394
            self.load_graphs_of(&mut st, batch)?;
395
            st.drop_graphs(batch);
396
        }
397
        Ok(())
398
    }
399
400
    /// Pass 4.
401
    fn fsck_derived(&self, rep: &mut FsckReport) -> Result<()> {
402
        let Ok(view) = self.view() else { return Ok(()) };
403
        let shallow = self.is_shallow();
404
        if !shallow {
405
            let mut checked: HashSet<ObjectRef> = HashSet::new();
406
            for (name, st) in &view.bookmarks {
407
                if !checked.insert(*st) {
408
                    continue;
409
                }
410
                let Ok(s) = self.store.get_state(st) else {
411
                    continue;
412
                };
413
                if let Err(e) = self.check_state(&s.heads) {
414
                    rep.error(
415
                        Some(*st),
416
                        format!("bookmark {name} does not form a valid state: {e}"),
417
                    );
418
                }
419
            }
420
        }
421
        for r in self.checkpoint_refs()? {
422
            rep.checkpoints += 1;
423
            let cp = match self.store.get_typed(&r, ObjectType::Checkpoint) {
424
                Ok(b) => match crate::objects::Checkpoint::decode(&b) {
425
                    Ok(cp) => cp,
426
                    Err(_) => continue,
427
                },
428
                Err(StoreError::NotFound(_)) => {
429
                    rep.warn(
430
                        Some(r),
431
                        "checkpoint pointer names a missing checkpoint; it will be rewritten",
432
                    );
433
                    continue;
434
                }
435
                Err(_) => continue,
436
            };
437
            let payload = match self.read_blob(&cp.payload) {
438
                Ok(p) => p,
439
                Err(e) => {
440
                    rep.error(Some(r), format!("checkpoint payload unreadable: {e}"));
441
                    continue;
442
                }
443
            };
444
            if let Err(e) = self.state_from_checkpoint(&payload) {
445
                rep.error(Some(r), format!("checkpoint payload does not decode: {e}"));
446
                continue;
447
            }
448
            // The payload must be what the named changes produce, in its own version.
449
            let version = crate::cbor::decode(&payload).ok().and_then(|v| {
450
                crate::objects::map(&v, "payload").ok().and_then(|m| {
451
                    m.get(&0)
452
                        .and_then(|x| crate::objects::uint(x, "version").ok())
453
                })
454
            });
455
            let expected = |full: &State| {
456
                if version == Some(crate::state::CHECKPOINT_INDEX_VERSION) {
457
                    full.checkpoint_index(&full.checkpoint_file_graphs(), Repo::blob_ref)
458
                } else {
459
                    full.checkpoint_payload()
460
                }
461
            };
462
            if !shallow {
463
                match self.load_state_full(&cp.heads) {
464
                    Ok(full) if expected(&full) != payload => {
465
                        rep.error(Some(r), "checkpoint disagrees with the changes it names")
466
                    }
467
                    Ok(_) => {}
468
                    Err(e) => rep.error(
469
                        Some(r),
470
                        format!("checkpoint names changes that do not form a state: {e}"),
471
                    ),
472
                }
473
            }
474
        }
475
        let mut attests = Vec::new();
476
        for name in view.bookmarks.keys() {
477
            if let Ok(Some((r, _))) = self.latest_ref_attest(name) {
478
                attests.push((r, format!("bookmark {name}")));
479
            }
480
        }
481
        for name in view.tags.keys() {
482
            if let Ok(Some((r, _))) = self.tag_attest(name) {
483
                attests.push((r, format!("tag {name}")));
484
            }
485
        }
486
        for (r, what) in attests {
487
            rep.attestations += 1;
488
            let sigs = self.store.signatures(&r)?;
489
            if sigs.is_empty() {
490
                rep.warn(Some(r), format!("attestation for {what} is unsigned"));
491
            }
492
            for sig in sigs {
493
                match self.store.get_identity(&sig.key) {
494
                    Ok(id) => {
495
                        if let Err(e) = verify_signature(&id, &sig, Domain::Attest, &r) {
496
                            rep.error(
497
                                Some(r),
498
                                format!("attestation for {what} has a bad signature: {e}"),
499
                            );
500
                        }
501
                    }
502
                    Err(_) => rep.warn(
503
                        Some(r),
504
                        format!(
505
                            "attestation for {what} is signed by {}, whose identity is not stored",
506
                            sig.key.short(12)
507
                        ),
508
                    ),
509
                }
510
            }
511
        }
512
        Ok(())
513
    }
514
}