bootc_lib/store/
mod.rs

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