Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 10 additions & 11 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ clap_mangen = { version = "0.3.0" }
# pulls), which drags in rustls-webpki/untrusted/webpki-root-certs whose
# licenses aren't in our cargo-deny allow list. Re-enable once that's sorted.
# See: https://github.com/bootc-dev/bootc/pull/2295
composefs-ctl = { git = "https://github.com/composefs/composefs-rs", rev = "0fbc853325ff92dbb842e715611bed7ddf9fe169", default-features = false, features = [
composefs-ctl = { git = "https://github.com/composefs/composefs-rs", rev = "76923702bc50a0d625b4adf64fb2ee40dd1304b9", default-features = false, features = [
"pre-6.15",
"pre-6.16",
"oci",
Expand Down
102 changes: 92 additions & 10 deletions crates/lib/src/bootc_composefs/boot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
//! 2. **Secondary**: Currently booted deployment (rollback option)

use std::cell::Cell;
use std::collections::HashMap;
use std::fs::create_dir_all;
use std::io::{Read, Seek, SeekFrom, Write};
use std::path::{Path, PathBuf};
Expand Down Expand Up @@ -91,6 +92,10 @@ use composefs_ctl::composefs_oci;
use fn_error_context::context;
use linux_kernel_cmdline::utf8::{Cmdline, Parameter};
use ostree_ext::composefs::dumpfile;
use ostree_ext::composefs::erofs::format::FormatVersion;
use ostree_ext::composefs::erofs::writer::{ValidatedFileSystem, mkfs_erofs_versioned};
use ostree_ext::composefs::generic_tree::{Directory, FileSystem, Inode, Leaf, LeafId};
use ostree_ext::composefs::repository::Repository;
use rustix::{mount::MountFlags, path::Arg};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
Expand Down Expand Up @@ -353,6 +358,11 @@ pub(crate) fn get_uki_addon_file_name(depl_verity: &str) -> String {
format!("{UKI_NAME_PREFIX}{depl_verity}{EFI_ADDON_FILE_EXT}")
}

/// Returns the name of the saved /boot directory EROFS
pub(crate) fn get_boot_erofs_name(depl_verity: &str) -> String {
format!("{depl_verity}.boot")
}

/// Compute SHA256Sum of VMlinuz + Initrd
///
/// # Arguments
Expand Down Expand Up @@ -966,7 +976,7 @@ fn write_pe_to_esp(

let pe_name = match pe_type {
PEType::Uki => &get_uki_name(&uki_id.to_hex()),
PEType::UkiAddon => file_path
PEType::UkiAddon | PEType::GlobalUkiAddon => file_path
.components()
.last()
.ok_or_else(|| anyhow::anyhow!("Failed to get UKI Addon file name"))?
Expand Down Expand Up @@ -1450,6 +1460,66 @@ fn get_secureboot_keys(fs: &Dir, p: &str) -> Result<Option<SecurebootKeys>> {
}));
}

fn copy_inode(
inode: &Inode<RegularFile<Sha512HashValue>>,
src_leaves: &[Leaf<RegularFile<Sha512HashValue>>],
dst: &mut FileSystem<RegularFile<Sha512HashValue>>,
id_map: &mut HashMap<LeafId, LeafId>,
) -> Inode<RegularFile<Sha512HashValue>> {
match inode {
Inode::Directory(dir) => {
let mut new_dir = Directory::new(dir.stat.clone());
for (name, child) in dir.entries() {
let new_child = copy_inode(child, src_leaves, dst, id_map);
new_dir.insert(name, new_child);
}
Inode::Directory(Box::new(new_dir))
}
Inode::Leaf(id, _) => {
let new_id = *id_map.entry(*id).or_insert_with(|| {
let leaf = &src_leaves[id.0];
dst.push_leaf(leaf.stat.clone(), leaf.content.clone())
});
Inode::leaf(new_id)
}
}
}

/// Accepts a filesystem, find the `/boot` directory, clones the `/boot` directory
/// into a new filesystem, validates the filesystem then returns the raw bytes for
/// the EROFS image corresponding to the newly created filesystem
#[context("Saving EROFS for /boot")]
pub(crate) fn save_boot_dir_erofs(
repo: &Repository<Sha512HashValue>,
fs: &FileSystem<RegularFile<Sha512HashValue>>,
depl_id: &Sha512HashValue,
) -> Result<()> {
for (name, inode) in fs.root.entries() {
if name.as_str() != Ok("boot") {
continue;
}

let Inode::Directory(..) = inode else {
anyhow::bail!("/boot is a file");
};

let mut new_fs: FileSystem<RegularFile<Sha512HashValue>> =
FileSystem::new(fs.root.stat.clone());
let mut id_map = HashMap::new();

let new_inode = copy_inode(inode, &fs.leaves, &mut new_fs, &mut id_map);
new_fs.root.insert(name, new_inode);

let validated_boot = ValidatedFileSystem::new(new_fs)?;
let erofs_bytes = mkfs_erofs_versioned(&validated_boot, FormatVersion::V1);
// NOTE: <depl_id>.boot is actually saved in refs
// The actual EROFS is still in composefs/images
repo.write_image(Some(&get_boot_erofs_name(&depl_id.to_hex())), &erofs_bytes)?;
}

Ok(())
}

#[context("Setting up composefs boot")]
pub(crate) async fn setup_composefs_boot(
root_setup: &RootSetup,
Expand All @@ -1475,21 +1545,28 @@ pub(crate) async fn setup_composefs_boot(
let repo = Arc::new(repo);

// Generate the bootable EROFS image (idempotent).
let id = composefs_oci::generate_boot_image(
let (id, mut fs) = composefs_oci::generate_boot_image_get_fs(
&repo,
&pull_result.manifest_digest,
&Default::default(),
)
.context("Generating bootable EROFS image")?;

// Reconstruct the OCI filesystem to discover boot entries (kernel, initramfs, etc.).
let fs = composefs_oci::image::create_filesystem(
&*repo,
&pull_result.config_digest,
None,
&Default::default(),
)
.context("Creating composefs filesystem for boot entry discovery")?;
if fs.is_none() {
// Reconstruct the OCI filesystem to discover boot entries (kernel, initramfs, etc.).
fs = Some(
composefs_oci::image::create_filesystem(
&*repo,
&pull_result.config_digest,
Some(&pull_result.config_verity),
&Default::default(),
)
.context("Creating composefs filesystem for boot entry discovery")?,
);
}

let fs = fs.ok_or_else(|| anyhow::anyhow!("Failed to get composefs filesystem"))?;

let entries =
get_boot_resources(&fs, &*repo).context("Extracting boot entries from OCI image")?;

Expand Down Expand Up @@ -1598,6 +1675,11 @@ pub(crate) async fn setup_composefs_boot(
)
})?;

// Save /boot for UKI images
if boot_type == BootType::Uki {
save_boot_dir_erofs(&repo, &fs, &id)?;
}

let boot_digest = match boot_type {
BootType::Bls => setup_composefs_bls_boot(
BootSetupType::Setup((&root_setup, &state, &postfetch)),
Expand Down
90 changes: 87 additions & 3 deletions crates/lib/src/bootc_composefs/gc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,12 @@ use composefs_boot::bootloader::EFI_EXT;
use composefs_ctl::composefs;
use composefs_ctl::composefs_boot;
use composefs_ctl::composefs_oci;
use ostree_ext::composefs::ImageNotFound;
use rustix::fs::AtFlags;
use rustix::fs::readlinkat;
use rustix::fs::{statat, unlinkat};

use crate::bootc_composefs::boot::get_boot_erofs_name;
use crate::{
bootc_composefs::{
boot::{BOOTC_UKI_DIR, BootType, get_type1_dir_name, get_uki_addon_dir_name, get_uki_name},
Expand Down Expand Up @@ -346,7 +351,7 @@ pub(crate) async fn composefs_gc(

// Collect the set of manifest digests referenced by live deployments,
// and track EROFS image verities as fallback additional_roots for
// deployments that predate the manifestimage link.
// deployments that predate the manifest -> image link.
let mut live_manifest_digests: Vec<composefs_oci::OciDigest> = Vec::new();
let mut additional_roots = Vec::new();
// Container image names for containers-storage pruning.
Expand All @@ -366,7 +371,7 @@ pub(crate) async fn composefs_gc(
}

// Keep the EROFS image as an additional root until all deployments
// have manifestimage refs. Once a deployment is pulled with the
// have manifest -> image refs. Once a deployment is pulled with the
// new code, its EROFS image is reachable from the manifest and
// this entry becomes redundant (but harmless).
additional_roots.push(verity.clone());
Expand Down Expand Up @@ -492,12 +497,91 @@ pub(crate) async fn composefs_gc(
// first action. Callers must ensure no other `Repository` handle on that
// same underlying file is still alive when this runs, or it will deadlock
// (see update.rs's do_upgrade() for an example of getting this wrong).
let gc_result = if gc_opts.dry_run {
let mut gc_result = if gc_opts.dry_run {
booted_cfs.repo.gc_dry_run(&additional_roots)?
} else {
booted_cfs.repo.gc(&additional_roots)?
};

// Now GC the UKI/UKI Addons from `.boot` EROFS if we have them
// These won't be GC'd by the above `repo.gc` as the EROFS have
// `/boot` masked
for verity in all_orphans {
let boot_img_name = get_boot_erofs_name(verity);

let objects = match booted_cfs.repo.objects_for_image(&boot_img_name) {
Ok(objects) => objects,
Err(e) => match e.downcast::<ImageNotFound>() {
Ok(_) => continue,
Err(e) => Err(e)?,
},
};

let objects_dir = booted_cfs
.repo
.objects_dir()
.context("Opening objects dir")?;

for object_sha in objects {
let path = object_sha.to_object_pathname();

tracing::debug!(
"{}: objects/{path}",
if gc_opts.dry_run {
"would remove"
} else {
"removing"
},
);

if gc_opts.dry_run {
continue;
}

// Get file size before removing
if let Ok(stat) = statat(&objects_dir, &path, AtFlags::empty()) {
gc_result.objects_bytes += stat.st_size as u64;
}

gc_result.objects_removed += 1;

unlinkat(&objects_dir, &path, AtFlags::empty())
.with_context(|| format!("Unlinking object {path}"))?;
}

let boot_img_path = format!("images/refs/{boot_img_name}");

// Delete the image now
let repo_fd = booted_cfs.repo.repo_fd();
let linked_img =
readlinkat(repo_fd, &boot_img_path, &[]).context("Reading boot image link")?;
let linked_img = linked_img.to_str().context("Converting link to str")?;

tracing::debug!("Boot image: {boot_img_name} -> {linked_img:?}");

// The link is realtive, we need the image name itself
let Some(linked_img) = linked_img.rsplit("/").next() else {
anyhow::bail!("{linked_img} is not a proper relative symlink to an image");
};

if gc_opts.dry_run {
continue;
}

let img_path = format!("images/{linked_img}");

if let Ok(stat) = statat(booted_cfs.repo.repo_fd(), &img_path, AtFlags::empty()) {
gc_result.objects_bytes += stat.st_size as u64;
}

unlinkat(booted_cfs.repo.repo_fd(), &img_path, AtFlags::empty())
.context("Removing boot image")?;
gc_result.images_pruned += 1;

unlinkat(booted_cfs.repo.repo_fd(), &boot_img_path, AtFlags::empty())
.context("Removing boot image symlink")?;
}

Ok(gc_result)
}

Expand Down
Loading
Loading