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: 19 additions & 2 deletions Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ test-composefs bootloader filesystem boot_type seal_state *ARGS:
--seal-state={{seal_state}} \
--boot-type={{boot_type}} \
{{ARGS}} \
$(if [ "{{boot_type}}" = "uki" ]; then echo "readonly"; else echo "integration"; fi)
$(if [ "{{boot_type}}" = "uki" ]; then echo "readonly image-upgrade-reboot"; else echo "integration"; fi)

# Run upgrade test: boot VM from published base image (with tmt deps added),
# upgrade to locally-built image, reboot, then run readonly tests to verify.
Expand Down Expand Up @@ -362,7 +362,24 @@ _keygen:
./hack/generate-secureboot-keys

_build-upgrade-image:
cat tmt/tests/Dockerfile.upgrade | podman build -t {{upgrade_img}} --from={{base_img}} -
#!/bin/bash
set -xeuo pipefail
# Secrets are always available (test-tmt depends on build which runs _keygen).
# Extra capabilities are only needed for UKI builds (composefs + fuse).
extra_args=()
if [ "{{boot_type}}" = "uki" ]; then
extra_args+=(--cap-add=all --security-opt=label=type:container_runtime_t --device /dev/fuse)
fi
podman build \
--build-arg "boot_type={{boot_type}}" \
--build-arg "seal_state={{seal_state}}" \
--build-arg "filesystem={{filesystem}}" \
--secret=id=secureboot_key,src=target/test-secureboot/db.key \
--secret=id=secureboot_cert,src=target/test-secureboot/db.crt \
"${extra_args[@]}" \
-t {{upgrade_img}} \
-f tmt/tests/Dockerfile.upgrade \
.

# Build the upgrade source image: base image + tmt dependencies (rsync, nu, cloud-init)
_build-upgrade-source-image:
Expand Down
11 changes: 11 additions & 0 deletions contrib/packaging/install-rpm-and-setup
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,16 @@ env DRACUT_NO_XATTR=1 dracut --add bootc -vf /usr/lib/modules/$kver/initramfs.im
# tests to know we're doing upstream CI.
touch /usr/lib/.bootc-dev-stamp

# Fedora 43+ ships a GRUB with the BLI module, so enable DPS
# auto-discovery for root. This must run after our RPM is installed
# since older bootc doesn't recognize the discoverable-partitions key.
. /usr/lib/os-release
if [ "${ID}" = "fedora" ] && [ "${VERSION_ID}" -ge 43 ] 2>/dev/null; then
cat > /usr/lib/bootc/install/20-discoverable-partitions.toml <<'EOF'
[install]
discoverable-partitions = true
EOF
fi

# Workaround for https://github.com/bootc-dev/bootc/issues/1546
rm -rf /root/buildinfo /var/roothome/buildinfo
8 changes: 1 addition & 7 deletions crates/lib/src/bootc_composefs/boot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ use std::io::Write;
use std::path::Path;

use anyhow::{Context, Result, anyhow, bail};
use bootc_kernel_cmdline::utf8::{Cmdline, Parameter, ParameterKey};
use bootc_kernel_cmdline::utf8::{Cmdline, Parameter};
use bootc_mount::tempmount::TempMount;
use camino::{Utf8Path, Utf8PathBuf};
use cap_std_ext::{
Expand Down Expand Up @@ -582,12 +582,6 @@ pub(crate) fn setup_composefs_bls_boot(
}
};

// Remove "root=" from kernel cmdline as systemd-auto-gpt-generator should use DPS
// UUID
if bootloader == Bootloader::Systemd {
cmdline_refs.remove(&ParameterKey::from("root"));
}

let is_upgrade = matches!(setup_type, BootSetupType::Upgrade(..));

let current_root = if is_upgrade {
Expand Down
41 changes: 38 additions & 3 deletions crates/lib/src/install/baseline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,31 @@ use bootc_kernel_cmdline::utf8::Cmdline;
#[cfg(feature = "install-to-disk")]
use bootc_mount::is_mounted_in_pid1_mountns;

/// Check whether DPS auto-discovery is enabled. When `true`,
/// `root=UUID=` is omitted and `systemd-gpt-auto-generator` discovers
/// the root partition via its DPS type GUID instead.
///
/// Defaults to `true` for systemd-boot (which always implements BLI).
/// For GRUB the default is `false` because we cannot know at install
/// time whether the GRUB build includes the `bli` module — the module
/// is baked into the signed EFI binary with no external manifest.
/// Distros shipping a BLI-capable GRUB should set
/// `discoverable-partitions = true` in their install config.
#[cfg(feature = "install-to-disk")]
fn use_discoverable_partitions(state: &State) -> bool {
// Explicit config takes priority
if let Some(ref config) = state.install_config {
if let Some(v) = config.discoverable_partitions {
return v;
}
}
// systemd-boot always supports BLI
matches!(
state.config_opts.bootloader,
Some(crate::spec::Bootloader::Systemd)
)
}

// This ensures we end up under 512 to be small-sized.
pub(crate) const BOOTPN_SIZE_MB: u32 = 510;
pub(crate) const EFIPN_SIZE_MB: u32 = 512;
Expand Down Expand Up @@ -226,10 +251,15 @@ pub(crate) fn install_create_rootfs(
};
let serial = device.serial.as_deref().unwrap_or("<unknown>");
let model = device.model.as_deref().unwrap_or("<unknown>");
let discoverable = use_discoverable_partitions(state);
println!("Block setup: {block_setup}");
println!(" Size: {}", device.size);
println!(" Serial: {serial}");
println!(" Model: {model}");
println!(
" Partitions: {}",
if discoverable { "Discoverable" } else { "UUID" }
);

let root_size = opts
.root_size
Expand Down Expand Up @@ -415,7 +445,6 @@ pub(crate) fn install_create_rootfs(
opts.wipe,
mkfs_options.iter().copied(),
)?;
let rootarg = format!("root=UUID={root_uuid}");
let bootsrc = boot_uuid.as_ref().map(|uuid| format!("UUID={uuid}"));
let bootarg = bootsrc.as_deref().map(|bootsrc| format!("boot={bootsrc}"));
let boot = bootsrc.map(|bootsrc| MountSpec {
Expand All @@ -434,8 +463,14 @@ pub(crate) fn install_create_rootfs(
}
}

// Add root= and rw argument
kargs.extend(&Cmdline::from(format!("{rootarg} {RW_KARG}")));
// When discoverable-partitions is enabled, omit root= so that
// systemd-gpt-auto-generator discovers root by its DPS type GUID.
if discoverable {
kargs.extend(&Cmdline::from(RW_KARG));
} else {
let rootarg = format!("root=UUID={root_uuid}");
kargs.extend(&Cmdline::from(format!("{rootarg} {RW_KARG}")));
}

// Add boot= argument if present
if let Some(bootarg) = bootarg {
Expand Down
39 changes: 39 additions & 0 deletions crates/lib/src/install/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,13 @@ pub(crate) struct InstallConfiguration {
pub(crate) bootupd: Option<Bootupd>,
/// Bootloader to use (grub, systemd, none)
pub(crate) bootloader: Option<Bootloader>,
/// Use the Discoverable Partitions Specification for root partition
/// discovery. When true, the `root=` kernel argument is omitted
/// and `systemd-gpt-auto-generator` discovers root via its DPS
/// type GUID. Requires the bootloader to implement the Boot Loader
/// Interface (systemd-boot always does, GRUB needs the `bli` module).
/// Defaults to false for broad compatibility.
pub(crate) discoverable_partitions: Option<bool>,
}

fn merge_basic<T>(s: &mut Option<T>, o: Option<T>, _env: &EnvProperties) {
Expand Down Expand Up @@ -203,6 +210,11 @@ impl Mergeable for InstallConfiguration {
merge_basic(&mut self.boot_mount_spec, other.boot_mount_spec, env);
self.bootupd.merge(other.bootupd, env);
merge_basic(&mut self.bootloader, other.bootloader, env);
merge_basic(
&mut self.discoverable_partitions,
other.discoverable_partitions,
env,
);
if let Some(other_kargs) = other.kargs {
self.kargs
.get_or_insert_with(Default::default)
Expand Down Expand Up @@ -876,3 +888,30 @@ bootloader = "grub"
install.merge(other, &env);
assert_eq!(install.bootloader, Some(Bootloader::None));
}

#[test]
fn test_parse_discoverable_partitions() {
let c: InstallConfigurationToplevel = toml::from_str(
r##"[install]
discoverable-partitions = true
"##,
)
.unwrap();
assert_eq!(c.install.unwrap().discoverable_partitions, Some(true));

let c: InstallConfigurationToplevel = toml::from_str(
r##"[install]
discoverable-partitions = false
"##,
)
.unwrap();
assert_eq!(c.install.unwrap().discoverable_partitions, Some(false));

let c: InstallConfigurationToplevel = toml::from_str(
r##"[install]
root-fs-type = "xfs"
"##,
)
.unwrap();
assert_eq!(c.install.unwrap().discoverable_partitions, None);
}
13 changes: 13 additions & 0 deletions docs/src/man/bootc-install-config.5.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ The `install` section supports these subfields:
- `boot-mount-spec`: A string specifying the /boot filesystem mount specification.
If not provided and /boot is a separate mount, its UUID will be used.
An empty string signals to omit boot mount kargs entirely.
- `discoverable-partitions`: Boolean. When `true`, root discovery uses the
Discoverable Partitions Specification via `systemd-gpt-auto-generator` and
the `root=` kernel argument is omitted. This requires the bootloader to
implement the Boot Loader Interface (BLI); systemd-boot always does, GRUB
needs the `bli` module (available in newer builds). Defaults to `true`
when using systemd-boot, `false` otherwise.

# filesystem

Expand Down Expand Up @@ -78,6 +84,13 @@ boot-mount-spec = "UUID=abcd-1234"
bls-append-except-default = 'grub_users=""'
```

Enable DPS auto-discovery for root (requires a BLI-capable bootloader):

```toml
[install]
discoverable-partitions = true
```

# SEE ALSO

**bootc(1)**
Expand Down
28 changes: 19 additions & 9 deletions docs/src/man/bootc-install-to-disk.8.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,15 +41,25 @@ use `install to-filesystem` if you need precise control over the partition layou

### Root filesystem discovery

Note that by default when used with "type 1" bootloader setups (i.e. non-UKI)
a kernel argument `root=UUID=<uuid of filesystem>` is injected by default.
This provides compatibility with existing initramfs implementations.

When used with the composefs backend and UKIs, it's recommended that
a bootloader implementing the DPS specification is used and that the root
partition is auto-discovered. In this configuration, `systemd-gpt-auto-generator`
in the initramfs will automatically find and mount the root partition based on
its DPS type GUID, without requiring an explicit `root=` kernel argument.
The root partition can be discovered at boot time in two ways:

- **UUID mode** (default): A kernel argument `root=UUID=<uuid>` is
injected, providing broad compatibility with all initramfs
implementations and bootloaders.

- **DPS auto-discovery**: The `root=` kernel argument is omitted
entirely. `systemd-gpt-auto-generator` in the initramfs discovers
the root partition by its DPS type GUID. This enables transparent
block-layer changes (such as adding LUKS encryption) without
updating kernel arguments. DPS auto-discovery requires the
bootloader to implement the Boot Loader Interface (BLI).
systemd-boot always supports this; GRUB supports it only with
newer builds that include the `bli` module.

When using systemd-boot, DPS auto-discovery is enabled by default.
For GRUB, container base images that ship a BLI-capable build should
set `discoverable-partitions = true` in their install configuration
(see **bootc-install-config**(5)).

# OPTIONS

Expand Down
65 changes: 62 additions & 3 deletions tmt/tests/Dockerfile.upgrade
Original file line number Diff line number Diff line change
@@ -1,3 +1,62 @@
# Just creates a file as a new layer for a synthetic upgrade test
FROM localhost/bootc
RUN touch --reference=/usr/bin/bash /usr/share/testing-bootc-upgrade-apply
# Creates a synthetic upgrade image for testing.
# For non-UKI builds, this just adds a marker file on top of localhost/bootc.
# For UKI builds (boot_type=uki), the image is re-sealed with a new composefs
# digest and (optionally signed) UKI.
#
# Build secrets required (for sealed builds):
# secureboot_key, secureboot_cert
ARG boot_type=bls
ARG seal_state=unsealed
ARG filesystem=ext4

# Capture contrib/packaging scripts for use in later stages
FROM scratch AS packaging
COPY contrib/packaging /

# Create the upgrade content (a simple marker file).
# For UKI builds, we also remove the existing UKI so that seal-uki can
# regenerate it with the correct composefs digest for this derived image.
FROM localhost/bootc AS upgrade-base
ARG boot_type
RUN touch --reference=/usr/bin/bash /usr/share/testing-bootc-upgrade-apply && \
if test "${boot_type}" = "uki"; then rm -rf /boot/EFI/Linux/*.efi; fi

# Tools for sealing (only meaningfully used for UKI builds)
FROM localhost/bootc AS tools
RUN --mount=type=tmpfs,target=/run --mount=type=tmpfs,target=/tmp \
--mount=type=bind,from=packaging,src=/,target=/run/packaging \
/run/packaging/initialize-sealing-tools

# Generate a sealed UKI for the upgrade image.
# bootc is already installed in localhost/bootc (our tools base); the
# container ukify command it provides is needed for seal-uki.
FROM tools AS sealed-upgrade-uki
ARG boot_type seal_state filesystem
RUN --network=none --mount=type=tmpfs,target=/run --mount=type=tmpfs,target=/tmp \
--mount=type=secret,id=secureboot_key \
--mount=type=secret,id=secureboot_cert \
--mount=type=bind,from=packaging,src=/,target=/run/packaging \
--mount=type=bind,from=upgrade-base,src=/,target=/run/target <<EORUN
set -xeuo pipefail

allow_missing_verity=false
if [ "${filesystem}" = "xfs" ]; then
allow_missing_verity=true
fi

if test "${boot_type}" = "uki"; then
/run/packaging/seal-uki /run/target /out /run/secrets "${allow_missing_verity}" "${seal_state}"
fi
EORUN

# Final stage: the upgrade image, optionally with a re-sealed UKI
FROM upgrade-base
ARG boot_type
RUN --network=none --mount=type=tmpfs,target=/run --mount=type=tmpfs,target=/tmp \
--mount=type=bind,from=packaging,src=/,target=/run/packaging \
--mount=type=bind,from=sealed-upgrade-uki,src=/,target=/run/sealed-uki <<EORUN
set -xeuo pipefail
if test "${boot_type}" = "uki"; then
/run/packaging/finalize-uki /run/sealed-uki/out
fi
EORUN
30 changes: 30 additions & 0 deletions tmt/tests/booted/readonly/052-test-bli-detection.nu
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
use std assert
use tap.nu

tap begin "DPS root discovery when partition-uuids is false"

# Parse os-release
let os = open /usr/lib/os-release
| lines
| filter {|l| $l != "" and not ($l | str starts-with "#") }
| parse "{key}={value}"
| reduce -f {} {|it, acc| $acc | upsert $it.key ($it.value | str trim -c '"') }

let os_id = ($os.ID? | default "unknown")
let version_id = ($os.VERSION_ID? | default "0" | into int)

# We inject this in our builds, but hopefully C10S gets this too at some point
if not ($os_id == "fedora" and $version_id >= 43) {
print $"# skip: only applies to Fedora 43+ \(found ($os_id) ($version_id)\)"
tap ok
exit 0
}

print $"Running on ($os_id) ($version_id), checking DPS root discovery"

let cmdline = (open /proc/cmdline)
let has_root_karg = ($cmdline | str contains "root=")

assert (not $has_root_karg) "Fedora 43+ should use DPS auto-discovery (no root= in cmdline)"

tap ok
Loading