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                //TODO: this assumes a single ESP on the root device
202                let root_dev =
203                    bootc_blockdev::list_dev_by_dir(&physical_root)?.require_single_root()?;
204                let esp_dev = root_dev.find_partition_of_esp()?;
205                let esp_mount = mount_esp(&esp_dev.path())?;
206
207                let boot_dir = match get_bootloader()? {
208                    Bootloader::Grub => physical_root.open_dir("boot").context("Opening boot")?,
209                    // NOTE: Handle XBOOTLDR partitions here if and when we use it
210                    Bootloader::Systemd => esp_mount.fd.try_clone().context("Cloning fd")?,
211                    Bootloader::None => unreachable!("Checked at install time"),
212                };
213
214                let storage = Storage {
215                    physical_root,
216                    physical_root_path: Utf8PathBuf::from("/sysroot"),
217                    run,
218                    boot_dir: Some(boot_dir),
219                    esp: Some(esp_mount),
220                    ostree: Default::default(),
221                    composefs: OnceCell::from(composefs),
222                    imgstore: Default::default(),
223                };
224
225                Some(Self { storage })
226            }
227            Environment::OstreeBooted => {
228                // The caller must have entered a private mount namespace before
229                // calling this function. This is because ostree's sysroot.load() will
230                // remount /sysroot as writable, and we call set_mount_namespace_in_use()
231                // to indicate we're in a mount namespace. Without actually being in a
232                // mount namespace, this would leave the global /sysroot writable.
233                let (physical_root, run) = get_physical_root_and_run()?;
234
235                let sysroot = ostree::Sysroot::new_default();
236                sysroot.set_mount_namespace_in_use();
237                let sysroot = ostree_ext::sysroot::SysrootLock::new_from_sysroot(&sysroot).await?;
238                sysroot.load(gio::Cancellable::NONE)?;
239
240                let storage = Storage {
241                    physical_root,
242                    physical_root_path: Utf8PathBuf::from("/sysroot"),
243                    run,
244                    boot_dir: None,
245                    esp: None,
246                    ostree: OnceCell::from(sysroot),
247                    composefs: Default::default(),
248                    imgstore: Default::default(),
249                };
250
251                Some(Self { storage })
252            }
253            // For container or non-bootc environments, there's no storage
254            Environment::Container | Environment::Other => None,
255        };
256        Ok(r)
257    }
258
259    /// Determine the boot storage backend kind.
260    ///
261    /// Returns information about whether the system booted via ostree or composefs,
262    /// along with the relevant sysroot/deployment or repository/cmdline data.
263    pub(crate) fn kind(&self) -> Result<BootedStorageKind<'_>> {
264        if let Some(cmdline) = composefs_booted()? {
265            // SAFETY: This must have been set above in new()
266            let repo = self.composefs.get().unwrap();
267            Ok(BootedStorageKind::Composefs(BootedComposefs {
268                repo: Arc::clone(repo),
269                cmdline,
270            }))
271        } else {
272            // SAFETY: This must have been set above in new()
273            let sysroot = self.ostree.get().unwrap();
274            let deployment = sysroot.require_booted_deployment()?;
275            Ok(BootedStorageKind::Ostree(BootedOstree {
276                sysroot,
277                deployment,
278            }))
279        }
280    }
281}
282
283/// A reference to a physical filesystem root, plus
284/// accessors for the different types of container storage.
285pub(crate) struct Storage {
286    /// Directory holding the physical root
287    pub physical_root: Dir,
288
289    /// Absolute path to the physical root directory.
290    /// This is `/sysroot` on a running system, or the target mount point during install.
291    pub physical_root_path: Utf8PathBuf,
292
293    /// The 'boot' directory, useful and `Some` only for composefs systems
294    /// For grub booted systems, this points to `/sysroot/boot`
295    /// For systemd booted systems, this points to the ESP
296    pub boot_dir: Option<Dir>,
297
298    /// The ESP mounted at a tmp location
299    pub esp: Option<TempMount>,
300
301    /// Our runtime state
302    run: Dir,
303
304    /// The OSTree storage
305    ostree: OnceCell<SysrootLock>,
306    /// The composefs storage
307    composefs: OnceCell<Arc<ComposefsRepository>>,
308    /// The containers-image storage used for LBIs
309    imgstore: OnceCell<CStorage>,
310}
311
312/// Cached image status data used for optimization.
313///
314/// This stores the current image status and any cached update information
315/// to avoid redundant fetches during status operations.
316#[derive(Default)]
317pub(crate) struct CachedImageStatus {
318    pub image: Option<ImageStatus>,
319    pub cached_update: Option<ImageStatus>,
320}
321
322impl Storage {
323    /// Create a new storage accessor from an existing ostree sysroot.
324    ///
325    /// This is used for non-booted scenarios (e.g., `bootc install`) where
326    /// we're operating on a target filesystem rather than the running system.
327    pub fn new_ostree(sysroot: SysrootLock, run: &Dir) -> Result<Self> {
328        let run = run.try_clone()?;
329
330        // ostree has historically always relied on
331        // having ostree -> sysroot/ostree as a symlink in the image to
332        // make it so that code doesn't need to distinguish between booted
333        // vs offline target. The ostree code all just looks at the ostree/
334        // directory, and will follow the link in the booted case.
335        //
336        // For composefs we aren't going to do a similar thing, so here
337        // we need to explicitly distinguish the two and the storage
338        // here hence holds a reference to the physical root.
339        let ostree_sysroot_dir = crate::utils::sysroot_dir(&sysroot)?;
340        let (physical_root, physical_root_path) = if sysroot.is_booted() {
341            (
342                ostree_sysroot_dir.open_dir(SYSROOT)?,
343                Utf8PathBuf::from("/sysroot"),
344            )
345        } else {
346            // For non-booted case (install), get the path from the sysroot
347            let path = sysroot.path();
348            let path_str = path.parse_name().to_string();
349            let path = Utf8PathBuf::from(path_str);
350            (ostree_sysroot_dir, path)
351        };
352
353        let ostree_cell = OnceCell::new();
354        let _ = ostree_cell.set(sysroot);
355
356        Ok(Self {
357            physical_root,
358            physical_root_path,
359            run,
360            boot_dir: None,
361            esp: None,
362            ostree: ostree_cell,
363            composefs: Default::default(),
364            imgstore: Default::default(),
365        })
366    }
367
368    /// Returns `boot_dir` if it exists
369    pub(crate) fn require_boot_dir(&self) -> Result<&Dir> {
370        self.boot_dir
371            .as_ref()
372            .ok_or_else(|| anyhow::anyhow!("Boot dir not found"))
373    }
374
375    /// Returns the mounted `esp` if it exists
376    pub(crate) fn require_esp(&self) -> Result<&TempMount> {
377        self.esp
378            .as_ref()
379            .ok_or_else(|| anyhow::anyhow!("ESP not found"))
380    }
381
382    /// Returns the Directory where the Type1 boot binaries are stored
383    /// `/sysroot/boot` for Grub, and ESP/EFI/Linux for systemd-boot
384    pub(crate) fn bls_boot_binaries_dir(&self) -> Result<Dir> {
385        let boot_dir = self.require_boot_dir()?;
386
387        // boot dir in case of systemd-boot points to the ESP, but we store
388        // the actual binaries inside ESP/EFI/Linux
389        let boot_dir = match get_bootloader()? {
390            Bootloader::Grub => boot_dir.try_clone()?,
391            Bootloader::Systemd => {
392                let boot_dir = boot_dir
393                    .open_dir(EFI_LINUX)
394                    .with_context(|| format!("Opening {EFI_LINUX}"))?;
395
396                boot_dir
397            }
398            Bootloader::None => anyhow::bail!("Unknown bootloader"),
399        };
400
401        Ok(boot_dir)
402    }
403
404    /// Access the underlying ostree repository
405    pub(crate) fn get_ostree(&self) -> Result<&SysrootLock> {
406        self.ostree
407            .get()
408            .ok_or_else(|| anyhow::anyhow!("OSTree storage not initialized"))
409    }
410
411    /// Get a cloned reference to the ostree sysroot.
412    ///
413    /// This is used when code needs an owned `ostree::Sysroot` rather than
414    /// a reference to the `SysrootLock`.
415    pub(crate) fn get_ostree_cloned(&self) -> Result<ostree::Sysroot> {
416        let r = self.get_ostree()?;
417        Ok((*r).clone())
418    }
419
420    /// Access the image storage; will automatically initialize it if necessary.
421    pub(crate) fn get_ensure_imgstore(&self) -> Result<&CStorage> {
422        if let Some(imgstore) = self.imgstore.get() {
423            return Ok(imgstore);
424        }
425        let ostree = self.get_ostree()?;
426        let sysroot_dir = crate::utils::sysroot_dir(ostree)?;
427
428        let sepolicy = if ostree.booted_deployment().is_none() {
429            // fallback to policy from container root
430            // this should only happen during cleanup of a broken install
431            tracing::trace!("falling back to container root's selinux policy");
432            let container_root = Dir::open_ambient_dir("/", cap_std::ambient_authority())?;
433            lsm::new_sepolicy_at(&container_root)?
434        } else {
435            // load the sepolicy from the booted ostree deployment so the imgstorage can be
436            // properly labeled with /var/lib/container/storage labels
437            tracing::trace!("loading sepolicy from booted ostree deployment");
438            let dep = ostree.booted_deployment().unwrap();
439            let dep_fs = deployment_fd(ostree, &dep)?;
440            lsm::new_sepolicy_at(&dep_fs)?
441        };
442
443        tracing::trace!("sepolicy in get_ensure_imgstore: {sepolicy:?}");
444
445        let imgstore = CStorage::create(&sysroot_dir, &self.run, sepolicy.as_ref())?;
446        Ok(self.imgstore.get_or_init(|| imgstore))
447    }
448
449    /// Ensure the image storage is properly SELinux-labeled. This should be
450    /// called after all image pulls are complete.
451    pub(crate) fn ensure_imgstore_labeled(&self) -> Result<()> {
452        if let Some(imgstore) = self.imgstore.get() {
453            imgstore.ensure_labeled()?;
454        }
455        Ok(())
456    }
457
458    /// Access the composefs repository; will automatically initialize it if necessary.
459    ///
460    /// This lazily opens the composefs repository, creating the directory if needed
461    /// and bootstrapping verity settings from the ostree configuration.
462    pub(crate) fn get_ensure_composefs(&self) -> Result<Arc<ComposefsRepository>> {
463        if let Some(composefs) = self.composefs.get() {
464            return Ok(Arc::clone(composefs));
465        }
466
467        ensure_composefs_dir(&self.physical_root)?;
468
469        // Bootstrap verity off of the ostree state. In practice this means disabled by
470        // default right now.
471        let ostree = self.get_ostree()?;
472        let ostree_repo = &ostree.repo();
473        let ostree_verity = ostree_ext::fsverity::is_verity_enabled(ostree_repo)?;
474        let mut composefs =
475            ComposefsRepository::open_path(self.physical_root.open_dir(COMPOSEFS)?, ".")?;
476        if !ostree_verity.enabled {
477            tracing::debug!("Setting insecure mode for composefs repo");
478            composefs.set_insecure(true);
479        }
480        let composefs = Arc::new(composefs);
481        let r = Arc::clone(self.composefs.get_or_init(|| composefs));
482        Ok(r)
483    }
484
485    /// Update the mtime on the storage root directory
486    #[context("Updating storage root mtime")]
487    pub(crate) fn update_mtime(&self) -> Result<()> {
488        let ostree = self.get_ostree()?;
489        let sysroot_dir = crate::utils::sysroot_dir(ostree).context("Reopen sysroot directory")?;
490
491        sysroot_dir
492            .update_timestamps(std::path::Path::new(BOOTC_ROOT))
493            .context("update_timestamps")
494    }
495}
496
497#[cfg(test)]
498mod tests {
499    use super::*;
500
501    /// The raw mode returned by metadata includes file type bits (S_IFDIR,
502    /// etc.) in addition to permission bits. This constant masks to only
503    /// the permission bits (owner/group/other rwx).
504    const PERMS: Mode = Mode::from_raw_mode(0o777);
505
506    #[test]
507    fn test_ensure_composefs_dir_mode() -> Result<()> {
508        use cap_std_ext::cap_primitives::fs::PermissionsExt as _;
509
510        let td = cap_std_ext::cap_tempfile::TempDir::new(cap_std::ambient_authority())?;
511
512        let assert_mode = || -> Result<()> {
513            let perms = td.metadata(COMPOSEFS)?.permissions();
514            let mode = Mode::from_raw_mode(perms.mode());
515            assert_eq!(mode & PERMS, COMPOSEFS_MODE);
516            Ok(())
517        };
518
519        ensure_composefs_dir(&td)?;
520        assert_mode()?;
521
522        // Calling again should be a no-op (ensure is idempotent)
523        ensure_composefs_dir(&td)?;
524        assert_mode()?;
525
526        Ok(())
527    }
528
529    #[test]
530    fn test_ensure_composefs_dir_fixes_existing() -> Result<()> {
531        use cap_std_ext::cap_primitives::fs::PermissionsExt as _;
532
533        let td = cap_std_ext::cap_tempfile::TempDir::new(cap_std::ambient_authority())?;
534
535        // Create with overly permissive mode (simulating old bootc behavior)
536        let mut db = DirBuilder::new();
537        db.mode(0o755);
538        td.create_dir_with(COMPOSEFS, &db)?;
539
540        // Verify it starts with wrong permissions
541        let perms = td.metadata(COMPOSEFS)?.permissions();
542        let mode = Mode::from_raw_mode(perms.mode());
543        assert_eq!(mode & PERMS, Mode::from_raw_mode(0o755));
544
545        // ensure_composefs_dir should fix the permissions
546        ensure_composefs_dir(&td)?;
547
548        let perms = td.metadata(COMPOSEFS)?.permissions();
549        let mode = Mode::from_raw_mode(perms.mode());
550        assert_eq!(mode & PERMS, COMPOSEFS_MODE);
551
552        Ok(())
553    }
554}