1use std::cell::OnceCell;
22use std::ops::Deref;
23use std::sync::Arc;
24
25use anyhow::{Context, Result};
26use bootc_mount::tempmount::TempMount;
27use camino::Utf8PathBuf;
28use cap_std_ext::cap_std;
29use cap_std_ext::cap_std::fs::{
30 Dir, DirBuilder, DirBuilderExt as _, Permissions, PermissionsExt as _,
31};
32use cap_std_ext::dirext::CapStdExtDirExt;
33use fn_error_context::context;
34
35use ostree_ext::container_utils::ostree_booted;
36use ostree_ext::prelude::FileExt;
37use ostree_ext::sysroot::SysrootLock;
38use ostree_ext::{gio, ostree};
39use rustix::fs::Mode;
40
41use composefs::fsverity::Sha512HashValue;
42use composefs_ctl::composefs;
43
44use crate::bootc_composefs::backwards_compat::bcompat_boot::prepend_custom_prefix;
45use crate::bootc_composefs::boot::{EFI_LINUX, mount_esp};
46use crate::bootc_composefs::status::{ComposefsCmdline, composefs_booted, get_bootloader};
47use crate::lsm;
48use crate::podstorage::CStorage;
49use crate::spec::{Bootloader, ImageStatus};
50use crate::utils::{deployment_fd, open_dir_remount_rw};
51
52pub type ComposefsRepository = composefs::repository::Repository<Sha512HashValue>;
54
55pub const SYSROOT: &str = "sysroot";
57
58pub const COMPOSEFS: &str = "composefs";
60
61pub(crate) const COMPOSEFS_MODE: Mode = Mode::from_raw_mode(0o700);
64
65pub(crate) fn ensure_composefs_dir(physical_root: &Dir) -> Result<()> {
68 let mut db = DirBuilder::new();
69 db.mode(COMPOSEFS_MODE.as_raw_mode());
70 physical_root
71 .ensure_dir_with(COMPOSEFS, &db)
72 .context("Creating composefs directory")?;
73 physical_root
76 .set_permissions(
77 COMPOSEFS,
78 Permissions::from_mode(COMPOSEFS_MODE.as_raw_mode()),
79 )
80 .context("Setting composefs directory permissions")?;
81 Ok(())
82}
83
84pub(crate) const BOOTC_ROOT: &str = "ostree/bootc";
89
90pub(crate) const COMPOSEFS_BOOTC_ROOT: &str = "composefs/bootc";
93
94pub(crate) fn ensure_composefs_bootc_link(physical_root: &Dir) -> Result<()> {
103 physical_root
105 .create_dir_all(COMPOSEFS_BOOTC_ROOT)
106 .with_context(|| format!("Creating {COMPOSEFS_BOOTC_ROOT}"))?;
107
108 physical_root
111 .create_dir_all("ostree")
112 .context("Creating ostree directory")?;
113
114 match physical_root.symlink_metadata(BOOTC_ROOT) {
118 Ok(meta) if meta.is_symlink() => {
119 return Ok(());
121 }
122 Ok(_meta) => {
123 tracing::warn!(
126 "{BOOTC_ROOT} already exists as a directory, not replacing with symlink"
127 );
128 return Ok(());
129 }
130 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
131 }
133 Err(e) => return Err(e).context(format!("Querying {BOOTC_ROOT}")),
134 }
135
136 physical_root
137 .symlink_contents(format!("../{COMPOSEFS_BOOTC_ROOT}"), BOOTC_ROOT)
138 .with_context(|| format!("Creating {BOOTC_ROOT} -> ../{COMPOSEFS_BOOTC_ROOT} symlink"))?;
139
140 tracing::info!("Created {BOOTC_ROOT} -> ../{COMPOSEFS_BOOTC_ROOT}");
141 Ok(())
142}
143
144pub(crate) struct BootedStorage {
149 pub(crate) storage: Storage,
150}
151
152impl Deref for BootedStorage {
153 type Target = Storage;
154
155 fn deref(&self) -> &Self::Target {
156 &self.storage
157 }
158}
159
160pub struct BootedOstree<'a> {
162 pub(crate) sysroot: &'a SysrootLock,
163 pub(crate) deployment: ostree::Deployment,
164}
165
166impl<'a> BootedOstree<'a> {
167 pub(crate) fn repo(&self) -> ostree::Repo {
169 self.sysroot.repo()
170 }
171
172 pub(crate) fn stateroot(&self) -> ostree::glib::GString {
174 self.deployment.osname()
175 }
176}
177
178#[allow(dead_code)]
180pub struct BootedComposefs {
181 pub repo: Arc<ComposefsRepository>,
182 pub cmdline: &'static ComposefsCmdline,
183}
184
185pub(crate) enum Environment {
189 OstreeBooted,
191 ComposefsBooted(ComposefsCmdline),
193 Container,
195 Other,
197}
198
199impl Environment {
200 pub(crate) fn detect() -> Result<Self> {
202 if ostree_ext::container_utils::running_in_container() {
203 return Ok(Self::Container);
204 }
205
206 if let Some(cmdline) = composefs_booted()? {
207 return Ok(Self::ComposefsBooted(cmdline.clone()));
208 }
209
210 if ostree_booted()? {
211 return Ok(Self::OstreeBooted);
212 }
213
214 Ok(Self::Other)
215 }
216
217 pub(crate) fn needs_mount_namespace(&self) -> bool {
220 matches!(self, Self::OstreeBooted | Self::ComposefsBooted(_))
221 }
222}
223
224pub(crate) enum BootedStorageKind<'a> {
227 Ostree(BootedOstree<'a>),
228 Composefs(BootedComposefs),
229}
230
231fn get_physical_root_and_run() -> Result<(Dir, Dir)> {
233 let physical_root = {
234 let d = Dir::open_ambient_dir("/sysroot", cap_std::ambient_authority())
235 .context("Opening /sysroot")?;
236 open_dir_remount_rw(&d, ".".into())?
237 };
238 let run =
239 Dir::open_ambient_dir("/run", cap_std::ambient_authority()).context("Opening /run")?;
240 Ok((physical_root, run))
241}
242
243impl BootedStorage {
244 pub(crate) async fn new(env: Environment) -> Result<Option<Self>> {
249 let r = match &env {
250 Environment::ComposefsBooted(cmdline) => {
251 let (physical_root, run) = get_physical_root_and_run()?;
252 let mut composefs = ComposefsRepository::open_path(&physical_root, COMPOSEFS)?;
253 if cmdline.allow_missing_fsverity {
254 composefs.set_insecure();
255 }
256 let composefs = Arc::new(composefs);
257
258 let root_dev = bootc_blockdev::list_dev_by_dir(&physical_root)?;
260 let esp_dev = root_dev.find_first_colocated_esp()?;
261 let esp_mount = mount_esp(&esp_dev.path())?;
262
263 let boot_dir = match get_bootloader()? {
264 Bootloader::Grub => physical_root.open_dir("boot").context("Opening boot")?,
265 Bootloader::Systemd => esp_mount.fd.try_clone().context("Cloning fd")?,
267 Bootloader::None => unreachable!("Checked at install time"),
268 };
269
270 let storage = Storage {
271 physical_root,
272 physical_root_path: Utf8PathBuf::from("/sysroot"),
273 run,
274 boot_dir: Some(boot_dir),
275 esp: Some(esp_mount),
276 ostree: Default::default(),
277 composefs: OnceCell::from(composefs.clone()),
278 imgstore: Default::default(),
279 };
280
281 let cmdline = composefs_booted()?
288 .ok_or_else(|| anyhow::anyhow!("Could not get booted composefs cmdline"))?;
289 prepend_custom_prefix(&storage, &cmdline).await?;
290
291 Some(Self { storage })
292 }
293 Environment::OstreeBooted => {
294 let (physical_root, run) = get_physical_root_and_run()?;
300
301 let sysroot = ostree::Sysroot::new_default();
302 sysroot.set_mount_namespace_in_use();
303 let sysroot = ostree_ext::sysroot::SysrootLock::new_from_sysroot(&sysroot).await?;
304 sysroot.load(gio::Cancellable::NONE)?;
305
306 let storage = Storage {
307 physical_root,
308 physical_root_path: Utf8PathBuf::from("/sysroot"),
309 run,
310 boot_dir: None,
311 esp: None,
312 ostree: OnceCell::from(sysroot),
313 composefs: Default::default(),
314 imgstore: Default::default(),
315 };
316
317 Some(Self { storage })
318 }
319 Environment::Container | Environment::Other => None,
321 };
322 Ok(r)
323 }
324
325 pub(crate) fn kind(&self) -> Result<BootedStorageKind<'_>> {
330 if let Some(cmdline) = composefs_booted()? {
331 let repo = self.composefs.get().unwrap();
333 Ok(BootedStorageKind::Composefs(BootedComposefs {
334 repo: Arc::clone(repo),
335 cmdline,
336 }))
337 } else {
338 let sysroot = self.ostree.get().unwrap();
340 let deployment = sysroot.require_booted_deployment()?;
341 Ok(BootedStorageKind::Ostree(BootedOstree {
342 sysroot,
343 deployment,
344 }))
345 }
346 }
347}
348
349pub(crate) struct Storage {
352 pub physical_root: Dir,
354
355 pub physical_root_path: Utf8PathBuf,
358
359 pub boot_dir: Option<Dir>,
363
364 pub esp: Option<TempMount>,
366
367 run: Dir,
369
370 ostree: OnceCell<SysrootLock>,
372 composefs: OnceCell<Arc<ComposefsRepository>>,
374 imgstore: OnceCell<CStorage>,
376}
377
378#[derive(Default)]
383pub(crate) struct CachedImageStatus {
384 pub image: Option<ImageStatus>,
385 pub cached_update: Option<ImageStatus>,
386}
387
388impl Storage {
389 pub fn new_ostree(sysroot: SysrootLock, run: &Dir) -> Result<Self> {
394 let run = run.try_clone()?;
395
396 let ostree_sysroot_dir = crate::utils::sysroot_dir(&sysroot)?;
406 let (physical_root, physical_root_path) = if sysroot.is_booted() {
407 (
408 ostree_sysroot_dir.open_dir(SYSROOT)?,
409 Utf8PathBuf::from("/sysroot"),
410 )
411 } else {
412 let path = sysroot.path();
414 let path_str = path.parse_name().to_string();
415 let path = Utf8PathBuf::from(path_str);
416 (ostree_sysroot_dir, path)
417 };
418
419 let ostree_cell = OnceCell::new();
420 let _ = ostree_cell.set(sysroot);
421
422 Ok(Self {
423 physical_root,
424 physical_root_path,
425 run,
426 boot_dir: None,
427 esp: None,
428 ostree: ostree_cell,
429 composefs: Default::default(),
430 imgstore: Default::default(),
431 })
432 }
433
434 pub(crate) fn require_boot_dir(&self) -> Result<&Dir> {
436 self.boot_dir
437 .as_ref()
438 .ok_or_else(|| anyhow::anyhow!("Boot dir not found"))
439 }
440
441 pub(crate) fn require_esp(&self) -> Result<&TempMount> {
443 self.esp
444 .as_ref()
445 .ok_or_else(|| anyhow::anyhow!("ESP not found"))
446 }
447
448 pub(crate) fn bls_boot_binaries_dir(&self) -> Result<Dir> {
451 let boot_dir = self.require_boot_dir()?;
452
453 let boot_dir = match get_bootloader()? {
456 Bootloader::Grub => boot_dir.try_clone()?,
457 Bootloader::Systemd => {
458 let boot_dir = boot_dir
459 .open_dir(EFI_LINUX)
460 .with_context(|| format!("Opening {EFI_LINUX}"))?;
461
462 boot_dir
463 }
464 Bootloader::None => anyhow::bail!("Unknown bootloader"),
465 };
466
467 Ok(boot_dir)
468 }
469
470 pub(crate) fn get_ostree(&self) -> Result<&SysrootLock> {
472 self.ostree
473 .get()
474 .ok_or_else(|| anyhow::anyhow!("OSTree storage not initialized"))
475 }
476
477 pub(crate) fn get_ostree_cloned(&self) -> Result<ostree::Sysroot> {
482 let r = self.get_ostree()?;
483 Ok((*r).clone())
484 }
485
486 pub(crate) fn get_ensure_imgstore(&self) -> Result<&CStorage> {
492 if let Some(imgstore) = self.imgstore.get() {
493 return Ok(imgstore);
494 }
495
496 let (sysroot_dir, sepolicy) = if let Ok(ostree) = self.get_ostree() {
497 let sysroot_dir = crate::utils::sysroot_dir(ostree)?;
498 let sepolicy = if ostree.booted_deployment().is_none() {
499 tracing::trace!("falling back to container root's selinux policy");
500 let container_root = Dir::open_ambient_dir("/", cap_std::ambient_authority())?;
501 lsm::new_sepolicy_at(&container_root)?
502 } else {
503 tracing::trace!("loading sepolicy from booted ostree deployment");
504 let dep = ostree.booted_deployment().unwrap();
505 let dep_fs = deployment_fd(ostree, &dep)?;
506 lsm::new_sepolicy_at(&dep_fs)?
507 };
508 (sysroot_dir, sepolicy)
509 } else {
510 let sysroot_dir = self.physical_root.try_clone()?;
513 let root = Dir::open_ambient_dir("/", cap_std::ambient_authority())?;
514 let sepolicy = lsm::new_sepolicy_at(&root)?;
515 (sysroot_dir, sepolicy)
516 };
517
518 tracing::trace!("sepolicy in get_ensure_imgstore: {sepolicy:?}");
519
520 let imgstore = CStorage::create(&sysroot_dir, &self.run, sepolicy.as_ref())?;
521 Ok(self.imgstore.get_or_init(|| imgstore))
522 }
523
524 pub(crate) fn ensure_imgstore_labeled(&self) -> Result<()> {
527 if let Some(imgstore) = self.imgstore.get() {
528 imgstore.ensure_labeled()?;
529 }
530 Ok(())
531 }
532
533 pub(crate) fn get_ensure_composefs(&self) -> Result<Arc<ComposefsRepository>> {
538 if let Some(composefs) = self.composefs.get() {
539 return Ok(Arc::clone(composefs));
540 }
541
542 ensure_composefs_dir(&self.physical_root)?;
543
544 let ostree = self.get_ostree()?;
547 let ostree_repo = &ostree.repo();
548 let ostree_verity = ostree_ext::fsverity::is_verity_enabled(ostree_repo)?;
549 let (mut composefs, _created) = ComposefsRepository::init_path(
550 self.physical_root.open_dir(COMPOSEFS)?,
551 ".",
552 composefs::fsverity::Algorithm::SHA512,
553 ostree_verity.enabled,
554 )?;
555 if !ostree_verity.enabled {
556 tracing::debug!("Setting insecure mode for composefs repo");
557 composefs.set_insecure();
558 }
559 let composefs = Arc::new(composefs);
560 let r = Arc::clone(self.composefs.get_or_init(|| composefs));
561 Ok(r)
562 }
563
564 #[context("Updating storage root mtime")]
569 pub(crate) fn update_mtime(&self) -> Result<()> {
570 let sysroot_dir = if let Ok(ostree) = self.get_ostree() {
573 crate::utils::sysroot_dir(ostree).context("Reopen sysroot directory")?
574 } else {
575 self.physical_root.try_clone()?
576 };
577
578 sysroot_dir
579 .update_timestamps(std::path::Path::new(BOOTC_ROOT))
580 .context("update_timestamps")
581 }
582}
583
584#[cfg(test)]
585mod tests {
586 use super::*;
587
588 const PERMS: Mode = Mode::from_raw_mode(0o777);
592
593 #[test]
594 fn test_ensure_composefs_dir_mode() -> Result<()> {
595 use cap_std_ext::cap_primitives::fs::PermissionsExt as _;
596
597 let td = cap_std_ext::cap_tempfile::TempDir::new(cap_std::ambient_authority())?;
598
599 let assert_mode = || -> Result<()> {
600 let perms = td.metadata(COMPOSEFS)?.permissions();
601 let mode = Mode::from_raw_mode(perms.mode());
602 assert_eq!(mode & PERMS, COMPOSEFS_MODE);
603 Ok(())
604 };
605
606 ensure_composefs_dir(&td)?;
607 assert_mode()?;
608
609 ensure_composefs_dir(&td)?;
611 assert_mode()?;
612
613 Ok(())
614 }
615
616 #[test]
617 fn test_ensure_composefs_dir_fixes_existing() -> Result<()> {
618 use cap_std_ext::cap_primitives::fs::PermissionsExt as _;
619
620 let td = cap_std_ext::cap_tempfile::TempDir::new(cap_std::ambient_authority())?;
621
622 let mut db = DirBuilder::new();
624 db.mode(0o755);
625 td.create_dir_with(COMPOSEFS, &db)?;
626
627 let perms = td.metadata(COMPOSEFS)?.permissions();
629 let mode = Mode::from_raw_mode(perms.mode());
630 assert_eq!(mode & PERMS, Mode::from_raw_mode(0o755));
631
632 ensure_composefs_dir(&td)?;
634
635 let perms = td.metadata(COMPOSEFS)?.permissions();
636 let mode = Mode::from_raw_mode(perms.mode());
637 assert_eq!(mode & PERMS, COMPOSEFS_MODE);
638
639 Ok(())
640 }
641}