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
24 changes: 9 additions & 15 deletions contrib/packaging/seal-uki
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,7 @@ shift
secrets=$1
shift

# Compute the composefs digest from the target rootfs
composefs_digest=$(bootc container compute-composefs-digest "${target}")

# Build the kernel command line
# enforcing=0: https://github.com/bootc-dev/bootc/issues/1826
# TODO: pick up kargs from /usr/lib/bootc/kargs.d
cmdline="composefs=${composefs_digest} console=ttyS0,115200n8 console=hvc0 enforcing=0 rw"

# Find the kernel version
# Find the kernel version (needed for output filename)
kver=$(bootc container inspect --rootfs "${target}" --json | jq -r '.kernel.version')
if [ -z "$kver" ] || [ "$kver" = "null" ]; then
echo "Error: No kernel found" >&2
Expand All @@ -29,12 +21,14 @@ fi

mkdir -p "${output}"

ukify build \
--linux "${target}/usr/lib/modules/${kver}/vmlinuz" \
--initrd "${target}/usr/lib/modules/${kver}/initramfs.img" \
--uname="${kver}" \
--cmdline "${cmdline}" \
--os-release "@${target}/usr/lib/os-release" \
# Build the UKI using bootc container ukify
# This computes the composefs digest, reads kargs from kargs.d, and invokes ukify
#
# WORKAROUND: SELinux must be permissive for sealed UKI boot
# See https://github.com/bootc-dev/bootc/issues/1826
bootc container ukify --rootfs "${target}" \
Copy link
Collaborator

Choose a reason for hiding this comment

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

This definitely looks nicer!

--karg enforcing=0 \
-- \
--signtool sbsign \
--secureboot-private-key "${secrets}/secureboot_key" \
--secureboot-certificate "${secrets}/secureboot_cert" \
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Mount the root filesystem read-write
kargs = ["rw"]
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
# https://bugzilla.redhat.com/show_bug.cgi?id=2353887
kargs = ["console=hvc0"]
# console=ttyS0 for QEMU serial, console=hvc0 for virtio/Xen console
kargs = ["console=ttyS0,115200n8", "console=hvc0"]
28 changes: 28 additions & 0 deletions crates/lib/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,29 @@ pub(crate) enum ContainerOpts {
/// Identifier for image; if not provided, the running image will be used.
image: Option<String>,
},
/// Build a Unified Kernel Image (UKI) using ukify.
///
/// This command computes the necessary arguments from the container image
/// (kernel, initrd, cmdline, os-release) and invokes ukify with them.
/// Any additional arguments after `--` are passed through to ukify unchanged.
///
/// Example:
/// bootc container ukify --rootfs /target -- --output /output/uki.efi
Ukify {
/// Operate on the provided rootfs.
#[clap(long, default_value = "/")]
rootfs: Utf8PathBuf,

/// Additional kernel arguments to append to the cmdline.
/// Can be specified multiple times.
/// This is a temporary workaround and will be removed.
#[clap(long = "karg", hide = true)]
kargs: Vec<String>,

/// Additional arguments to pass to ukify (after `--`).
#[clap(last = true)]
args: Vec<OsString>,
},
}

/// Subcommands which operate on images.
Expand Down Expand Up @@ -1598,6 +1621,11 @@ async fn run_from_opt(opt: Opt) -> Result<()> {

Ok(())
}
ContainerOpts::Ukify {
rootfs,
kargs,
args,
} => crate::ukify::build_ukify(&rootfs, &kargs, &args),
},
Opt::Completion { shell } => {
use clap_complete::aot::generate;
Expand Down
2 changes: 1 addition & 1 deletion crates/lib/src/install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1587,7 +1587,7 @@ async fn prepare_install(

let composefs_required = if let Some(root) = target_rootfs.as_ref() {
crate::kernel::find_kernel(root)?
.map(|k| k.unified)
.map(|k| k.kernel.unified)
.unwrap_or(false)
} else {
false
Expand Down
86 changes: 67 additions & 19 deletions crates/lib/src/kernel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use std::path::Path;

use anyhow::Result;
use camino::Utf8PathBuf;
use cap_std_ext::cap_std::fs::Dir;
use cap_std_ext::dirext::CapStdExtDirExt;
use serde::Serialize;
Expand All @@ -25,35 +26,67 @@ pub(crate) struct Kernel {
pub(crate) unified: bool,
}

/// Internal-only kernel wrapper with extra information (paths to
/// vmlinuz, initramfs) that are useful but we don't want to leak out
/// via serialization to inspection.
///
/// `Kernel` implements `From<KernelInternal>` so we can just `.into()`
/// to get the "public" form where needed.
pub(crate) struct KernelInternal {
pub(crate) kernel: Kernel,
/// Path to vmlinuz for traditional kernels.
/// This is `None` for UKI images.
pub(crate) vmlinuz: Option<Utf8PathBuf>,
/// Path to initramfs.img for traditional kernels.
/// This is `None` for UKI images.
pub(crate) initramfs: Option<Utf8PathBuf>,
}

impl From<KernelInternal> for Kernel {
fn from(kernel_internal: KernelInternal) -> Self {
kernel_internal.kernel
}
}

/// Find the kernel in a container image root directory.
///
/// This function first attempts to find a UKI in `/boot/EFI/Linux/*.efi`.
/// If that doesn't exist, it falls back to looking for a traditional kernel
/// layout with `/usr/lib/modules/<version>/vmlinuz`.
///
/// Returns `None` if no kernel is found.
pub(crate) fn find_kernel(root: &Dir) -> Result<Option<Kernel>> {
pub(crate) fn find_kernel(root: &Dir) -> Result<Option<KernelInternal>> {
// First, try to find a UKI
if let Some(uki_filename) = find_uki_filename(root)? {
let version = uki_filename
.strip_suffix(".efi")
.unwrap_or(&uki_filename)
.to_owned();
return Ok(Some(Kernel {
version,
unified: true,
return Ok(Some(KernelInternal {
kernel: Kernel {
version,
unified: true,
},
vmlinuz: None,
initramfs: None,
}));
}

// Fall back to checking for a traditional kernel via ostree_ext
if let Some(kernel_dir) = ostree_ext::bootabletree::find_kernel_dir_fs(root)? {
let version = kernel_dir
if let Some(modules_dir) = ostree_ext::bootabletree::find_kernel_dir_fs(root)? {
let version = modules_dir
.file_name()
.ok_or_else(|| anyhow::anyhow!("kernel dir should have a file name: {kernel_dir}"))?
.ok_or_else(|| anyhow::anyhow!("kernel dir should have a file name: {modules_dir}"))?
.to_owned();
return Ok(Some(Kernel {
version,
unified: false,
let vmlinuz = modules_dir.join("vmlinuz");
let initramfs = modules_dir.join("initramfs.img");
return Ok(Some(KernelInternal {
kernel: Kernel {
version,
unified: false,
},
vmlinuz: Some(vmlinuz),
initramfs: Some(initramfs),
}));
}

Expand Down Expand Up @@ -93,6 +126,7 @@ fn find_uki_filename(root: &Dir) -> Result<Option<String>> {
#[cfg(test)]
mod tests {
use super::*;
use camino::Utf8Path;
use cap_std_ext::{cap_std, cap_tempfile, dirext::CapStdExtDirExt};

#[test]
Expand All @@ -111,9 +145,21 @@ mod tests {
b"fake kernel",
)?;

let kernel = find_kernel(&tempdir)?.expect("should find kernel");
assert_eq!(kernel.version, "6.12.0-100.fc41.x86_64");
assert!(!kernel.unified);
let kernel_internal = find_kernel(&tempdir)?.expect("should find kernel");
assert_eq!(kernel_internal.kernel.version, "6.12.0-100.fc41.x86_64");
assert!(!kernel_internal.kernel.unified);
assert_eq!(
kernel_internal.vmlinuz.as_deref(),
Some(Utf8Path::new(
"usr/lib/modules/6.12.0-100.fc41.x86_64/vmlinuz"
))
);
assert_eq!(
kernel_internal.initramfs.as_deref(),
Some(Utf8Path::new(
"usr/lib/modules/6.12.0-100.fc41.x86_64/initramfs.img"
))
);
Ok(())
}

Expand All @@ -123,9 +169,11 @@ mod tests {
tempdir.create_dir_all("boot/EFI/Linux")?;
tempdir.atomic_write("boot/EFI/Linux/fedora-6.12.0.efi", b"fake uki")?;

let kernel = find_kernel(&tempdir)?.expect("should find kernel");
assert_eq!(kernel.version, "fedora-6.12.0");
assert!(kernel.unified);
let kernel_internal = find_kernel(&tempdir)?.expect("should find kernel");
assert_eq!(kernel_internal.kernel.version, "fedora-6.12.0");
assert!(kernel_internal.kernel.unified);
assert!(kernel_internal.vmlinuz.is_none());
assert!(kernel_internal.initramfs.is_none());
Ok(())
}

Expand All @@ -141,10 +189,10 @@ mod tests {
tempdir.create_dir_all("boot/EFI/Linux")?;
tempdir.atomic_write("boot/EFI/Linux/fedora-6.12.0.efi", b"fake uki")?;

let kernel = find_kernel(&tempdir)?.expect("should find kernel");
let kernel_internal = find_kernel(&tempdir)?.expect("should find kernel");
// UKI should take precedence
assert_eq!(kernel.version, "fedora-6.12.0");
assert!(kernel.unified);
assert_eq!(kernel_internal.kernel.version, "fedora-6.12.0");
assert!(kernel_internal.kernel.unified);
Ok(())
}

Expand Down
1 change: 1 addition & 0 deletions crates/lib/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ pub mod spec;
mod status;
mod store;
mod task;
mod ukify;
mod utils;

#[cfg(feature = "docgen")]
Expand Down
2 changes: 1 addition & 1 deletion crates/lib/src/status.rs
Original file line number Diff line number Diff line change
Expand Up @@ -863,7 +863,7 @@ pub(crate) fn container_inspect(
)?;
let kargs = crate::bootc_kargs::get_kargs_in_root(&root, std::env::consts::ARCH)?;
let kargs: Vec<String> = kargs.iter_str().map(|s| s.to_owned()).collect();
let kernel = crate::kernel::find_kernel(&root)?;
let kernel = crate::kernel::find_kernel(&root)?.map(Into::into);
let inspect = crate::spec::ContainerInspect { kargs, kernel };

// Determine output format: explicit --format wins, then --json, then default to human-readable
Expand Down
Loading
Loading