bootc_lib/store/
mod.rs

1//! The [`Storage`] type holds references to three different types of
2//! storage:
3//!
4//! # OSTree
5//!
6//! The default backend for the bootable container store; this
7//! lives in `/ostree` in the physical root.
8//!
9//! # containers-storage:
10//!
11//! Later, bootc gained support for Logically Bound Images.
12//! This is a `containers-storage:` instance that lives
13//! in `/ostree/bootc/storage`
14//!
15//! # composefs
16//!
17//! This lives in `/composefs` in the physical root.
18
19use std::cell::OnceCell;
20use std::ops::Deref;
21use std::sync::Arc;
22
23use anyhow::{Context, Result};
24use bootc_mount::tempmount::TempMount;
25use camino::Utf8PathBuf;
26use cap_std_ext::cap_std;
27use cap_std_ext::cap_std::fs::{
28    Dir, DirBuilder, DirBuilderExt as _, Permissions, PermissionsExt as _,
29};
30use cap_std_ext::dirext::CapStdExtDirExt;
31use fn_error_context::context;
32
33use ostree_ext::container_utils::ostree_booted;
34use ostree_ext::prelude::FileExt;
35use ostree_ext::sysroot::SysrootLock;
36use ostree_ext::{gio, ostree};
37use rustix::fs::Mode;
38
39use crate::bootc_composefs::boot::{get_esp_partition, get_sysroot_parent_dev, mount_esp};
40use crate::bootc_composefs::status::{ComposefsCmdline, composefs_booted, get_bootloader};
41use crate::lsm;
42use crate::podstorage::CStorage;
43use crate::spec::{Bootloader, ImageStatus};
44use crate::utils::{deployment_fd, open_dir_remount_rw};
45
46/// See <https://github.com/containers/composefs-rs/issues/159>
47pub type ComposefsRepository =
48    composefs::repository::Repository<composefs::fsverity::Sha512HashValue>;
49pub type ComposefsFilesystem = composefs::tree::FileSystem<composefs::fsverity::Sha512HashValue>;
50
51/// Path to the physical root
52pub const SYSROOT: &str = "sysroot";
53
54/// The toplevel composefs directory path
55pub const COMPOSEFS: &str = "composefs";
56
57/// The mode for the composefs directory; this is intentionally restrictive
58/// to avoid leaking information.
59pub(crate) const COMPOSEFS_MODE: Mode = Mode::from_raw_mode(0o700);
60
61/// Ensure the composefs directory exists in the given physical root
62/// with the correct permissions (mode 0700).
63pub(crate) fn ensure_composefs_dir(physical_root: &Dir) -> Result<()> {
64    let mut db = DirBuilder::new();
65    db.mode(COMPOSEFS_MODE.as_raw_mode());
66    physical_root
67        .ensure_dir_with(COMPOSEFS, &db)
68        .context("Creating composefs directory")?;
69    // Always update permissions, in case the directory pre-existed
70    // with incorrect mode (e.g. from an older version of bootc).
71    physical_root
72        .set_permissions(
73            COMPOSEFS,
74            Permissions::from_mode(COMPOSEFS_MODE.as_raw_mode()),
75        )
76        .context("Setting composefs directory permissions")?;
77    Ok(())
78}
79
80/// The path to the bootc root directory, relative to the physical
81/// system root
82pub(crate) const BOOTC_ROOT: &str = "ostree/bootc";
83
84/// Storage accessor for a booted system.
85///
86/// This wraps [`Storage`] and can determine whether the system is booted
87/// via ostree or composefs, providing a unified interface for both.
88pub(crate) struct BootedStorage {
89    pub(crate) storage: Storage,
90}
91
92impl Deref for BootedStorage {
93    type Target = Storage;
94
95    fn deref(&self) -> &Self::Target {
96        &self.storage
97    }
98}
99
100/// Represents an ostree-based boot environment
101pub struct BootedOstree<'a> {
102    pub(crate) sysroot: &'a SysrootLock,
103    pub(crate) deployment: ostree::Deployment,
104}
105
106impl<'a> BootedOstree<'a> {
107    /// Get the ostree repository
108    pub(crate) fn repo(&self) -> ostree::Repo {
109        self.sysroot.repo()
110    }
111
112    /// Get the stateroot name
113    pub(crate) fn stateroot(&self) -> ostree::glib::GString {
114        self.deployment.osname()
115    }
116}
117
118/// Represents a composefs-based boot environment
119#[allow(dead_code)]
120pub struct BootedComposefs {
121    pub repo: Arc<ComposefsRepository>,
122    pub cmdline: &'static ComposefsCmdline,
123}
124
125/// Discriminated union representing the boot storage backend.
126///
127/// The runtime environment in which bootc is executing.
128pub(crate) enum Environment {
129    /// System booted via ostree
130    OstreeBooted,
131    /// System booted via composefs
132    ComposefsBooted(ComposefsCmdline),
133    /// Running in a container
134    Container,
135    /// Other (not booted via bootc)
136    Other,
137}
138
139impl Environment {
140    /// Detect the current runtime environment.
141    pub(crate) fn detect() -> Result<Self> {
142        if ostree_ext::container_utils::running_in_container() {
143            return Ok(Self::Container);
144        }
145
146        if let Some(cmdline) = composefs_booted()? {
147            return Ok(Self::ComposefsBooted(cmdline.clone()));
148        }
149
150        if ostree_booted()? {
151            return Ok(Self::OstreeBooted);
152        }
153
154        Ok(Self::Other)
155    }
156
157    /// Returns true if this environment requires entering a mount namespace
158    /// before loading storage (to avoid leaving /sysroot writable).
159    pub(crate) fn needs_mount_namespace(&self) -> bool {
160        matches!(self, Self::OstreeBooted | Self::ComposefsBooted(_))
161    }
162}
163
164/// A system can boot via either ostree or composefs; this enum
165/// allows code to handle both cases while maintaining type safety.
166pub(crate) enum BootedStorageKind<'a> {
167    Ostree(BootedOstree<'a>),
168    Composefs(BootedComposefs),
169}
170
171/// Open the physical root (/sysroot) and /run directories for a booted system.
172fn get_physical_root_and_run() -> Result<(Dir, Dir)> {
173    let physical_root = {
174        let d = Dir::open_ambient_dir("/sysroot", cap_std::ambient_authority())
175            .context("Opening /sysroot")?;
176        open_dir_remount_rw(&d, ".".into())?
177    };
178    let run =
179        Dir::open_ambient_dir("/run", cap_std::ambient_authority()).context("Opening /run")?;
180    Ok((physical_root, run))
181}
182
183impl BootedStorage {
184    /// Create a new booted storage accessor for the given environment.
185    ///
186    /// The caller must have already called `prepare_for_write()` if
187    /// `env.needs_mount_namespace()` is true.
188    pub(crate) async fn new(env: Environment) -> Result<Option<Self>> {
189        let r = match &env {
190            Environment::ComposefsBooted(cmdline) => {
191                let (physical_root, run) = get_physical_root_and_run()?;
192                let mut composefs = ComposefsRepository::open_path(&physical_root, COMPOSEFS)?;
193                if cmdline.insecure {
194                    composefs.set_insecure(true);
195                }
196                let composefs = Arc::new(composefs);
197
198                // NOTE: This is assuming that we'll only have composefs in a UEFI system
199                // We do have this assumptions in a lot of other places
200                let parent = get_sysroot_parent_dev(&physical_root)?;
201                let (esp_part, ..) = get_esp_partition(&parent)?;
202                let esp_mount = mount_esp(&esp_part)?;
203
204                let boot_dir = match get_bootloader()? {
205                    Bootloader::Grub => physical_root.open_dir("boot").context("Opening boot")?,
206                    // NOTE: Handle XBOOTLDR partitions here if and when we use it
207                    Bootloader::Systemd => esp_mount.fd.try_clone().context("Cloning fd")?,
208                };
209
210                let storage = Storage {
211                    physical_root,
212                    physical_root_path: Utf8PathBuf::from("/sysroot"),
213                    run,
214                    boot_dir: Some(boot_dir),
215                    esp: Some(esp_mount),
216                    ostree: Default::default(),
217                    composefs: OnceCell::from(composefs),
218                    imgstore: Default::default(),
219                };
220
221                Some(Self { storage })
222            }
223            Environment::OstreeBooted => {
224                // The caller must have entered a private mount namespace before
225                // calling this function. This is because ostree's sysroot.load() will
226                // remount /sysroot as writable, and we call set_mount_namespace_in_use()
227                // to indicate we're in a mount namespace. Without actually being in a
228                // mount namespace, this would leave the global /sysroot writable.
229                let (physical_root, run) = get_physical_root_and_run()?;
230
231                let sysroot = ostree::Sysroot::new_default();
232                sysroot.set_mount_namespace_in_use();
233                let sysroot = ostree_ext::sysroot::SysrootLock::new_from_sysroot(&sysroot).await?;
234                sysroot.load(gio::Cancellable::NONE)?;
235
236                let storage = Storage {
237                    physical_root,
238                    physical_root_path: Utf8PathBuf::from("/sysroot"),
239                    run,
240                    boot_dir: None,
241                    esp: None,
242                    ostree: OnceCell::from(sysroot),
243                    composefs: Default::default(),
244                    imgstore: Default::default(),
245                };
246
247                Some(Self { storage })
248            }
249            // For container or non-bootc environments, there's no storage
250            Environment::Container | Environment::Other => None,
251        };
252        Ok(r)
253    }
254
255    /// Determine the boot storage backend kind.
256    ///
257    /// Returns information about whether the system booted via ostree or composefs,
258    /// along with the relevant sysroot/deployment or repository/cmdline data.
259    pub(crate) fn kind(&self) -> Result<BootedStorageKind<'_>> {
260        if let Some(cmdline) = composefs_booted()? {
261            // SAFETY: This must have been set above in new()
262            let repo = self.composefs.get().unwrap();
263            Ok(BootedStorageKind::Composefs(BootedComposefs {
264                repo: Arc::clone(repo),
265                cmdline,
266            }))
267        } else {
268            // SAFETY: This must have been set above in new()
269            let sysroot = self.ostree.get().unwrap();
270            let deployment = sysroot.require_booted_deployment()?;
271            Ok(BootedStorageKind::Ostree(BootedOstree {
272                sysroot,
273                deployment,
274            }))
275        }
276    }
277}
278
279/// A reference to a physical filesystem root, plus
280/// accessors for the different types of container storage.
281pub(crate) struct Storage {
282    /// Directory holding the physical root
283    pub physical_root: Dir,
284
285    /// Absolute path to the physical root directory.
286    /// This is `/sysroot` on a running system, or the target mount point during install.
287    pub physical_root_path: Utf8PathBuf,
288
289    /// The 'boot' directory, useful and `Some` only for composefs systems
290    /// For grub booted systems, this points to `/sysroot/boot`
291    /// For systemd booted systems, this points to the ESP
292    pub boot_dir: Option<Dir>,
293
294    /// The ESP mounted at a tmp location
295    pub esp: Option<TempMount>,
296
297    /// Our runtime state
298    run: Dir,
299
300    /// The OSTree storage
301    ostree: OnceCell<SysrootLock>,
302    /// The composefs storage
303    composefs: OnceCell<Arc<ComposefsRepository>>,
304    /// The containers-image storage used for LBIs
305    imgstore: OnceCell<CStorage>,
306}
307
308/// Cached image status data used for optimization.
309///
310/// This stores the current image status and any cached update information
311/// to avoid redundant fetches during status operations.
312#[derive(Default)]
313pub(crate) struct CachedImageStatus {
314    pub image: Option<ImageStatus>,
315    pub cached_update: Option<ImageStatus>,
316}
317
318impl Storage {
319    /// Create a new storage accessor from an existing ostree sysroot.
320    ///
321    /// This is used for non-booted scenarios (e.g., `bootc install`) where
322    /// we're operating on a target filesystem rather than the running system.
323    pub fn new_ostree(sysroot: SysrootLock, run: &Dir) -> Result<Self> {
324        let run = run.try_clone()?;
325
326        // ostree has historically always relied on
327        // having ostree -> sysroot/ostree as a symlink in the image to
328        // make it so that code doesn't need to distinguish between booted
329        // vs offline target. The ostree code all just looks at the ostree/
330        // directory, and will follow the link in the booted case.
331        //
332        // For composefs we aren't going to do a similar thing, so here
333        // we need to explicitly distinguish the two and the storage
334        // here hence holds a reference to the physical root.
335        let ostree_sysroot_dir = crate::utils::sysroot_dir(&sysroot)?;
336        let (physical_root, physical_root_path) = if sysroot.is_booted() {
337            (
338                ostree_sysroot_dir.open_dir(SYSROOT)?,
339                Utf8PathBuf::from("/sysroot"),
340            )
341        } else {
342            // For non-booted case (install), get the path from the sysroot
343            let path = sysroot.path();
344            let path_str = path.parse_name().to_string();
345            let path = Utf8PathBuf::from(path_str);
346            (ostree_sysroot_dir, path)
347        };
348
349        let ostree_cell = OnceCell::new();
350        let _ = ostree_cell.set(sysroot);
351
352        Ok(Self {
353            physical_root,
354            physical_root_path,
355            run,
356            boot_dir: None,
357            esp: None,
358            ostree: ostree_cell,
359            composefs: Default::default(),
360            imgstore: Default::default(),
361        })
362    }
363
364    /// Returns `boot_dir` if it exists
365    pub(crate) fn require_boot_dir(&self) -> Result<&Dir> {
366        self.boot_dir
367            .as_ref()
368            .ok_or_else(|| anyhow::anyhow!("Boot dir not found"))
369    }
370
371    /// Access the underlying ostree repository
372    pub(crate) fn get_ostree(&self) -> Result<&SysrootLock> {
373        self.ostree
374            .get()
375            .ok_or_else(|| anyhow::anyhow!("OSTree storage not initialized"))
376    }
377
378    /// Get a cloned reference to the ostree sysroot.
379    ///
380    /// This is used when code needs an owned `ostree::Sysroot` rather than
381    /// a reference to the `SysrootLock`.
382    pub(crate) fn get_ostree_cloned(&self) -> Result<ostree::Sysroot> {
383        let r = self.get_ostree()?;
384        Ok((*r).clone())
385    }
386
387    /// Access the image storage; will automatically initialize it if necessary.
388    pub(crate) fn get_ensure_imgstore(&self) -> Result<&CStorage> {
389        if let Some(imgstore) = self.imgstore.get() {
390            return Ok(imgstore);
391        }
392        let ostree = self.get_ostree()?;
393        let sysroot_dir = crate::utils::sysroot_dir(ostree)?;
394
395        let sepolicy = if ostree.booted_deployment().is_none() {
396            // fallback to policy from container root
397            // this should only happen during cleanup of a broken install
398            tracing::trace!("falling back to container root's selinux policy");
399            let container_root = Dir::open_ambient_dir("/", cap_std::ambient_authority())?;
400            lsm::new_sepolicy_at(&container_root)?
401        } else {
402            // load the sepolicy from the booted ostree deployment so the imgstorage can be
403            // properly labeled with /var/lib/container/storage labels
404            tracing::trace!("loading sepolicy from booted ostree deployment");
405            let dep = ostree.booted_deployment().unwrap();
406            let dep_fs = deployment_fd(ostree, &dep)?;
407            lsm::new_sepolicy_at(&dep_fs)?
408        };
409
410        tracing::trace!("sepolicy in get_ensure_imgstore: {sepolicy:?}");
411
412        let imgstore = CStorage::create(&sysroot_dir, &self.run, sepolicy.as_ref())?;
413        Ok(self.imgstore.get_or_init(|| imgstore))
414    }
415
416    /// Access the composefs repository; will automatically initialize it if necessary.
417    ///
418    /// This lazily opens the composefs repository, creating the directory if needed
419    /// and bootstrapping verity settings from the ostree configuration.
420    pub(crate) fn get_ensure_composefs(&self) -> Result<Arc<ComposefsRepository>> {
421        if let Some(composefs) = self.composefs.get() {
422            return Ok(Arc::clone(composefs));
423        }
424
425        ensure_composefs_dir(&self.physical_root)?;
426
427        // Bootstrap verity off of the ostree state. In practice this means disabled by
428        // default right now.
429        let ostree = self.get_ostree()?;
430        let ostree_repo = &ostree.repo();
431        let ostree_verity = ostree_ext::fsverity::is_verity_enabled(ostree_repo)?;
432        let mut composefs =
433            ComposefsRepository::open_path(self.physical_root.open_dir(COMPOSEFS)?, ".")?;
434        if !ostree_verity.enabled {
435            tracing::debug!("Setting insecure mode for composefs repo");
436            composefs.set_insecure(true);
437        }
438        let composefs = Arc::new(composefs);
439        let r = Arc::clone(self.composefs.get_or_init(|| composefs));
440        Ok(r)
441    }
442
443    /// Update the mtime on the storage root directory
444    #[context("Updating storage root mtime")]
445    pub(crate) fn update_mtime(&self) -> Result<()> {
446        let ostree = self.get_ostree()?;
447        let sysroot_dir = crate::utils::sysroot_dir(ostree).context("Reopen sysroot directory")?;
448
449        sysroot_dir
450            .update_timestamps(std::path::Path::new(BOOTC_ROOT))
451            .context("update_timestamps")
452    }
453}
454
455#[cfg(test)]
456mod tests {
457    use super::*;
458
459    /// The raw mode returned by metadata includes file type bits (S_IFDIR,
460    /// etc.) in addition to permission bits. This constant masks to only
461    /// the permission bits (owner/group/other rwx).
462    const PERMS: Mode = Mode::from_raw_mode(0o777);
463
464    #[test]
465    fn test_ensure_composefs_dir_mode() -> Result<()> {
466        use cap_std_ext::cap_primitives::fs::PermissionsExt as _;
467
468        let td = cap_std_ext::cap_tempfile::TempDir::new(cap_std::ambient_authority())?;
469
470        let assert_mode = || -> Result<()> {
471            let perms = td.metadata(COMPOSEFS)?.permissions();
472            let mode = Mode::from_raw_mode(perms.mode());
473            assert_eq!(mode & PERMS, COMPOSEFS_MODE);
474            Ok(())
475        };
476
477        ensure_composefs_dir(&td)?;
478        assert_mode()?;
479
480        // Calling again should be a no-op (ensure is idempotent)
481        ensure_composefs_dir(&td)?;
482        assert_mode()?;
483
484        Ok(())
485    }
486
487    #[test]
488    fn test_ensure_composefs_dir_fixes_existing() -> Result<()> {
489        use cap_std_ext::cap_primitives::fs::PermissionsExt as _;
490
491        let td = cap_std_ext::cap_tempfile::TempDir::new(cap_std::ambient_authority())?;
492
493        // Create with overly permissive mode (simulating old bootc behavior)
494        let mut db = DirBuilder::new();
495        db.mode(0o755);
496        td.create_dir_with(COMPOSEFS, &db)?;
497
498        // Verify it starts with wrong permissions
499        let perms = td.metadata(COMPOSEFS)?.permissions();
500        let mode = Mode::from_raw_mode(perms.mode());
501        assert_eq!(mode & PERMS, Mode::from_raw_mode(0o755));
502
503        // ensure_composefs_dir should fix the permissions
504        ensure_composefs_dir(&td)?;
505
506        let perms = td.metadata(COMPOSEFS)?.permissions();
507        let mode = Mode::from_raw_mode(perms.mode());
508        assert_eq!(mode & PERMS, COMPOSEFS_MODE);
509
510        Ok(())
511    }
512}