Skip to content

Commit 5d6dd67

Browse files
jeckersbcgwalters
authored andcommitted
feat: Add bootc container ukify command
Add a new subcommand that builds a Unified Kernel Image (UKI) by computing the necessary arguments from a container image and invoking ukify. This simplifies the sealed image build workflow by having bootc internally compute: - The composefs digest (via existing compute-composefs-digest logic) - Kernel arguments from /usr/lib/bootc/kargs.d/*.toml files - Paths to kernel, initrd, and os-release Any additional arguments are passed through to ukify unchanged, allowing full control over signing, output paths, and other ukify options. The seal-uki script is updated to use this new command instead of manually computing these values and invoking ukify directly. Also adds kargs.d configuration files for the sealed UKI workflow: - 10-rootfs-rw.toml: Mount root filesystem read-write - 21-console-hvc0.toml: Console configuration for QEMU/virtio Closes: #1955 Assisted-by: OpenCode (Opus 4.5) Signed-off-by: John Eckersberg <jeckersb@redhat.com>
1 parent 4f51a5f commit 5d6dd67

12 files changed

Lines changed: 320 additions & 38 deletions

File tree

contrib/packaging/seal-uki

Lines changed: 9 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,7 @@ shift
1212
secrets=$1
1313
shift
1414

15-
# Compute the composefs digest from the target rootfs
16-
composefs_digest=$(bootc container compute-composefs-digest "${target}")
17-
18-
# Build the kernel command line
19-
# enforcing=0: https://github.com/bootc-dev/bootc/issues/1826
20-
# TODO: pick up kargs from /usr/lib/bootc/kargs.d
21-
cmdline="composefs=${composefs_digest} console=ttyS0,115200n8 console=hvc0 enforcing=0 rw"
22-
23-
# Find the kernel version
15+
# Find the kernel version (needed for output filename)
2416
kver=$(bootc container inspect --rootfs "${target}" --json | jq -r '.kernel.version')
2517
if [ -z "$kver" ] || [ "$kver" = "null" ]; then
2618
echo "Error: No kernel found" >&2
@@ -29,12 +21,14 @@ fi
2921

3022
mkdir -p "${output}"
3123

32-
ukify build \
33-
--linux "${target}/usr/lib/modules/${kver}/vmlinuz" \
34-
--initrd "${target}/usr/lib/modules/${kver}/initramfs.img" \
35-
--uname="${kver}" \
36-
--cmdline "${cmdline}" \
37-
--os-release "@${target}/usr/lib/os-release" \
24+
# Build the UKI using bootc container ukify
25+
# This computes the composefs digest, reads kargs from kargs.d, and invokes ukify
26+
#
27+
# WORKAROUND: SELinux must be permissive for sealed UKI boot
28+
# See https://github.com/bootc-dev/bootc/issues/1826
29+
bootc container ukify --rootfs "${target}" \
30+
--karg enforcing=0 \
31+
-- \
3832
--signtool sbsign \
3933
--secureboot-private-key "${secrets}/secureboot_key" \
4034
--secureboot-certificate "${secrets}/secureboot_cert" \
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Mount the root filesystem read-write
2+
kargs = ["rw"]
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
# https://bugzilla.redhat.com/show_bug.cgi?id=2353887
2-
kargs = ["console=hvc0"]
2+
# console=ttyS0 for QEMU serial, console=hvc0 for virtio/Xen console
3+
kargs = ["console=ttyS0,115200n8", "console=hvc0"]

crates/lib/src/cli.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -392,6 +392,29 @@ pub(crate) enum ContainerOpts {
392392
/// Identifier for image; if not provided, the running image will be used.
393393
image: Option<String>,
394394
},
395+
/// Build a Unified Kernel Image (UKI) using ukify.
396+
///
397+
/// This command computes the necessary arguments from the container image
398+
/// (kernel, initrd, cmdline, os-release) and invokes ukify with them.
399+
/// Any additional arguments after `--` are passed through to ukify unchanged.
400+
///
401+
/// Example:
402+
/// bootc container ukify --rootfs /target -- --output /output/uki.efi
403+
Ukify {
404+
/// Operate on the provided rootfs.
405+
#[clap(long, default_value = "/")]
406+
rootfs: Utf8PathBuf,
407+
408+
/// Additional kernel arguments to append to the cmdline.
409+
/// Can be specified multiple times.
410+
/// This is a temporary workaround and will be removed.
411+
#[clap(long = "karg", hide = true)]
412+
kargs: Vec<String>,
413+
414+
/// Additional arguments to pass to ukify (after `--`).
415+
#[clap(last = true)]
416+
args: Vec<OsString>,
417+
},
395418
}
396419

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

15991622
Ok(())
16001623
}
1624+
ContainerOpts::Ukify {
1625+
rootfs,
1626+
kargs,
1627+
args,
1628+
} => crate::ukify::build_ukify(&rootfs, &kargs, &args),
16011629
},
16021630
Opt::Completion { shell } => {
16031631
use clap_complete::aot::generate;

crates/lib/src/install.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1587,7 +1587,7 @@ async fn prepare_install(
15871587

15881588
let composefs_required = if let Some(root) = target_rootfs.as_ref() {
15891589
crate::kernel::find_kernel(root)?
1590-
.map(|k| k.unified)
1590+
.map(|k| k.kernel.unified)
15911591
.unwrap_or(false)
15921592
} else {
15931593
false

crates/lib/src/kernel.rs

Lines changed: 67 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use std::path::Path;
88

99
use anyhow::Result;
10+
use camino::Utf8PathBuf;
1011
use cap_std_ext::cap_std::fs::Dir;
1112
use cap_std_ext::dirext::CapStdExtDirExt;
1213
use serde::Serialize;
@@ -25,35 +26,67 @@ pub(crate) struct Kernel {
2526
pub(crate) unified: bool,
2627
}
2728

29+
/// Internal-only kernel wrapper with extra information (paths to
30+
/// vmlinuz, initramfs) that are useful but we don't want to leak out
31+
/// via serialization to inspection.
32+
///
33+
/// `Kernel` implements `From<KernelInternal>` so we can just `.into()`
34+
/// to get the "public" form where needed.
35+
pub(crate) struct KernelInternal {
36+
pub(crate) kernel: Kernel,
37+
/// Path to vmlinuz for traditional kernels.
38+
/// This is `None` for UKI images.
39+
pub(crate) vmlinuz: Option<Utf8PathBuf>,
40+
/// Path to initramfs.img for traditional kernels.
41+
/// This is `None` for UKI images.
42+
pub(crate) initramfs: Option<Utf8PathBuf>,
43+
}
44+
45+
impl From<KernelInternal> for Kernel {
46+
fn from(kernel_internal: KernelInternal) -> Self {
47+
kernel_internal.kernel
48+
}
49+
}
50+
2851
/// Find the kernel in a container image root directory.
2952
///
3053
/// This function first attempts to find a UKI in `/boot/EFI/Linux/*.efi`.
3154
/// If that doesn't exist, it falls back to looking for a traditional kernel
3255
/// layout with `/usr/lib/modules/<version>/vmlinuz`.
3356
///
3457
/// Returns `None` if no kernel is found.
35-
pub(crate) fn find_kernel(root: &Dir) -> Result<Option<Kernel>> {
58+
pub(crate) fn find_kernel(root: &Dir) -> Result<Option<KernelInternal>> {
3659
// First, try to find a UKI
3760
if let Some(uki_filename) = find_uki_filename(root)? {
3861
let version = uki_filename
3962
.strip_suffix(".efi")
4063
.unwrap_or(&uki_filename)
4164
.to_owned();
42-
return Ok(Some(Kernel {
43-
version,
44-
unified: true,
65+
return Ok(Some(KernelInternal {
66+
kernel: Kernel {
67+
version,
68+
unified: true,
69+
},
70+
vmlinuz: None,
71+
initramfs: None,
4572
}));
4673
}
4774

4875
// Fall back to checking for a traditional kernel via ostree_ext
49-
if let Some(kernel_dir) = ostree_ext::bootabletree::find_kernel_dir_fs(root)? {
50-
let version = kernel_dir
76+
if let Some(modules_dir) = ostree_ext::bootabletree::find_kernel_dir_fs(root)? {
77+
let version = modules_dir
5178
.file_name()
52-
.ok_or_else(|| anyhow::anyhow!("kernel dir should have a file name: {kernel_dir}"))?
79+
.ok_or_else(|| anyhow::anyhow!("kernel dir should have a file name: {modules_dir}"))?
5380
.to_owned();
54-
return Ok(Some(Kernel {
55-
version,
56-
unified: false,
81+
let vmlinuz = modules_dir.join("vmlinuz");
82+
let initramfs = modules_dir.join("initramfs.img");
83+
return Ok(Some(KernelInternal {
84+
kernel: Kernel {
85+
version,
86+
unified: false,
87+
},
88+
vmlinuz: Some(vmlinuz),
89+
initramfs: Some(initramfs),
5790
}));
5891
}
5992

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

98132
#[test]
@@ -111,9 +145,21 @@ mod tests {
111145
b"fake kernel",
112146
)?;
113147

114-
let kernel = find_kernel(&tempdir)?.expect("should find kernel");
115-
assert_eq!(kernel.version, "6.12.0-100.fc41.x86_64");
116-
assert!(!kernel.unified);
148+
let kernel_internal = find_kernel(&tempdir)?.expect("should find kernel");
149+
assert_eq!(kernel_internal.kernel.version, "6.12.0-100.fc41.x86_64");
150+
assert!(!kernel_internal.kernel.unified);
151+
assert_eq!(
152+
kernel_internal.vmlinuz.as_deref(),
153+
Some(Utf8Path::new(
154+
"usr/lib/modules/6.12.0-100.fc41.x86_64/vmlinuz"
155+
))
156+
);
157+
assert_eq!(
158+
kernel_internal.initramfs.as_deref(),
159+
Some(Utf8Path::new(
160+
"usr/lib/modules/6.12.0-100.fc41.x86_64/initramfs.img"
161+
))
162+
);
117163
Ok(())
118164
}
119165

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

126-
let kernel = find_kernel(&tempdir)?.expect("should find kernel");
127-
assert_eq!(kernel.version, "fedora-6.12.0");
128-
assert!(kernel.unified);
172+
let kernel_internal = find_kernel(&tempdir)?.expect("should find kernel");
173+
assert_eq!(kernel_internal.kernel.version, "fedora-6.12.0");
174+
assert!(kernel_internal.kernel.unified);
175+
assert!(kernel_internal.vmlinuz.is_none());
176+
assert!(kernel_internal.initramfs.is_none());
129177
Ok(())
130178
}
131179

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

144-
let kernel = find_kernel(&tempdir)?.expect("should find kernel");
192+
let kernel_internal = find_kernel(&tempdir)?.expect("should find kernel");
145193
// UKI should take precedence
146-
assert_eq!(kernel.version, "fedora-6.12.0");
147-
assert!(kernel.unified);
194+
assert_eq!(kernel_internal.kernel.version, "fedora-6.12.0");
195+
assert!(kernel_internal.kernel.unified);
148196
Ok(())
149197
}
150198

crates/lib/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ pub mod spec;
9393
mod status;
9494
mod store;
9595
mod task;
96+
mod ukify;
9697
mod utils;
9798

9899
#[cfg(feature = "docgen")]

crates/lib/src/status.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -863,7 +863,7 @@ pub(crate) fn container_inspect(
863863
)?;
864864
let kargs = crate::bootc_kargs::get_kargs_in_root(&root, std::env::consts::ARCH)?;
865865
let kargs: Vec<String> = kargs.iter_str().map(|s| s.to_owned()).collect();
866-
let kernel = crate::kernel::find_kernel(&root)?;
866+
let kernel = crate::kernel::find_kernel(&root)?.map(Into::into);
867867
let inspect = crate::spec::ContainerInspect { kargs, kernel };
868868

869869
// Determine output format: explicit --format wins, then --json, then default to human-readable

0 commit comments

Comments
 (0)