1
0
mirror of https://github.com/containers/bootc.git synced 2026-02-05 06:45:13 +01:00
Files
bootc/crates/lib/src/store/mod.rs
Colin Walters 3efcbddeb3 lib: Fix bootc status on non-bootc systems
Previously, `BootedStorage::new()` unconditionally tried to open
`/sysroot` before checking the environment type. This caused
`bootc status` to fail on non-ostree/composefs systems.

(We did work in containers and we had tests for that; but
 the container case is special cased even earlier)

Fixes: https://issues.redhat.com/browse/RHEL-135687

Assisted-by: Claude Code (Opus 4.5)
Signed-off-by: Colin Walters <walters@verbum.org>
2025-12-14 10:06:53 -05:00

433 lines
16 KiB
Rust

//! The [`Store`] holds references to three different types of
//! storage:
//!
//! # OSTree
//!
//! The default backend for the bootable container store; this
//! lives in `/ostree` in the physical root.
//!
//! # containers-storage:
//!
//! Later, bootc gained support for Logically Bound Images.
//! This is a `containers-storage:` instance that lives
//! in `/ostree/bootc/storage`
//!
//! # composefs
//!
//! This lives in `/composefs` in the physical root.
use std::cell::OnceCell;
use std::ops::Deref;
use std::sync::Arc;
use anyhow::{Context, Result};
use bootc_mount::tempmount::TempMount;
use camino::Utf8PathBuf;
use cap_std_ext::cap_std;
use cap_std_ext::cap_std::fs::{Dir, DirBuilder, DirBuilderExt as _};
use cap_std_ext::dirext::CapStdExtDirExt;
use fn_error_context::context;
use ostree_ext::container_utils::ostree_booted;
use ostree_ext::prelude::FileExt;
use ostree_ext::sysroot::SysrootLock;
use ostree_ext::{gio, ostree};
use rustix::fs::Mode;
use crate::bootc_composefs::boot::{get_esp_partition, get_sysroot_parent_dev, mount_esp};
use crate::bootc_composefs::status::{composefs_booted, get_bootloader, ComposefsCmdline};
use crate::lsm;
use crate::podstorage::CStorage;
use crate::spec::{Bootloader, ImageStatus};
use crate::utils::{deployment_fd, open_dir_remount_rw};
/// See https://github.com/containers/composefs-rs/issues/159
pub type ComposefsRepository =
composefs::repository::Repository<composefs::fsverity::Sha512HashValue>;
pub type ComposefsFilesystem = composefs::tree::FileSystem<composefs::fsverity::Sha512HashValue>;
/// Path to the physical root
pub const SYSROOT: &str = "sysroot";
/// The toplevel composefs directory path
pub const COMPOSEFS: &str = "composefs";
#[allow(dead_code)]
pub const COMPOSEFS_MODE: Mode = Mode::from_raw_mode(0o700);
/// The path to the bootc root directory, relative to the physical
/// system root
pub(crate) const BOOTC_ROOT: &str = "ostree/bootc";
/// Storage accessor for a booted system.
///
/// This wraps [`Storage`] and can determine whether the system is booted
/// via ostree or composefs, providing a unified interface for both.
pub(crate) struct BootedStorage {
pub(crate) storage: Storage,
}
impl Deref for BootedStorage {
type Target = Storage;
fn deref(&self) -> &Self::Target {
&self.storage
}
}
/// Represents an ostree-based boot environment
pub struct BootedOstree<'a> {
pub(crate) sysroot: &'a SysrootLock,
pub(crate) deployment: ostree::Deployment,
}
impl<'a> BootedOstree<'a> {
/// Get the ostree repository
pub(crate) fn repo(&self) -> ostree::Repo {
self.sysroot.repo()
}
/// Get the stateroot name
pub(crate) fn stateroot(&self) -> ostree::glib::GString {
self.deployment.osname()
}
}
/// Represents a composefs-based boot environment
#[allow(dead_code)]
pub struct BootedComposefs {
pub repo: Arc<ComposefsRepository>,
pub cmdline: &'static ComposefsCmdline,
}
/// Discriminated union representing the boot storage backend.
///
/// The runtime environment in which bootc is executing.
pub(crate) enum Environment {
/// System booted via ostree
OstreeBooted,
/// System booted via composefs
ComposefsBooted(ComposefsCmdline),
/// Running in a container
Container,
/// Other (not booted via bootc)
Other,
}
impl Environment {
/// Detect the current runtime environment.
pub(crate) fn detect() -> Result<Self> {
if ostree_ext::container_utils::running_in_container() {
return Ok(Self::Container);
}
if let Some(cmdline) = composefs_booted()? {
return Ok(Self::ComposefsBooted(cmdline.clone()));
}
if ostree_booted()? {
return Ok(Self::OstreeBooted);
}
Ok(Self::Other)
}
/// Returns true if this environment requires entering a mount namespace
/// before loading storage (to avoid leaving /sysroot writable).
pub(crate) fn needs_mount_namespace(&self) -> bool {
matches!(self, Self::OstreeBooted | Self::ComposefsBooted(_))
}
}
/// A system can boot via either ostree or composefs; this enum
/// allows code to handle both cases while maintaining type safety.
pub(crate) enum BootedStorageKind<'a> {
Ostree(BootedOstree<'a>),
Composefs(BootedComposefs),
}
/// Open the physical root (/sysroot) and /run directories for a booted system.
fn get_physical_root_and_run() -> Result<(Dir, Dir)> {
let physical_root = {
let d = Dir::open_ambient_dir("/sysroot", cap_std::ambient_authority())
.context("Opening /sysroot")?;
open_dir_remount_rw(&d, ".".into())?
};
let run =
Dir::open_ambient_dir("/run", cap_std::ambient_authority()).context("Opening /run")?;
Ok((physical_root, run))
}
impl BootedStorage {
/// Create a new booted storage accessor for the given environment.
///
/// The caller must have already called `prepare_for_write()` if
/// `env.needs_mount_namespace()` is true.
pub(crate) async fn new(env: Environment) -> Result<Option<Self>> {
let r = match &env {
Environment::ComposefsBooted(cmdline) => {
let (physical_root, run) = get_physical_root_and_run()?;
let mut composefs = ComposefsRepository::open_path(&physical_root, COMPOSEFS)?;
if cmdline.insecure {
composefs.set_insecure(true);
}
let composefs = Arc::new(composefs);
// NOTE: This is assuming that we'll only have composefs in a UEFI system
// We do have this assumptions in a lot of other places
let parent = get_sysroot_parent_dev(&physical_root)?;
let (esp_part, ..) = get_esp_partition(&parent)?;
let esp_mount = mount_esp(&esp_part)?;
let boot_dir = match get_bootloader()? {
Bootloader::Grub => physical_root.open_dir("boot").context("Opening boot")?,
// NOTE: Handle XBOOTLDR partitions here if and when we use it
Bootloader::Systemd => esp_mount.fd.try_clone().context("Cloning fd")?,
};
let storage = Storage {
physical_root,
physical_root_path: Utf8PathBuf::from("/sysroot"),
run,
boot_dir: Some(boot_dir),
esp: Some(esp_mount),
ostree: Default::default(),
composefs: OnceCell::from(composefs),
imgstore: Default::default(),
};
Some(Self { storage })
}
Environment::OstreeBooted => {
// The caller must have entered a private mount namespace before
// calling this function. This is because ostree's sysroot.load() will
// remount /sysroot as writable, and we call set_mount_namespace_in_use()
// to indicate we're in a mount namespace. Without actually being in a
// mount namespace, this would leave the global /sysroot writable.
let (physical_root, run) = get_physical_root_and_run()?;
let sysroot = ostree::Sysroot::new_default();
sysroot.set_mount_namespace_in_use();
let sysroot = ostree_ext::sysroot::SysrootLock::new_from_sysroot(&sysroot).await?;
sysroot.load(gio::Cancellable::NONE)?;
let storage = Storage {
physical_root,
physical_root_path: Utf8PathBuf::from("/sysroot"),
run,
boot_dir: None,
esp: None,
ostree: OnceCell::from(sysroot),
composefs: Default::default(),
imgstore: Default::default(),
};
Some(Self { storage })
}
// For container or non-bootc environments, there's no storage
Environment::Container | Environment::Other => None,
};
Ok(r)
}
/// Determine the boot storage backend kind.
///
/// Returns information about whether the system booted via ostree or composefs,
/// along with the relevant sysroot/deployment or repository/cmdline data.
pub(crate) fn kind(&self) -> Result<BootedStorageKind<'_>> {
if let Some(cmdline) = composefs_booted()? {
// SAFETY: This must have been set above in new()
let repo = self.composefs.get().unwrap();
Ok(BootedStorageKind::Composefs(BootedComposefs {
repo: Arc::clone(repo),
cmdline,
}))
} else {
// SAFETY: This must have been set above in new()
let sysroot = self.ostree.get().unwrap();
let deployment = sysroot.require_booted_deployment()?;
Ok(BootedStorageKind::Ostree(BootedOstree {
sysroot,
deployment,
}))
}
}
}
/// A reference to a physical filesystem root, plus
/// accessors for the different types of container storage.
pub(crate) struct Storage {
/// Directory holding the physical root
pub physical_root: Dir,
/// Absolute path to the physical root directory.
/// This is `/sysroot` on a running system, or the target mount point during install.
pub physical_root_path: Utf8PathBuf,
/// The 'boot' directory, useful and `Some` only for composefs systems
/// For grub booted systems, this points to `/sysroot/boot`
/// For systemd booted systems, this points to the ESP
pub boot_dir: Option<Dir>,
/// The ESP mounted at a tmp location
pub esp: Option<TempMount>,
/// Our runtime state
run: Dir,
/// The OSTree storage
ostree: OnceCell<SysrootLock>,
/// The composefs storage
composefs: OnceCell<Arc<ComposefsRepository>>,
/// The containers-image storage used for LBIs
imgstore: OnceCell<CStorage>,
}
/// Cached image status data used for optimization.
///
/// This stores the current image status and any cached update information
/// to avoid redundant fetches during status operations.
#[derive(Default)]
pub(crate) struct CachedImageStatus {
pub image: Option<ImageStatus>,
pub cached_update: Option<ImageStatus>,
}
impl Storage {
/// Create a new storage accessor from an existing ostree sysroot.
///
/// This is used for non-booted scenarios (e.g., `bootc install`) where
/// we're operating on a target filesystem rather than the running system.
pub fn new_ostree(sysroot: SysrootLock, run: &Dir) -> Result<Self> {
let run = run.try_clone()?;
// ostree has historically always relied on
// having ostree -> sysroot/ostree as a symlink in the image to
// make it so that code doesn't need to distinguish between booted
// vs offline target. The ostree code all just looks at the ostree/
// directory, and will follow the link in the booted case.
//
// For composefs we aren't going to do a similar thing, so here
// we need to explicitly distinguish the two and the storage
// here hence holds a reference to the physical root.
let ostree_sysroot_dir = crate::utils::sysroot_dir(&sysroot)?;
let (physical_root, physical_root_path) = if sysroot.is_booted() {
(
ostree_sysroot_dir.open_dir(SYSROOT)?,
Utf8PathBuf::from("/sysroot"),
)
} else {
// For non-booted case (install), get the path from the sysroot
let path = sysroot.path();
let path_str = path.parse_name().to_string();
let path = Utf8PathBuf::from(path_str);
(ostree_sysroot_dir, path)
};
let ostree_cell = OnceCell::new();
let _ = ostree_cell.set(sysroot);
Ok(Self {
physical_root,
physical_root_path,
run,
boot_dir: None,
esp: None,
ostree: ostree_cell,
composefs: Default::default(),
imgstore: Default::default(),
})
}
/// Returns `boot_dir` if it exists
pub(crate) fn require_boot_dir(&self) -> Result<&Dir> {
self.boot_dir
.as_ref()
.ok_or_else(|| anyhow::anyhow!("Boot dir not found"))
}
/// Access the underlying ostree repository
pub(crate) fn get_ostree(&self) -> Result<&SysrootLock> {
self.ostree
.get()
.ok_or_else(|| anyhow::anyhow!("OSTree storage not initialized"))
}
/// Get a cloned reference to the ostree sysroot.
///
/// This is used when code needs an owned `ostree::Sysroot` rather than
/// a reference to the `SysrootLock`.
pub(crate) fn get_ostree_cloned(&self) -> Result<ostree::Sysroot> {
let r = self.get_ostree()?;
Ok((*r).clone())
}
/// Access the image storage; will automatically initialize it if necessary.
pub(crate) fn get_ensure_imgstore(&self) -> Result<&CStorage> {
if let Some(imgstore) = self.imgstore.get() {
return Ok(imgstore);
}
let ostree = self.get_ostree()?;
let sysroot_dir = crate::utils::sysroot_dir(ostree)?;
let sepolicy = if ostree.booted_deployment().is_none() {
// fallback to policy from container root
// this should only happen during cleanup of a broken install
tracing::trace!("falling back to container root's selinux policy");
let container_root = Dir::open_ambient_dir("/", cap_std::ambient_authority())?;
lsm::new_sepolicy_at(&container_root)?
} else {
// load the sepolicy from the booted ostree deployment so the imgstorage can be
// properly labeled with /var/lib/container/storage labels
tracing::trace!("loading sepolicy from booted ostree deployment");
let dep = ostree.booted_deployment().unwrap();
let dep_fs = deployment_fd(ostree, &dep)?;
lsm::new_sepolicy_at(&dep_fs)?
};
tracing::trace!("sepolicy in get_ensure_imgstore: {sepolicy:?}");
let imgstore = CStorage::create(&sysroot_dir, &self.run, sepolicy.as_ref())?;
Ok(self.imgstore.get_or_init(|| imgstore))
}
/// Access the composefs repository; will automatically initialize it if necessary.
///
/// This lazily opens the composefs repository, creating the directory if needed
/// and bootstrapping verity settings from the ostree configuration.
pub(crate) fn get_ensure_composefs(&self) -> Result<Arc<ComposefsRepository>> {
if let Some(composefs) = self.composefs.get() {
return Ok(Arc::clone(composefs));
}
let mut db = DirBuilder::new();
db.mode(COMPOSEFS_MODE.as_raw_mode());
self.physical_root.ensure_dir_with(COMPOSEFS, &db)?;
// Bootstrap verity off of the ostree state. In practice this means disabled by
// default right now.
let ostree = self.get_ostree()?;
let ostree_repo = &ostree.repo();
let ostree_verity = ostree_ext::fsverity::is_verity_enabled(ostree_repo)?;
let mut composefs =
ComposefsRepository::open_path(self.physical_root.open_dir(COMPOSEFS)?, ".")?;
if !ostree_verity.enabled {
tracing::debug!("Setting insecure mode for composefs repo");
composefs.set_insecure(true);
}
let composefs = Arc::new(composefs);
let r = Arc::clone(self.composefs.get_or_init(|| composefs));
Ok(r)
}
/// Update the mtime on the storage root directory
#[context("Updating storage root mtime")]
pub(crate) fn update_mtime(&self) -> Result<()> {
let ostree = self.get_ostree()?;
let sysroot_dir = crate::utils::sysroot_dir(ostree).context("Reopen sysroot directory")?;
sysroot_dir
.update_timestamps(std::path::Path::new(BOOTC_ROOT))
.context("update_timestamps")
}
}