1use std::io::Read;
7use std::os::fd::{AsFd, OwnedFd};
8use std::str::FromStr;
9
10use anyhow::{Context, Result};
11use camino::Utf8Path;
12use cap_std_ext::dirext::CapStdExtDirExt;
13use fn_error_context::context;
14use ocidir::cap_std::fs::Dir;
15use ostree::glib::object::Cast;
16use ostree::prelude::FileExt;
17use ostree::{gio, glib};
18
19use crate::composefs::mount::{MountOptions, VerityRequirement, composefs_fsmount};
20use crate::keyfileext::KeyFileExt;
21use crate::ostree_manual;
22use bootc_utils::ResultExt;
23
24pub const CONF_PATH: &str = "ostree/prepare-root.conf";
26
27pub const COMPOSEFS_IMAGE: &str = ".ostree.cfs";
30
31pub fn load_config(root: &ostree::RepoFile) -> Result<Option<glib::KeyFile>> {
33 let cancellable = gio::Cancellable::NONE;
34 let kf = glib::KeyFile::new();
35 for path in ["etc", "usr/lib"].into_iter().map(Utf8Path::new) {
36 let path = &path.join(CONF_PATH);
37 let f = root.resolve_relative_path(path);
38 if !f.query_exists(cancellable) {
39 continue;
40 }
41 let f = f.downcast_ref::<ostree::RepoFile>().unwrap();
42 let contents = ostree_manual::repo_file_read_to_string(f)?;
43 kf.load_from_data(&contents, glib::KeyFileFlags::NONE)
44 .with_context(|| format!("Parsing {path}"))?;
45 tracing::debug!("Loaded {path}");
46 return Ok(Some(kf));
47 }
48 tracing::debug!("No {CONF_PATH} found");
49 Ok(None)
50}
51
52pub fn load_config_from_root(root: &Dir) -> Result<Option<glib::KeyFile>> {
54 for path in ["etc", "usr/lib"].into_iter().map(Utf8Path::new) {
55 let path = path.join(CONF_PATH);
56 let Some(mut f) = root.open_optional(&path)? else {
57 continue;
58 };
59 let mut contents = String::new();
60 f.read_to_string(&mut contents)?;
61 let kf = glib::KeyFile::new();
62 kf.load_from_data(&contents, glib::KeyFileFlags::NONE)
63 .with_context(|| format!("Parsing {path}"))?;
64 return Ok(Some(kf));
65 }
66 Ok(None)
67}
68
69pub fn require_config_from_root(root: &Dir) -> Result<glib::KeyFile> {
71 load_config_from_root(root)?
72 .ok_or_else(|| anyhow::anyhow!("Failed to find {CONF_PATH} in /usr/lib or /etc"))
73}
74
75pub fn overlayfs_root_enabled(root: &ostree::RepoFile) -> Result<bool> {
78 if let Some(config) = load_config(root)? {
79 overlayfs_enabled_in_config(&config)
80 } else {
81 Ok(false)
82 }
83}
84
85#[derive(Debug, PartialEq, Eq, Clone)]
87pub enum Tristate {
88 Enabled,
90 Disabled,
92 Maybe,
94}
95
96impl FromStr for Tristate {
97 type Err = anyhow::Error;
98
99 fn from_str(s: &str) -> Result<Self> {
100 let r = match s {
101 "yes" | "true" | "1" => Tristate::Enabled,
103 "no" | "false" | "0" => Tristate::Disabled,
104 "maybe" => Tristate::Maybe,
105 o => anyhow::bail!("Invalid tristate value: {o}"),
106 };
107 Ok(r)
108 }
109}
110
111impl Default for Tristate {
112 fn default() -> Self {
113 Self::Disabled
114 }
115}
116
117impl Tristate {
118 pub(crate) fn maybe_enabled(&self) -> bool {
119 match self {
120 Self::Enabled | Self::Maybe => true,
121 Self::Disabled => false,
122 }
123 }
124}
125
126#[derive(Debug, PartialEq, Eq)]
128pub enum ComposefsState {
129 Signed,
131 Verity,
133 Tristate(Tristate),
135}
136
137impl Default for ComposefsState {
138 fn default() -> Self {
139 Self::Tristate(Tristate::default())
140 }
141}
142
143impl FromStr for ComposefsState {
144 type Err = anyhow::Error;
145
146 #[context("Parsing composefs.enabled value {s}")]
147 fn from_str(s: &str) -> Result<Self> {
148 let r = match s {
149 "signed" => Self::Signed,
150 "verity" => Self::Verity,
151 o => Self::Tristate(Tristate::from_str(o)?),
152 };
153 Ok(r)
154 }
155}
156
157impl ComposefsState {
158 pub(crate) fn maybe_enabled(&self) -> bool {
159 match self {
160 ComposefsState::Signed | ComposefsState::Verity => true,
161 ComposefsState::Tristate(t) => t.maybe_enabled(),
162 }
163 }
164
165 pub fn requires_fsverity(&self) -> bool {
167 matches!(self, ComposefsState::Signed | ComposefsState::Verity)
168 }
169}
170
171pub fn overlayfs_enabled_in_config(config: &glib::KeyFile) -> Result<bool> {
173 let root_transient = config
174 .optional_bool("root", "transient")?
175 .unwrap_or_default();
176 let composefs = composefs_state_in_config(config)
177 .log_err_default()
178 .unwrap_or_default();
179 Ok(root_transient || composefs.maybe_enabled())
180}
181
182pub fn composefs_state_in_config(config: &glib::KeyFile) -> Result<Option<ComposefsState>> {
184 config
185 .optional_string("composefs", "enabled")?
186 .map(|s| ComposefsState::from_str(s.as_str()))
187 .transpose()
188}
189
190pub fn mount_composefs(
204 deployment: &Dir,
205 repo: &Dir,
206 name: &str,
207 config: Option<&glib::KeyFile>,
208) -> Result<Option<OwnedFd>> {
209 let state = config.map(composefs_state_in_config).transpose()?.flatten();
210 if state == Some(ComposefsState::Tristate(Tristate::Disabled)) {
211 return Ok(None);
212 }
213 let Some(image) = deployment
214 .open_optional(COMPOSEFS_IMAGE)
215 .with_context(|| format!("Opening {COMPOSEFS_IMAGE}"))?
216 else {
217 let required = matches!(
218 state,
219 Some(ComposefsState::Signed | ComposefsState::Verity)
220 | Some(ComposefsState::Tristate(Tristate::Enabled))
221 );
222 anyhow::ensure!(
223 !required,
224 "composefs is enabled, but {COMPOSEFS_IMAGE} is missing"
225 );
226 return Ok(None);
227 };
228 let verity = if state
229 .as_ref()
230 .is_some_and(ComposefsState::requires_fsverity)
231 {
232 VerityRequirement::Required
233 } else {
234 VerityRequirement::Disabled
235 };
236 let objects = repo
237 .open_dir("objects")
238 .context("Opening repository objects")?;
239 let mount = composefs_fsmount(
240 image.into_std().into(),
241 name,
242 &[objects.as_fd()],
243 verity,
244 &MountOptions::default(),
245 )
246 .with_context(|| format!("Mounting {COMPOSEFS_IMAGE}"))?;
247 Ok(Some(mount))
248}
249
250#[cfg(test)]
251mod tests {
252 use super::*;
253
254 #[test]
255 fn test_tristate() {
256 for v in ["yes", "true", "1"] {
257 assert_eq!(Tristate::from_str(v).unwrap(), Tristate::Enabled);
258 }
259 assert_eq!(Tristate::from_str("maybe").unwrap(), Tristate::Maybe);
260 for v in ["no", "false", "0"] {
261 assert_eq!(Tristate::from_str(v).unwrap(), Tristate::Disabled);
262 }
263 for v in ["", "junk", "fal", "tr1"] {
264 assert!(Tristate::from_str(v).is_err());
265 }
266 }
267
268 #[test]
269 fn test_composefs_state() {
270 assert_eq!(
271 ComposefsState::from_str("signed").unwrap(),
272 ComposefsState::Signed
273 );
274 for v in ["yes", "true", "1"] {
275 assert_eq!(
276 ComposefsState::from_str(v).unwrap(),
277 ComposefsState::Tristate(Tristate::Enabled)
278 );
279 }
280 assert_eq!(Tristate::from_str("maybe").unwrap(), Tristate::Maybe);
281 for v in ["no", "false", "0"] {
282 assert_eq!(
283 ComposefsState::from_str(v).unwrap(),
284 ComposefsState::Tristate(Tristate::Disabled)
285 );
286 }
287 }
288
289 #[test]
290 fn test_overlayfs_enabled() {
291 let d0 = indoc::indoc! { r#"
292[foo]
293bar = baz
294[root]
295"# };
296 let d1 = indoc::indoc! { r#"
297[root]
298transient = false
299 "# };
300 let d2 = indoc::indoc! { r#"
301[composefs]
302enabled = false
303 "# };
304 for v in ["", d0, d1, d2] {
305 let kf = glib::KeyFile::new();
306 kf.load_from_data(v, glib::KeyFileFlags::empty()).unwrap();
307 assert!(!overlayfs_enabled_in_config(&kf).unwrap());
308 }
309
310 let e0 = format!("{d0}\n[root]\ntransient = true");
311 let e1 = format!("{d1}\n[composefs]\nenabled = true\n[other]\nsomekey = someval");
312 let e2 = format!("{d1}\n[composefs]\nenabled = yes");
313 let e3 = format!("{d1}\n[composefs]\nenabled = signed");
314 for v in [e0, e1, e2, e3] {
315 let kf = glib::KeyFile::new();
316 kf.load_from_data(&v, glib::KeyFileFlags::empty()).unwrap();
317 assert!(overlayfs_enabled_in_config(&kf).unwrap());
318 }
319 }
320
321 #[test]
322 fn test_mount_composefs_without_image() {
323 let td =
324 cap_std_ext::cap_tempfile::tempdir(cap_std_ext::cap_std::ambient_authority()).unwrap();
325 let cases = [
327 (None, false),
328 (Some("no"), false),
329 (Some("maybe"), false),
330 (Some("yes"), true),
331 (Some("verity"), true),
332 (Some("signed"), true),
333 ];
334 for (enabled, err) in cases {
335 let kf = glib::KeyFile::new();
336 if let Some(v) = enabled {
337 kf.set_string("composefs", "enabled", v);
338 }
339 match mount_composefs(&td, &td, "test", Some(&kf)) {
340 Ok(m) => {
341 assert!(!err, "{enabled:?}: expected an error");
342 assert!(m.is_none(), "{enabled:?}");
343 }
344 Err(e) => assert!(err, "{enabled:?}: {e}"),
345 }
346 }
347 assert!(mount_composefs(&td, &td, "test", None).unwrap().is_none());
348 td.write(COMPOSEFS_IMAGE, "").unwrap();
350 let kf = glib::KeyFile::new();
351 kf.set_string("composefs", "enabled", "no");
352 assert!(
353 mount_composefs(&td, &td, "test", Some(&kf))
354 .unwrap()
355 .is_none()
356 );
357 }
358}