ostree_ext/
fsverity.rs

1//! Integration with fsverity
2
3use std::os::fd::AsFd;
4use std::os::unix::ffi::OsStrExt;
5use std::path::Path;
6use std::str::FromStr;
7
8use anyhow::{Context, Result};
9use cap_std::fs::Dir;
10use cap_std_ext::cap_std;
11use composefs::fsverity as composefs_fsverity;
12use composefs_fsverity::Sha256HashValue;
13use ostree::gio;
14
15use crate::keyfileext::KeyFileExt;
16use crate::ostree_prepareroot::Tristate;
17
18/// The relative path to the repository config file.
19const CONFIG_PATH: &str = "config";
20
21/// The ostree integrity config section
22pub const INTEGRITY_SECTION: &str = "ex-integrity";
23/// The ostree repo config option to enable fsverity
24pub const INTEGRITY_FSVERITY: &str = "fsverity";
25
26/// State of fsverity in a repo
27#[derive(Debug, Clone)]
28pub struct RepoVerityState {
29    /// True if fsverity is desired to be enabled
30    pub desired: Tristate,
31    /// True if fsverity is known to be enabled on all objects
32    pub enabled: bool,
33}
34
35/// Check if fsverity is fully enabled for the target repository.
36pub fn is_verity_enabled(repo: &ostree::Repo) -> Result<RepoVerityState> {
37    let desired = repo
38        .config()
39        .optional_string(INTEGRITY_SECTION, INTEGRITY_FSVERITY)?
40        .map(|s| Tristate::from_str(s.as_str()))
41        .transpose()?
42        .unwrap_or_default();
43    let repo_dir = &Dir::reopen_dir(&repo.dfd_borrow())?;
44    let config = repo_dir
45        .open(CONFIG_PATH)
46        .with_context(|| format!("Opening repository {CONFIG_PATH}"))?;
47    // We use the flag of having fsverity set on the repository config as a flag to say that
48    // fsverity is fully enabled; all objects have it.
49    let enabled = composefs_fsverity::measure_verity::<Sha256HashValue>(config.as_fd()).is_ok();
50    Ok(RepoVerityState { desired, enabled })
51}
52
53/// Enable fsverity on regular file objects in this directory.
54fn enable_fsverity_in_objdir(d: &Dir) -> anyhow::Result<()> {
55    for ent in d.entries()? {
56        let ent = ent?;
57        if !ent.file_type()?.is_file() {
58            continue;
59        }
60        let name = ent.file_name();
61        let Some(b"file") = Path::new(&name).extension().map(|e| e.as_bytes()) else {
62            continue;
63        };
64        let f = d.open(&name)?;
65        let enabled =
66            composefs::fsverity::measure_verity_opt::<Sha256HashValue>(f.as_fd())?.is_some();
67        if !enabled {
68            // NOTE: We're not using the _with_copy API here because for us it'd require
69            // copying all the metadata too which is mildly tedious.
70            // For main composefs we don't need to care about the per-file metadata
71            // in general which simplifies a lot.
72            composefs_fsverity::enable_verity_with_retry::<Sha256HashValue>(f.as_fd())?;
73        }
74    }
75    Ok(())
76}
77
78/// Ensure that fsverity is enabled on this repository.
79///
80/// - Walk over all regular file objects and ensure that fsverity is enabled on them
81/// - Update the repo config if necessary to ensure that future objects have it by default
82/// - Update the repo config to enable fsverity on the file itself as a completion flag
83pub async fn ensure_verity(repo: &ostree::Repo) -> Result<()> {
84    let state = is_verity_enabled(repo)?;
85    // If we're already enabled, then we're done.
86    if state.enabled {
87        return Ok(());
88    }
89
90    // Limit concurrency here
91    const MAX_CONCURRENT: usize = 3;
92
93    let repodir = Dir::reopen_dir(&repo.dfd_borrow())?;
94
95    // It's convenient here to reuse tokio's spawn_blocking as a threadpool basically.
96    let mut joinset = tokio::task::JoinSet::new();
97
98    // Walk over all objects
99    for ent in repodir.read_dir("objects")? {
100        // Block here if the queue is full
101        while joinset.len() >= MAX_CONCURRENT {
102            // SAFETY: We just checked the length so we know there's something pending
103            let _: () = joinset.join_next().await.unwrap()??;
104        }
105        let ent = ent?;
106        if !ent.file_type()?.is_dir() {
107            continue;
108        }
109        let objdir = ent.open_dir()?;
110        // Spawn a thread for each object directory just on general principle
111        // of doing multi-threading.
112        joinset.spawn_blocking(move || enable_fsverity_in_objdir(&objdir));
113    }
114
115    // Drain the remaining tasks.
116    while let Some(output) = joinset.join_next().await {
117        let _: () = output??;
118    }
119
120    // Ensure the flag is set in the config file, which is what libostree parses.
121    if state.desired != Tristate::Enabled {
122        let config = repo.copy_config();
123        config.set_boolean(INTEGRITY_SECTION, INTEGRITY_FSVERITY, true);
124        repo.write_config(&config)?;
125        repo.reload_config(gio::Cancellable::NONE)?;
126    }
127    // And finally, enable fsverity as a flag that we have successfully
128    // enabled fsverity on all objects.
129    let f = repodir.open(CONFIG_PATH)?;
130    match composefs_fsverity::enable_verity_raw::<Sha256HashValue>(f.as_fd()) {
131        Ok(()) => Ok(()),
132        Err(composefs_fsverity::EnableVerityError::AlreadyEnabled) => Ok(()),
133        Err(e) => Err(e.into()),
134    }
135}