Skip to content
Closed
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
34 changes: 16 additions & 18 deletions crates/lib/src/bootc_composefs/boot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,14 +92,14 @@ use composefs_ctl::composefs;
use composefs_ctl::composefs_boot;
use composefs_ctl::composefs_oci;
use fn_error_context::context;
use linux_kernel_cmdline::utf8::{Cmdline, Parameter, ParameterKey};
use linux_kernel_cmdline::utf8::{Cmdline, ParameterKey};
use ostree_ext::composefs::dumpfile;
use rustix::{mount::MountFlags, path::Arg};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

use crate::bootc_composefs::state::{get_booted_bls, write_composefs_state};
use crate::bootc_composefs::status::build_composefs_karg;
use crate::bootc_composefs::status::build_bls_composefs_kargs;
use crate::bootc_kargs::compute_new_kargs;
use crate::composefs_consts::{TYPE1_BOOT_DIR_PREFIX, TYPE1_ENT_PATH, TYPE1_ENT_PATH_STAGED};
use crate::parsers::bls_config::{BLSConfig, BLSConfigType, EFIKey};
Expand Down Expand Up @@ -720,13 +720,11 @@ struct BLSEntryPath {
config_path: Utf8PathBuf,
}

/// Replace either karg spelling to ensure only the selected EROFS format remains.
fn replace_composefs_karg(cmdline: &mut Cmdline, new_karg: &str) -> Result<()> {
/// Replace both karg spellings with `new_kargs` (see [`build_bls_composefs_kargs`]).
fn replace_composefs_karg(cmdline: &mut Cmdline, new_kargs: &str) {
cmdline.remove(&ParameterKey::from(KARG_V2));
cmdline.remove(&ParameterKey::from(KARG_COMPOSEFS_DIGEST));
let parameter = Parameter::parse(new_karg).context("Parsing composefs kernel parameter")?;
cmdline.add_or_modify(&parameter);
Ok(())
cmdline.extend(&Cmdline::from(new_kargs));
}

/// Sets up and writes BLS entries and binaries (VMLinuz + Initrd) to disk
Expand Down Expand Up @@ -758,7 +756,7 @@ pub(crate) fn setup_composefs_bls_boot(
}

let composefs_cmdline =
build_composefs_karg(id.clone(), format_version, allow_missing_fsverity);
build_bls_composefs_kargs(id.clone(), format_version, allow_missing_fsverity);
cmdline_options.extend(&Cmdline::from(&composefs_cmdline));

// If there's a separate /boot partition, add a systemd.mount-extra
Expand Down Expand Up @@ -807,12 +805,12 @@ pub(crate) fn setup_composefs_bls_boot(

replace_composefs_karg(
&mut cmdline,
&build_composefs_karg(
&build_bls_composefs_kargs(
id.clone(),
format_version,
booted_cfs.cmdline.allow_missing_fsverity,
),
)?;
);

// Locate ESP partition device by walking up to the root disk(s)
let root_dev = bootc_blockdev::list_dev_by_dir(&storage.physical_root)?;
Expand Down Expand Up @@ -2277,17 +2275,17 @@ mod tests {

#[test]
fn test_replace_composefs_karg() {
let hex = "f".repeat(128);
let mut cmdline =
Cmdline::from("root=UUID=abc composefs=old composefs.digest=v1-sha512-12:stale");
Cmdline::from("root=UUID=abc composefs=old composefs.digest=v1-sha512-12:stale rw");
replace_composefs_karg(
&mut cmdline,
"composefs.digest=v1-sha512-12:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
)
.unwrap();
let rendered = cmdline.to_string();
assert!(!rendered.contains("composefs=old"));
assert!(!rendered.contains(":stale"));
assert!(rendered.contains("root=UUID=abc"));
&format!("composefs.digest=v1-sha512-12:{hex} composefs={hex}"),
);
assert_eq!(
cmdline.to_string(),
format!("root=UUID=abc rw composefs.digest=v1-sha512-12:{hex} composefs={hex}")
);
}

#[test]
Expand Down
63 changes: 63 additions & 0 deletions crates/lib/src/bootc_composefs/status.rs
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,37 @@ pub(crate) fn build_composefs_karg(
.to_cmdline_arg()
}

/// Render the composefs kargs for a Type 1 BLS entry, which (unlike a UKI)
/// may be written by a newer bootc than the one in the target's initramfs.
///
/// For a V1 digest this is `composefs.digest=v1-...` followed by the legacy
/// `composefs=` key carrying the *same* digest: the initramfs of bootc 1.16
/// and earlier only starts `bootc-root-setup.service` when a karg named
/// `composefs` is present, and before 1.16.3 it only parses `composefs=`.
/// The digest must stay the deployment's own, since the initramfs also uses
/// it to find `state/deploy/<digest>`. Current parsers (the initramfs and
/// [`ComposefsCmdline::find_in_cmdline`]) prefer `composefs.digest=`, and
/// bootc 1.16.4 through 1.16.14 already wrote V1 digests under `composefs=`.
///
/// The legacy karg can be dropped once bootc no longer needs to write boot
/// entries for images whose initramfs predates `composefs.digest=` support in
/// `bootc-root-setup.service` (i.e. 1.16.x and older are out of support).
pub(crate) fn build_bls_composefs_kargs(
digest: Sha512HashValue,
format_version: FormatVersion,
allow_missing_fsverity: bool,
) -> String {
let karg = build_composefs_karg(digest.clone(), format_version, allow_missing_fsverity);
match format_version {
FormatVersion::V2 => karg,
FormatVersion::V0 | FormatVersion::V1 => {
let legacy =
BootComposefsCmdline::new_v2(digest, allow_missing_fsverity).to_cmdline_arg();
format!("{karg} {legacy}")
}
}
}

impl std::fmt::Display for ComposefsCmdline {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let allow_missing_fsverity = if self.allow_missing_fsverity { "?" } else { "" };
Expand Down Expand Up @@ -1198,6 +1229,38 @@ mod tests {
Ok(())
}

#[test]
fn test_build_bls_composefs_kargs() -> Result<()> {
let hex = "ab".repeat(64);
let digest = || Sha512HashValue::from_hex(&hex).unwrap();

let cases = [
(
FormatVersion::V1,
false,
format!("composefs.digest=v1-sha512-12:{hex} composefs={hex}"),
),
(
FormatVersion::V1,
true,
format!("composefs.digest=?v1-sha512-12:{hex} composefs=?{hex}"),
),
(FormatVersion::V2, false, format!("composefs={hex}")),
(FormatVersion::V2, true, format!("composefs=?{hex}")),
];
for (format_version, insecure, expected) in cases {
let kargs = build_bls_composefs_kargs(digest(), format_version, insecure);
assert_eq!(kargs, expected);
// Current parsers still resolve the deployment from either form.
let parsed = ComposefsCmdline::find_in_cmdline(&Cmdline::from(kargs))?
.expect("composefs argument should be present");
assert_eq!(parsed.digest.as_ref(), hex);
assert_eq!(parsed.allow_missing_fsverity, insecure);
}

Ok(())
}

#[test]
fn classify_bootloader_cases() {
struct Case {
Expand Down
23 changes: 21 additions & 2 deletions docs/src/experimental-composefs.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,23 @@ So bootc accepts a bare `composefs=` digest that matches either a V1 or a V2
image, and only enforces the format for the explicit
`composefs.digest=v1-…`/`composefs.digest=v2-…` form.

### Boot entries (BLS)

For a Type 1 (BLS) boot entry, bootc writes the kernel arguments itself, and
the image it writes them for may carry an initramfs from an older bootc, for
example after `bootc switch` to an image that still ships bootc 1.16. That
initramfs only runs its root setup when an argument named `composefs` is
present, and before 1.16.3 it only reads `composefs=`. So for a V1 deployment
bootc writes `composefs.digest=v1-sha512-12:<digest>` followed by
`composefs=<digest>` with the **same V1 digest**, which also names the
deployment's state directory.

This differs from UKIs, where the bare `composefs=` argument carries the V2
digest, and from composefs-rs's documentation, which defines bare
`composefs=` as an alias for V2. Current initramfs versions try both
arguments in order; for a BLS entry both name the same image, and
`composefs.digest=` comes first.

### Upgrading from bootc 1.16

When you update bootc in an image, **regenerate the initramfs before
Expand All @@ -80,8 +97,10 @@ moves the system to V1. bootc 1.16.4 and later already understand

The 1.16.0 path is covered by the `test-49-composefs-1-16-bridge` TMT test for
both sealed and `--allow-missing-verity` UKIs, including rollback and garbage
collection. Upgrades from other releases, and from BLS (non-UKI) composefs
installs, are not yet tested.
collection. Upgrades from other releases are not yet tested. For BLS
(non-UKI) installs, `test-56-composefs-legacy-initramfs` covers a current
bootc switching to an image whose initramfs has the bootc 1.16 unit
condition; upgrades by a 1.16 client are not yet tested.

## Storage and repository structure

Expand Down
8 changes: 8 additions & 0 deletions tmt/plans/integration.fmf
Original file line number Diff line number Diff line change
Expand Up @@ -331,4 +331,12 @@ execute:
enabled: true
extra-try_bind_storage: true
extra-skip_if_ostree: true

/plan-56-composefs-legacy-initramfs:
summary: Boot a BLS entry for an image whose initramfs predates composefs.digest=
discover:
how: fmf
test:
- /tmt/tests/tests/test-56-composefs-legacy-initramfs
extra-skip_if_ostree: true
# END GENERATED PLANS
77 changes: 77 additions & 0 deletions tmt/tests/booted/test-composefs-legacy-initramfs.nu
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# number: 56
# tmt:
# summary: Boot a BLS entry for an image whose initramfs predates composefs.digest=
# duration: 30m
# extra:
# skip_if_ostree: true
#
# In bootc 1.16 initramfs images, bootc-root-setup.service only starts when
# a karg named `composefs` is present. When a newer bootc writes the BLS
# entry for such an image (e.g. `bootc switch` to an image still shipping
# bootc 1.16), it must emit the legacy `composefs=` karg next to
# `composefs.digest=`, or the target drops to the emergency shell.
# Simulate that initramfs by rebuilding it with the 1.16 unit condition.
use std assert
use tap.nu
use bootc_testlib.nu

const legacy_image = "localhost/bootc-legacy-initramfs"
const unit = "usr/lib/systemd/system/bootc-root-setup.service"
const legacy_condition = "ConditionKernelCommandLine=composefs"

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

def first_boot [] {
tap begin "switch to an image whose initramfs only knows composefs="

bootc image copy-to-storage

r#'
FROM localhost/bootc
RUN sed -i -e '/^ConditionKernelCommandLine=/d' \
-e '/^\[Unit\]/a ConditionKernelCommandLine=composefs' \
/usr/lib/systemd/system/bootc-root-setup.service
RUN set -x; kver=$(cd /usr/lib/modules && echo *); \
dracut -vf --add bootc /usr/lib/modules/$kver/initramfs.img $kver
'# | podman build -t $legacy_image . -f -

# Check the simulation took: the initramfs must carry only the 1.16 condition.
let conditions = podman run --rm $legacy_image bash -c $"lsinitrd -f ($unit) /usr/lib/modules/*/initramfs.img"
| lines | where { |l| $l starts-with "ConditionKernelCommandLine=" }
assert equal $conditions [$legacy_condition]

bootc switch --transport containers-storage $legacy_image

tmt-reboot
}

def second_boot [] {
assert equal $booted.image.image $legacy_image

let cmdline = bootc_testlib parse_cmdline
let verity = $st.status.booted.composefs.verity
assert ($cmdline | any { |k| $k starts-with "composefs.digest=" and ($k | str ends-with $verity) })
assert ($cmdline | any { |k| $k == $"composefs=($verity)" or $k == $"composefs=?($verity)" })

let root = findmnt --json --mountpoint / --output SOURCE | from json
let root_source = ($root.filesystems | first | get source | into string)
assert ($root_source | str contains $verity) $"root not mounted from composefs: ($root_source)"

tap ok
}

def main [] {
# A UKI carries its own kargs, built with the image's bootc; only BLS
# entries are written by the (possibly newer) host bootc.
if ($st.status.booted.composefs.bootType | str downcase) == "uki" {
print "# Skipping: UKI boot"
return
}

match $env.TMT_REBOOT_COUNT? {
null | "0" => first_boot,
"1" => second_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 @@ -206,3 +206,8 @@ check:
- when: composefs_bridge == true
enabled: true
test: nu booted/test-49-composefs-1-16-bridge.nu

/test-56-composefs-legacy-initramfs:
summary: Boot a BLS entry for an image whose initramfs predates composefs.digest=
duration: 30m
test: nu booted/test-composefs-legacy-initramfs.nu