diff --git a/Cargo.toml b/Cargo.toml index 2726a77e..40b1572c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,7 +37,7 @@ composefs = { version = "0.9.3", path = "crates/composefs", default-features = f composefs-ctl = { version = "0.9.3", path = "crates/composefs-ctl", default-features = false } composefs-ioctls = { version = "0.9.3", path = "crates/composefs-ioctls", default-features = false } composefs-oci = { version = "0.9.3", path = "crates/composefs-oci", default-features = false } -composefs-boot = { version = "0.9.3", path = "crates/composefs-boot", default-features = false } +composefs-boot = { version = "0.9.3", path = "crates/composefs-boot" } composefs-fuse = { version = "0.9.3", path = "crates/composefs-fuse", default-features = false } composefs-http = { version = "0.9.3", path = "crates/composefs-http", default-features = false } composefs-ostree = { version = "0.9.3", path = "crates/composefs-ostree", default-features = false } diff --git a/crates/composefs-boot/Cargo.toml b/crates/composefs-boot/Cargo.toml index 0e32b453..d89af601 100644 --- a/crates/composefs-boot/Cargo.toml +++ b/crates/composefs-boot/Cargo.toml @@ -10,14 +10,17 @@ repository.workspace = true rust-version.workspace = true version.workspace = true +[features] +default = ["composefs-integration"] +composefs-integration = ["dep:anyhow", "dep:composefs", "dep:fn-error-context", "dep:pcre2", "dep:regex-automata", "dep:rustix"] + [dependencies] -anyhow = { version = "1.0.87", default-features = false } -fn-error-context = "0.2" -composefs = { workspace = true } -hex = { version = "0.4.0", default-features = false, features = ["std"] } -pcre2 = "0.2.11" -regex-automata = { version = "0.4.4", default-features = false, features = ["hybrid", "std", "syntax"] } -rustix = { version = "1.0.0", default-features = false, features = ["fs", "std"] } +anyhow = { version = "1.0.87", default-features = false, optional = true } +fn-error-context = { version = "0.2", optional = true } +composefs = { workspace = true, optional = true } +pcre2 = { version = "0.2.11", optional = true } +regex-automata = { version = "0.4.4", default-features = false, features = ["hybrid", "std", "syntax"], optional = true } +rustix = { version = "1.0.0", default-features = false, features = ["fs", "std"], optional = true } thiserror = { version = "2.0.0", default-features = false } zerocopy = { version = "0.8.0", default-features = false, features = ["derive"] } diff --git a/crates/composefs-boot/src/integration.rs b/crates/composefs-boot/src/integration.rs new file mode 100644 index 00000000..4fc3bde4 --- /dev/null +++ b/crates/composefs-boot/src/integration.rs @@ -0,0 +1,109 @@ +use std::ffi::OsStr; + +use anyhow::Result; +use rustix::fd::AsFd; + +use composefs::{fsverity::FsVerityHashValue, repository::Repository, tree::FileSystem}; + +use crate::{ + bootloader::{BootEntry, get_boot_resources}, + selabel, +}; + +/// These directories are required to exist in images. +/// They may have content in the container, but we don't +/// want to expose them in the final merged root. +/// +/// # /boot +/// +/// This is how sealed UKIs are handled; the UKI in /boot has the composefs +/// digest, so we can't include it in the rendered image. +/// +/// # /sysroot +/// +/// See https://github.com/containers/composefs-rs/issues/164 +/// Basically there is only content here in ostree-container cases, +/// and us traversing there for SELinux labeling will cause problems. +/// The ostree-container code special cases it in a different way, but +/// here we can just ignore it. +const REQUIRED_TOPLEVEL_TO_EMPTY_DIRS: &[&str] = &["boot", "sysroot"]; + +/// Empty the required top-level directories and set their mtime to match /usr. +fn empty_toplevel_dirs(fs: &mut FileSystem) -> Result<()> { + let usr = fs.root.get_directory(OsStr::new("usr"))?; + let usr_mtime_stat_sec = usr.stat.st_mtim_sec; + let usr_mtime_stat_nsec = usr.stat.st_mtim_nsec; + + for d in REQUIRED_TOPLEVEL_TO_EMPTY_DIRS { + let d = fs.root.get_directory_mut(d.as_ref())?; + d.stat.st_mtim_sec = usr_mtime_stat_sec; + d.stat.st_mtim_nsec = usr_mtime_stat_nsec; + d.clear(); + } + + Ok(()) +} + +/// Trait for transforming filesystem images for boot scenarios. +/// +/// This trait provides functionality to prepare composefs filesystem images for booting by +/// extracting boot resources and applying necessary transformations like SELinux labeling. +pub trait BootOps { + /// Transforms a filesystem image for boot by extracting boot entries and applying SELinux labels. + /// + /// This method extracts boot resources from the filesystem, empties required top-level + /// directories (/boot, /sysroot), and applies SELinux security contexts. + /// + /// # Arguments + /// + /// * `repo` - The composefs repository containing filesystem objects + /// + /// # Returns + /// + /// A vector of boot entries extracted from the filesystem (Type 1 BLS entries, Type 2 UKIs, etc.) + fn transform_for_boot( + &mut self, + repo: &Repository, + ) -> Result>>; + + /// Apply boot filesystem transformations using an on-disk directory for file content. + /// + /// This applies the same filesystem transformations as [`BootOps::transform_for_boot`] + /// (emptying /boot and /sysroot, SELinux relabeling) but reads SELinux policy files + /// directly from the on-disk filesystem via a directory fd rather than from the + /// composefs repository. + /// + /// This does not extract boot entries (Type 1 BLS entries, UKIs, etc.) since those + /// are only needed for writing to the boot partition, not for computing the composefs + /// digest. + /// + /// # Arguments + /// + /// * `rootfs` - A directory fd pointing to the root of the on-disk filesystem + fn transform_for_boot_from_dir(&mut self, rootfs: impl AsFd) -> Result<()>; +} + +impl BootOps for FileSystem { + fn transform_for_boot( + &mut self, + repo: &Repository, + ) -> Result>> { + let boot_entries = get_boot_resources(self, repo)?; + empty_toplevel_dirs(self)?; + // Compact the leaves table after clearing directories, so that leaves + // which were only referenced by /boot or /sysroot are removed and + // don't appear as orphans when the filesystem is validated. + self.compact(); + selabel::selabel(self, repo)?; + + Ok(boot_entries) + } + + fn transform_for_boot_from_dir(&mut self, rootfs: impl AsFd) -> Result<()> { + empty_toplevel_dirs(self)?; + // Same as above: compact to remove leaves orphaned by clearing dirs. + self.compact(); + selabel::selabel_from_dir(self, rootfs)?; + Ok(()) + } +} diff --git a/crates/composefs-boot/src/lib.rs b/crates/composefs-boot/src/lib.rs index 46706273..b8f111f0 100644 --- a/crates/composefs-boot/src/lib.rs +++ b/crates/composefs-boot/src/lib.rs @@ -4,124 +4,30 @@ //! scenarios by extracting boot resources, applying SELinux labels, and preparing //! bootloader entries. It supports both Boot Loader Specification (Type 1) entries //! and Unified Kernel Images (Type 2) for UEFI boot. +//! +//! The `composefs-integration` feature is enabled by default. Disable default features +//! to use the Android boot image and UKI parsers without the composefs integration dependencies. #![forbid(unsafe_code)] #![deny(missing_debug_implementations)] pub mod android_boot; +#[cfg(feature = "composefs-integration")] pub mod bootloader; +#[cfg(feature = "composefs-integration")] pub mod cmdline; pub mod os_release; +#[cfg(feature = "composefs-integration")] pub mod selabel; pub mod uki; +#[cfg(feature = "composefs-integration")] pub mod write_boot; #[cfg(doc)] pub mod design; -use std::ffi::OsStr; - -use anyhow::Result; -use rustix::fd::AsFd; - -use composefs::{fsverity::FsVerityHashValue, repository::Repository, tree::FileSystem}; - -use crate::bootloader::{BootEntry, get_boot_resources}; - -/// These directories are required to exist in images. -/// They may have content in the container, but we don't -/// want to expose them in the final merged root. -/// -/// # /boot -/// -/// This is how sealed UKIs are handled; the UKI in /boot has the composefs -/// digest, so we can't include it in the rendered image. -/// -/// # /sysroot -/// -/// See https://github.com/containers/composefs-rs/issues/164 -/// Basically there is only content here in ostree-container cases, -/// and us traversing there for SELinux labeling will cause problems. -/// The ostree-container code special cases it in a different way, but -/// here we can just ignore it. -const REQUIRED_TOPLEVEL_TO_EMPTY_DIRS: &[&str] = &["boot", "sysroot"]; - -/// Empty the required top-level directories and set their mtime to match /usr. -fn empty_toplevel_dirs(fs: &mut FileSystem) -> Result<()> { - let usr = fs.root.get_directory(OsStr::new("usr"))?; - let usr_mtime_stat_sec = usr.stat.st_mtim_sec; - let usr_mtime_stat_nsec = usr.stat.st_mtim_nsec; - - for d in REQUIRED_TOPLEVEL_TO_EMPTY_DIRS { - let d = fs.root.get_directory_mut(d.as_ref())?; - d.stat.st_mtim_sec = usr_mtime_stat_sec; - d.stat.st_mtim_nsec = usr_mtime_stat_nsec; - d.clear(); - } - - Ok(()) -} - -/// Trait for transforming filesystem images for boot scenarios. -/// -/// This trait provides functionality to prepare composefs filesystem images for booting by -/// extracting boot resources and applying necessary transformations like SELinux labeling. -pub trait BootOps { - /// Transforms a filesystem image for boot by extracting boot entries and applying SELinux labels. - /// - /// This method extracts boot resources from the filesystem, empties required top-level - /// directories (/boot, /sysroot), and applies SELinux security contexts. - /// - /// # Arguments - /// - /// * `repo` - The composefs repository containing filesystem objects - /// - /// # Returns - /// - /// A vector of boot entries extracted from the filesystem (Type 1 BLS entries, Type 2 UKIs, etc.) - fn transform_for_boot( - &mut self, - repo: &Repository, - ) -> Result>>; - - /// Apply boot filesystem transformations using an on-disk directory for file content. - /// - /// This applies the same filesystem transformations as [`BootOps::transform_for_boot`] - /// (emptying /boot and /sysroot, SELinux relabeling) but reads SELinux policy files - /// directly from the on-disk filesystem via a directory fd rather than from the - /// composefs repository. - /// - /// This does not extract boot entries (Type 1 BLS entries, UKIs, etc.) since those - /// are only needed for writing to the boot partition, not for computing the composefs - /// digest. - /// - /// # Arguments - /// - /// * `rootfs` - A directory fd pointing to the root of the on-disk filesystem - fn transform_for_boot_from_dir(&mut self, rootfs: impl AsFd) -> Result<()>; -} - -impl BootOps for FileSystem { - fn transform_for_boot( - &mut self, - repo: &Repository, - ) -> Result>> { - let boot_entries = get_boot_resources(self, repo)?; - empty_toplevel_dirs(self)?; - // Compact the leaves table after clearing directories, so that leaves - // which were only referenced by /boot or /sysroot are removed and - // don't appear as orphans when the filesystem is validated. - self.compact(); - selabel::selabel(self, repo)?; - - Ok(boot_entries) - } +#[cfg(feature = "composefs-integration")] +mod integration; - fn transform_for_boot_from_dir(&mut self, rootfs: impl AsFd) -> Result<()> { - empty_toplevel_dirs(self)?; - // Same as above: compact to remove leaves orphaned by clearing dirs. - self.compact(); - selabel::selabel_from_dir(self, rootfs)?; - Ok(()) - } -} +#[cfg(feature = "composefs-integration")] +pub use integration::BootOps; diff --git a/crates/composefs-boot/src/uki.rs b/crates/composefs-boot/src/uki.rs index 6f31f272..f9fc61c4 100644 --- a/crates/composefs-boot/src/uki.rs +++ b/crates/composefs-boot/src/uki.rs @@ -341,6 +341,7 @@ pub(crate) mod test { ) } + #[cfg(feature = "composefs-integration")] pub(crate) fn uki_with_linux_initrd() -> Vec { let linux = b"linux"; let initrd = b"initrd";