ostree_ext/
ostree_prepareroot.rs

1//! Logic related to parsing ostree-prepare-root.conf.
2//!
3
4// SPDX-License-Identifier: Apache-2.0 OR MIT
5
6use std::io::Read;
7use std::str::FromStr;
8
9use anyhow::{Context, Result};
10use camino::Utf8Path;
11use cap_std_ext::dirext::CapStdExtDirExt;
12use fn_error_context::context;
13use ocidir::cap_std::fs::Dir;
14use ostree::glib::object::Cast;
15use ostree::prelude::FileExt;
16use ostree::{gio, glib};
17
18use crate::keyfileext::KeyFileExt;
19use crate::ostree_manual;
20use bootc_utils::ResultExt;
21
22/// The relative path to ostree-prepare-root's config.
23pub const CONF_PATH: &str = "ostree/prepare-root.conf";
24
25/// Load the ostree prepare-root config from the given ostree repository.
26pub fn load_config(root: &ostree::RepoFile) -> Result<Option<glib::KeyFile>> {
27    let cancellable = gio::Cancellable::NONE;
28    let kf = glib::KeyFile::new();
29    for path in ["etc", "usr/lib"].into_iter().map(Utf8Path::new) {
30        let path = &path.join(CONF_PATH);
31        let f = root.resolve_relative_path(path);
32        if !f.query_exists(cancellable) {
33            continue;
34        }
35        let f = f.downcast_ref::<ostree::RepoFile>().unwrap();
36        let contents = ostree_manual::repo_file_read_to_string(f)?;
37        kf.load_from_data(&contents, glib::KeyFileFlags::NONE)
38            .with_context(|| format!("Parsing {path}"))?;
39        tracing::debug!("Loaded {path}");
40        return Ok(Some(kf));
41    }
42    tracing::debug!("No {CONF_PATH} found");
43    Ok(None)
44}
45
46/// Load the configuration from the target root.
47pub fn load_config_from_root(root: &Dir) -> Result<Option<glib::KeyFile>> {
48    for path in ["etc", "usr/lib"].into_iter().map(Utf8Path::new) {
49        let path = path.join(CONF_PATH);
50        let Some(mut f) = root.open_optional(&path)? else {
51            continue;
52        };
53        let mut contents = String::new();
54        f.read_to_string(&mut contents)?;
55        let kf = glib::KeyFile::new();
56        kf.load_from_data(&contents, glib::KeyFileFlags::NONE)
57            .with_context(|| format!("Parsing {path}"))?;
58        return Ok(Some(kf));
59    }
60    Ok(None)
61}
62
63/// Require the configuration in the target root.
64pub fn require_config_from_root(root: &Dir) -> Result<glib::KeyFile> {
65    load_config_from_root(root)?
66        .ok_or_else(|| anyhow::anyhow!("Failed to find {CONF_PATH} in /usr/lib or /etc"))
67}
68
69/// Query whether the target root has the `root.transient` key
70/// which sets up a transient overlayfs.
71pub fn overlayfs_root_enabled(root: &ostree::RepoFile) -> Result<bool> {
72    if let Some(config) = load_config(root)? {
73        overlayfs_enabled_in_config(&config)
74    } else {
75        Ok(false)
76    }
77}
78
79/// An option which can be enabled, disabled, or possibly enabled.
80#[derive(Debug, PartialEq, Eq, Clone)]
81pub enum Tristate {
82    /// Enabled
83    Enabled,
84    /// Disabled
85    Disabled,
86    /// Maybe
87    Maybe,
88}
89
90impl FromStr for Tristate {
91    type Err = anyhow::Error;
92
93    fn from_str(s: &str) -> Result<Self> {
94        let r = match s {
95            // Keep this in sync with ot_keyfile_get_tristate_with_default from ostree
96            "yes" | "true" | "1" => Tristate::Enabled,
97            "no" | "false" | "0" => Tristate::Disabled,
98            "maybe" => Tristate::Maybe,
99            o => anyhow::bail!("Invalid tristate value: {o}"),
100        };
101        Ok(r)
102    }
103}
104
105impl Default for Tristate {
106    fn default() -> Self {
107        Self::Disabled
108    }
109}
110
111impl Tristate {
112    pub(crate) fn maybe_enabled(&self) -> bool {
113        match self {
114            Self::Enabled | Self::Maybe => true,
115            Self::Disabled => false,
116        }
117    }
118}
119
120/// The state of a composefs for ostree
121#[derive(Debug, PartialEq, Eq)]
122pub enum ComposefsState {
123    /// The composefs must be signed and use fsverity
124    Signed,
125    /// The composefs must use fsverity
126    Verity,
127    /// The composefs may or may not be enabled.
128    Tristate(Tristate),
129}
130
131impl Default for ComposefsState {
132    fn default() -> Self {
133        Self::Tristate(Tristate::default())
134    }
135}
136
137impl FromStr for ComposefsState {
138    type Err = anyhow::Error;
139
140    #[context("Parsing composefs.enabled value {s}")]
141    fn from_str(s: &str) -> Result<Self> {
142        let r = match s {
143            "signed" => Self::Signed,
144            "verity" => Self::Verity,
145            o => Self::Tristate(Tristate::from_str(o)?),
146        };
147        Ok(r)
148    }
149}
150
151impl ComposefsState {
152    pub(crate) fn maybe_enabled(&self) -> bool {
153        match self {
154            ComposefsState::Signed | ComposefsState::Verity => true,
155            ComposefsState::Tristate(t) => t.maybe_enabled(),
156        }
157    }
158
159    /// This configuration requires fsverity on the target filesystem.
160    pub fn requires_fsverity(&self) -> bool {
161        matches!(self, ComposefsState::Signed | ComposefsState::Verity)
162    }
163}
164
165/// Query whether the config uses an overlayfs model (composefs or plain overlayfs).
166pub fn overlayfs_enabled_in_config(config: &glib::KeyFile) -> Result<bool> {
167    let root_transient = config
168        .optional_bool("root", "transient")?
169        .unwrap_or_default();
170    let composefs = config
171        .optional_string("composefs", "enabled")?
172        .map(|s| ComposefsState::from_str(s.as_str()))
173        .transpose()
174        .log_err_default()
175        .unwrap_or_default();
176    Ok(root_transient || composefs.maybe_enabled())
177}
178
179#[cfg(test)]
180mod tests {
181    use super::*;
182
183    #[test]
184    fn test_tristate() {
185        for v in ["yes", "true", "1"] {
186            assert_eq!(Tristate::from_str(v).unwrap(), Tristate::Enabled);
187        }
188        assert_eq!(Tristate::from_str("maybe").unwrap(), Tristate::Maybe);
189        for v in ["no", "false", "0"] {
190            assert_eq!(Tristate::from_str(v).unwrap(), Tristate::Disabled);
191        }
192        for v in ["", "junk", "fal", "tr1"] {
193            assert!(Tristate::from_str(v).is_err());
194        }
195    }
196
197    #[test]
198    fn test_composefs_state() {
199        assert_eq!(
200            ComposefsState::from_str("signed").unwrap(),
201            ComposefsState::Signed
202        );
203        for v in ["yes", "true", "1"] {
204            assert_eq!(
205                ComposefsState::from_str(v).unwrap(),
206                ComposefsState::Tristate(Tristate::Enabled)
207            );
208        }
209        assert_eq!(Tristate::from_str("maybe").unwrap(), Tristate::Maybe);
210        for v in ["no", "false", "0"] {
211            assert_eq!(
212                ComposefsState::from_str(v).unwrap(),
213                ComposefsState::Tristate(Tristate::Disabled)
214            );
215        }
216    }
217
218    #[test]
219    fn test_overlayfs_enabled() {
220        let d0 = indoc::indoc! { r#"
221[foo]
222bar = baz
223[root]
224"# };
225        let d1 = indoc::indoc! { r#"
226[root]
227transient = false
228    "# };
229        let d2 = indoc::indoc! { r#"
230[composefs]
231enabled = false
232    "# };
233        for v in ["", d0, d1, d2] {
234            let kf = glib::KeyFile::new();
235            kf.load_from_data(v, glib::KeyFileFlags::empty()).unwrap();
236            assert!(!overlayfs_enabled_in_config(&kf).unwrap());
237        }
238
239        let e0 = format!("{d0}\n[root]\ntransient = true");
240        let e1 = format!("{d1}\n[composefs]\nenabled = true\n[other]\nsomekey = someval");
241        let e2 = format!("{d1}\n[composefs]\nenabled = yes");
242        let e3 = format!("{d1}\n[composefs]\nenabled = signed");
243        for v in [e0, e1, e2, e3] {
244            let kf = glib::KeyFile::new();
245            kf.load_from_data(&v, glib::KeyFileFlags::empty()).unwrap();
246            assert!(overlayfs_enabled_in_config(&kf).unwrap());
247        }
248    }
249}