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
4 changes: 4 additions & 0 deletions contrib/packaging/finalize-uki
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ mkdir -p /boot/EFI/Linux
target=/boot/EFI/Linux/${kver}.efi
cp "${uki_src}/${kver}.efi" "${target}"

if [[ -f "${uki_src}/${kver}.dump" ]]; then
cp "${uki_src}/${kver}.dump" /boot
fi

# NOTE: We used to create a symlink from /usr/lib/modules/${kver}/${kver}.efi to the UKI
# for tooling compatibility. However, composefs-boot's find_uki_components() doesn't
# handle symlinks correctly and fails with "is not a regular file". The UKI is already
Expand Down
9 changes: 8 additions & 1 deletion contrib/packaging/seal-uki
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
set -xeuo pipefail

missing_verity=()
dumpfile_args=()

while [ ! -z "${1:-}" ]; do
case "$1" in
Expand Down Expand Up @@ -38,6 +39,12 @@ while [ ! -z "${1:-}" ]; do
shift
;;

"--write-dumpfile-to")
dumpfile_args=(--write-dumpfile-to "$2")
shift
shift
;;

# Path to the directory containing kernel and initramfs
"--kernel-dir")
kernel_dir="$2"
Expand Down Expand Up @@ -85,4 +92,4 @@ containerukifyargs=(--rootfs "${target}")

# Build the UKI using bootc container ukify
# This computes the composefs digest, reads kargs from kargs.d, and invokes ukify
bootc container ukify "${containerukifyargs[@]}" "${kernel_params[@]}" "${missing_verity[@]}" -- "${ukifyargs[@]}"
bootc container ukify "${containerukifyargs[@]}" "${kernel_params[@]}" "${missing_verity[@]}" "${dumpfile_args[@]}" -- "${ukifyargs[@]}"
105 changes: 92 additions & 13 deletions crates/lib/src/bootc_composefs/boot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
//! 1. **Primary**: New/upgraded deployment (default boot target)
//! 2. **Secondary**: Currently booted deployment (rollback option)

use std::ffi::OsStr;
use std::fs::create_dir_all;
use std::io::{Read, Seek, SeekFrom, Write};
use std::path::Path;
Expand Down Expand Up @@ -89,6 +90,7 @@ use composefs_ctl::composefs_boot;
use composefs_ctl::composefs_oci;
use fn_error_context::context;
use linux_kernel_cmdline::utf8::{Cmdline, Parameter};
use ostree_ext::composefs::dumpfile;
use rustix::{mount::MountFlags, path::Arg};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
Expand Down Expand Up @@ -134,6 +136,14 @@ const AUTH_EXT: &str = "auth";
/// This is relative to the ESP
pub(crate) const BOOTC_UKI_DIR: &str = "EFI/Linux/bootc";

#[derive(thiserror::Error, Debug)]
#[error("The UKI has the wrong composefs= parameter (is '{actual}', should be '{expected}')")]
pub(crate) struct UKIDigestMismatch {
pub actual: String,
pub expected: String,
pub uki_name: Option<String>,
}

pub(crate) enum BootSetupType<'a> {
/// For initial setup, i.e. install to-disk
Setup((&'a RootSetup, &'a State, &'a PostFetchState)),
Expand Down Expand Up @@ -491,7 +501,7 @@ struct BLSEntryPath {
#[context("Setting up BLS boot")]
pub(crate) fn setup_composefs_bls_boot(
setup_type: BootSetupType,
repo: crate::store::ComposefsRepository,
repo: &crate::store::ComposefsRepository,
id: &Sha512HashValue,
entry: &ComposefsBootEntry<Sha512HashValue>,
mounted_erofs: &Dir,
Expand Down Expand Up @@ -845,10 +855,15 @@ fn write_pe_to_esp(
_ => { /* no-op */ }
}

let file_name = file_path.file_name();

if *composefs_cmdline != *uki_id {
anyhow::bail!(
"The UKI has the wrong composefs= parameter (is '{composefs_cmdline:?}', should be {uki_id:?})"
);
return Err(UKIDigestMismatch {
actual: composefs_cmdline.to_hex(),
expected: uki_id.to_hex(),
uki_name: file_name.map(|x| x.to_string()),
}
.into());
}

uki_reader.seek(SeekFrom::Start(0))?;
Expand Down Expand Up @@ -1083,7 +1098,7 @@ fn write_systemd_uki_config(
#[context("Setting up UKI boot")]
pub(crate) fn setup_composefs_uki_boot(
setup_type: BootSetupType,
repo: crate::store::ComposefsRepository,
repo: &crate::store::ComposefsRepository,
id: &Sha512HashValue,
entries: Vec<ComposefsBootEntry<Sha512HashValue>>,
) -> Result<String> {
Expand Down Expand Up @@ -1450,7 +1465,6 @@ pub(crate) async fn setup_composefs_boot(

let boot_type = BootType::from(entry);

// Unwrap Arc to pass owned repo to boot setup functions.
let repo = Arc::try_unwrap(repo).map_err(|_| {
anyhow::anyhow!(
"BUG: Arc<Repository> still has other references after boot image generation"
Expand All @@ -1460,17 +1474,82 @@ pub(crate) async fn setup_composefs_boot(
let boot_digest = match boot_type {
BootType::Bls => setup_composefs_bls_boot(
BootSetupType::Setup((&root_setup, &state, &postfetch)),
repo,
&repo,
&id,
entry,
mounted_root.dir(),
)?,
BootType::Uki => setup_composefs_uki_boot(
BootSetupType::Setup((&root_setup, &state, &postfetch)),
repo,
&id,
entries,
)?,
BootType::Uki => {
let uki_setup_result = setup_composefs_uki_boot(
BootSetupType::Setup((&root_setup, &state, &postfetch)),
&repo,
&id,
entries,
);

match uki_setup_result {
Ok(boot_digest) => boot_digest,
Err(e) => match e.downcast::<UKIDigestMismatch>() {
Ok(mismatch) => {
// We expect dumpfile in /boot as that's the only directory that gets
// masked
let boot_dir = fs.root.get_directory(OsStr::new("boot"))?;

// We expect the dumpfile to be named the same as the UKI
// Ex. UKI - 6.19.14-108.fc42.x86_64.efi
// Dumpfile - 6.19.14-108.fc42.x86_64.dump
let dumpfile_name = mismatch
.uki_name
.as_ref()
.and_then(|x| x.strip_suffix(EFI_EXT).map(|x| format!("{x}.dump")));

let Some(dumpfile_name) = &dumpfile_name else {
tracing::warn!("Dumpfile not found for diff");

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a normal situation, not a warning case.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right. I'll change it to info

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

or debug maybe

return Err(mismatch.into());
};

let dumpfile = boot_dir
.get_file_opt(OsStr::new(&dumpfile_name), &fs.leaves)?
.map(|df| read_file(&df, &repo))
.transpose()
.context("Reading dumpfile")?;

let Some(embedded) = dumpfile else {
tracing::warn!("Dumpfile not found for diff");
return Err(mismatch.into());
};

let tempdir = tempfile::tempdir()?;
let path = tempdir.path();
let tempdir = Dir::open_ambient_dir(path, ambient_authority())?;

// TODO: This can use the `dump_files` API once we have
// https://github.com/composefs/composefs-rs/pull/359
let mut original = tempdir.create("original")?;
original.write_all(&embedded)?;

let mut tmpfile = tempdir.create("current")?;
dumpfile::write_dumpfile(&mut tmpfile, &fs).context("Writing dumpfile")?;

let mut cmd = std::process::Command::new("diff");

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't seem to accept files as params, only strings, ranges etc. Reading the entire diff into memory is okay-ish I guess as it's usually around < 20M

let out = cmd
.arg("--color=auto")
.arg(format!("{}/original", path.display()))
.arg(format!("{}/current", path.display()))
.status();

// Intentionally not short-circuiting here as the real error is digest
// mismtach
if let Err(e) = out {
tracing::warn!("diffing dumpfiles failed with Err: {e:?}");
};

return Err(mismatch.into());
}
Err(e) => Err(e)?,
},
}
}
};

write_composefs_state(
Expand Down
4 changes: 2 additions & 2 deletions crates/lib/src/bootc_composefs/update.rs
Original file line number Diff line number Diff line change
Expand Up @@ -307,15 +307,15 @@ pub(crate) async fn do_upgrade(
let boot_digest = match boot_type {
BootType::Bls => setup_composefs_bls_boot(
BootSetupType::Upgrade((storage, booted_cfs, &host)),
repo,
&repo,
&id,
entry,
&mounted_fs,
)?,

BootType::Uki => setup_composefs_uki_boot(
BootSetupType::Upgrade((storage, booted_cfs, &host)),
repo,
&repo,
&id,
entries,
)?,
Expand Down
7 changes: 7 additions & 0 deletions tmt/plans/integration.fmf
Original file line number Diff line number Diff line change
Expand Up @@ -292,4 +292,11 @@ execute:
how: fmf
test:
- /tmt/tests/tests/test-46-etc-merge-conflict

/plan-48-composefs-uki-dumpfile:
summary: Test composefs garbage collection for UKI
discover:
how: fmf
test:
- /tmt/tests/tests/test-48-composefs-uki-dumpfile
# END GENERATED PLANS
9 changes: 7 additions & 2 deletions tmt/tests/booted/tap.nu
Original file line number Diff line number Diff line change
Expand Up @@ -111,13 +111,18 @@ export def make_uki_containerfile [containerfile: string] {
FROM base as sealed-uki
RUN --network=none --mount=type=tmpfs,target=/run --mount=type=tmpfs,target=/tmp \\
--mount=type=bind,from=base-final,src=/,target=/run/target \\
--mount=type=bind,from=kernel,src=/,target=/run/kernel \\
--mount=type=bind,from=kernel,src=/,target=/run/kernel <<-EOF

kver=$\(bootc container inspect --rootfs /run/kernel --json | jq -r '.kernel.version'\)

/usr/bin/seal-uki \\
--target /run/target \\
--output /out \\
--secrets /run/secrets ($allow_missing_verity) \\
--kernel-dir /run/kernel/boot/$\(bootc container inspect --rootfs /run/kernel --json | jq -r '.kernel.version'\) \\
--kernel-dir /run/kernel/boot/${kver} \\
--write-dumpfile-to /out/${kver}.dump \\
--seal-state ($seal_state)
EOF

FROM base-final

Expand Down
60 changes: 60 additions & 0 deletions tmt/tests/booted/test-composefs-uki-dumpfile.nu
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# number: 48
# tmt:
# summary: Test composefs garbage collection for UKI
# duration: 30m

use std assert
use tap.nu

if not (tap is_composefs) {
exit 0
}

# bootc status
let st = bootc status --json | from json
let booted = $st.status.booted.image

let is_uki = (($st.status.booted.composefs.bootType | str downcase) == "uki")

if not $is_uki {
exit 0
}

def first_boot [] {
bootc image copy-to-storage

mut containerfile = $"
FROM localhost/bootc as base
RUN touch /usr/share/accepted-file
"

$containerfile = (tap make_uki_containerfile $containerfile)

$containerfile += "
RUN touch /usr/share/new-file
"

echo $containerfile | podman build -t localhost/dump-diff . -f -

let result = do { bootc switch --transport containers-storage localhost/dump-diff } | complete

let actual_digest = ./bootc internals cfs oci compute-id $"@(podman images --no-trunc | grep dump-diff | awk '{print $3}')"

assert ($result.exit_code != 0) "bootc switch should fail"

print ($result.stderr)

assert ($result.stderr | str contains "The UKI has the wrong composefs= parameter") $"Expected 'The UKI has the wrong composefs= parameter' in stderr"
assert ($result.stderr | str contains $"should be '($actual_digest)'") $"Expected digest to be ($actual_digest) in stderr"
assert ($result.stderr | str contains "/usr/share/new-file") $"Expected '/usr/share/new-file' in stderr"

tap ok
}

def main [] {
match $env.TMT_REBOOT_COUNT? {
null | "0" => first_boot,
$o => { error make { msg: $"Invalid TMT_REBOOT_COUNT ($o)" } },
}
}

5 changes: 5 additions & 0 deletions tmt/tests/tests.fmf
Original file line number Diff line number Diff line change
Expand Up @@ -183,3 +183,8 @@ check:
summary: Verify etc merge conflicts are caught during upgrade, not finalization
duration: 15m
test: nu booted/test-etc-merge-conflict.nu

/test-48-composefs-uki-dumpfile:
summary: Test composefs garbage collection for UKI
duration: 30m
test: nu booted/test-composefs-uki-dumpfile.nu
Loading