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