Skip to content

Add support for multiple composefs= digests - #2248

Merged
cgwalters merged 11 commits into
bootc-dev:mainfrom
cgwalters:adapt-composefs-rs-v1-erofs-2
Sep 23, 2026
Merged

cgwalters merged 11 commits into
bootc-dev:mainfrom
cgwalters:adapt-composefs-rs-v1-erofs-2

Conversation

@cgwalters

Copy link
Copy Markdown
Collaborator

This adapts bootc to build on top of the work in composefs/composefs-rs#297

A toplevel goal here is supporting both the v1 and v2 EROFS formats, which means we'll work with RHEL9 era systems.

Right now bootc container ukify still generates composefs= i.e. v2, but I'd like to change that to do both - it's a pretty cheap thing (the main cost is generating the fsverity digests).

@github-actions github-actions Bot added the area/install Issues related to `bootc install` label Jun 12, 2026
@bootc-bot
bootc-bot Bot requested a review from jeckersb June 12, 2026 10:42
@cgwalters cgwalters mentioned this pull request Jun 22, 2026
@jmarrero
jmarrero self-requested a review June 22, 2026 13:28
@cgwalters

Copy link
Copy Markdown
Collaborator Author

OK I think the chain here should be:

@cgwalters
cgwalters force-pushed the adapt-composefs-rs-v1-erofs-2 branch from 3b90847 to f92013b Compare June 22, 2026 17:30
@cgwalters
cgwalters marked this pull request as ready for review June 22, 2026 18:51
@bootc-bot
bootc-bot Bot requested a review from henrywang June 22, 2026 18:51
@cgwalters cgwalters added the ci/tier-1 Run CI for tier-1 OS (centos-10) only label Jun 22, 2026
@cgwalters
cgwalters force-pushed the adapt-composefs-rs-v1-erofs-2 branch from f92013b to 69757fc Compare June 22, 2026 21:56
jmarrero
jmarrero previously approved these changes Jun 28, 2026

@jmarrero jmarrero left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

lgtm

just needs a rebase I think.

The only nit is that

            config.erofs_formats = composefs_ctl::composefs::erofs::format::FormatConfig {
                default: composefs_ctl::composefs::erofs::format::FormatVersion::V1,
                extra: [composefs_ctl::composefs::erofs::format::FormatVersion::V2].into(),
            };

Is duplicated 3 times here. So if we add a new version in the future we need to care about 3 places. Maybe it should be a helper function?

@Johan-Liebert1 Johan-Liebert1 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Lots of .clone() that I believe shouldn't be needed, I might be wrong though. Supporting both V1 and V2 are great, but it is a bit messy, not sure if there's a better way to do this. Also, some inconsistencies here and there (esp in comments) regarding whether V1 is the default or V2

Comment thread contrib/packaging/seal-uki Outdated
seal_state=$1
shift
# EROFS format version to pass to bootc container ukify (optional, default: v2)
erofs_version=${1:-v2}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Kind of conflicts with composefs/composefs-rs#330. We'd probably want to have the same defaults everywhere

Comment thread crates/lib/src/bootc_composefs/boot.rs Outdated
os_id: Option<String>,
boot_digest: String,
/// The composefs image digest parsed from (and validated against) the UKI's
/// own cmdline. This is the authoritative deployment key for UKI boots:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is for every boot right, not just UKIs?

Comment thread crates/lib/src/bootc_composefs/boot.rs Outdated
let composefs_info = BootComposefsCmdline::<Sha512HashValue>::from_cmdline(&cmdline)
.context("Parsing composefs=")?
.ok_or_else(|| anyhow::anyhow!("No composefs= or composefs.digest.v1= karg found in UKI cmdline"))?;
let composefs_cmdline = composefs_info.digest().clone();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This name is a bit confusing. afaiu this is only the digest and not the entire cmdline?

Comment thread tmt/tests/Dockerfile.upgrade Outdated

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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We also need this in tmt/tests/booted/test-install-to-filesystem-var-mount.sh

// (see setup_composefs_boot for the full rationale). Provisional value for
// BLS (where bootc writes the karg from this same id); overridden for UKI by
// the digest the UKI cmdline actually carries.
let provisional_deploy_id = boot_id_v2.clone().unwrap_or_else(|| id.clone());

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

id here is confusing especially with both boot_id_v1/v2 defined. I believe it's the erofs digest corresponding to the erofs version that the repo is currently using?

// Authoritative collision check against the final deploy key. For UKI this
// may differ from the provisional checked above (the UKI may carry a
// non-default digest), so this is the load-bearing guarantee.
ensure_no_deploy_collision(host, &deploy_id)?;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

why do we need to do this again?

Comment thread crates/lib/src/bootc_composefs/boot.rs Outdated
// setup-root opens `state/deploy/<this>` using that same karg, so we must
// key the deployment off exactly this value -- whether the UKI was sealed
// with the V2 (default) or V1 EROFS digest.
let deploy_id = uki_info.composefs_cmdline.clone();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

we shouldn't need to clone this?

@cgwalters cgwalters added this to the 1.17 milestone Jul 6, 2026
@cgwalters
cgwalters force-pushed the adapt-composefs-rs-v1-erofs-2 branch 3 times, most recently from 6ec1bf4 to cbff4b5 Compare July 9, 2026 15:20
@cgwalters

Copy link
Copy Markdown
Collaborator Author

In the general case we may need to add support for "older bootc version" which includes not just the v2 digest but the xattr filtering logic too? See composefs/composefs-rs#337

@cgwalters
cgwalters force-pushed the adapt-composefs-rs-v1-erofs-2 branch from cbff4b5 to 8e7417f Compare August 31, 2026 16:16
@github-actions github-actions Bot added the area/documentation Updates to the documentation label Aug 31, 2026
@cgwalters
cgwalters force-pushed the adapt-composefs-rs-v1-erofs-2 branch from 4bf145c to 45e92e4 Compare September 1, 2026 11:59
@cgwalters

Copy link
Copy Markdown
Collaborator Author

OK, this one wants #2290 to land first which fixes our composefs mounts on c9s.

@cgwalters

Copy link
Copy Markdown
Collaborator Author

OK, this one wants #2290 to land first which fixes our composefs mounts on c9s.

OK! Finally that landed. I rebased this, but it's still somewhat raw, especially the last commit.

@cgwalters cgwalters added the ci/merge Run full CI suite (all OSes) — equivalent to merge queue label Sep 16, 2026
@Johan-Liebert1 Johan-Liebert1 self-assigned this Sep 16, 2026
Comment thread crates/lib/src/bootc_composefs/boot.rs Outdated
Comment on lines +1205 to +1207
repo_requires_fsverity: bool,
missing_fsverity_allowed: bool,
uki_allows_missing_fsverity: bool,

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.

Too many bool here...I think we may need an enum

Comment thread crates/lib/src/bootc_composefs/boot.rs Outdated
Ok(())
}

/// Validate every primary UKI before any bootloader or ESP operation. The

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.

Hmmm I'm not sure, I think we need to be robust to corrupted state and allow recovery. Bailing early hurts that.

This seems more like a bootc internals fsck style thing.

Comment thread crates/lib/src/bootc_composefs/boot.rs Outdated
.file_path
.file_name()
.ok_or_else(|| anyhow!("Could not get UKI file name"))?
.to_string_lossy()

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.

No. We should require UTF-8.

Comment thread crates/lib/src/ukify.rs Outdated
const COMPOSEFS_DIGEST_V1_FEATURE: &str = "/usr/lib/bootc/initramfs-features/composefs-digest-v1";
const COMPOSEFS_DIGEST_V1_FEATURE_CONTENT: &[u8] = b"composefs-digest-v1 state-v1\n";

/// Query `lsinitrd` without unpacking or executing any initramfs contents.

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.

No, this is awful. I don't want ukify to be parsing the initramfs.

Among other things I don't want to hard depend on dracut.

I think we should just default to injecting both EROFS kargs right?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

If we just want the cmdline, we have a function in composefs-rs for that. get_uki_cmdline_buffered

@cgwalters
cgwalters force-pushed the adapt-composefs-rs-v1-erofs-2 branch from c30d873 to d986021 Compare September 22, 2026 13:48
@cgwalters-bot

cgwalters-bot commented Sep 22, 2026 •

Copy link
Copy Markdown
Contributor

I tested upgrading a sealed composefs RHEL 10.2 system (stock bootc 1.16.4, UKI + systemd-boot + Secure Boot, the rhel-bootc-examples sealing example) to this PR's c10s RPMs, with the new bootc in the compose so the initramfs is regenerated. The upgrade chain works: 1.16.4 bootc switch → PR image, then PR bootc upgrade → second PR image, then bootc rollback, all with Secure Boot, verity=require, /etc and /var state intact, and composefs-gc --assert-no-op clean.

But there's a problem with bare composefs=: every bootc from v1.16.4 through v1.16.13 seals a V1 EROFS digest under composefs=, because composefs-rs a29622c (pinned by v1.16.4) switched the default format to V1 while ukify kept emitting the legacy karg. Using the actual RHEL 10.2 binary against the same rootfs:

bootc 1.16.4 (RHEL 10.2)                 compute-composefs-digest -> 6c60fb07…2af2678f
this PR  --erofs-version=v1              compute-composefs-digest -> 6c60fb07…2af2678f
this PR  --erofs-version=v2              compute-composefs-digest -> 53bab3fa…2b3ca8

(The on-disk image header also has composefs_version=1.) Two consequences:

  1. The documented 1.16 bridge path doesn't happen for ≥1.16.4. The old client already parses composefs.digest= first and stages V1 directly; the V2 fallback is never used and the V2 image isn't created. test-49-composefs-1-16-bridge pins 1.16.0, which predates the V1 default, so it can't catch the 1.16.4+ behavior.

  2. The PR client refuses to deploy images sealed by 1.16.4–1.16.13. On a host running this PR, switching back to the original 1.16.4-sealed image:

    # bootc switch <1.16.4-sealed image>
    error: Switching: Composefs Switching: Performing Upgrade Operation: Pulling composefs repository: Verifying composefs digest against UKI: UKI composefs candidate format does not match recovered EROFS format
    

    parse_uki_composefs_candidates (crates/lib/src/bootc_composefs/boot.rs:1036) maps composefs= to V2, so ids_for_candidate (:1100) looks only in the V2 set; ensure_correct_composefs_digest (:1420) then finds the matching V1 image, and resolve_boot_image_match (:1482) rejects it on format. validate_uki_candidates (:1126) has the same assumption. The log also reads confusingly: "Freshly computed composefs digest (sha512:6c60fb…) doesn't match the digest embedded in UKI (sha512:6c60fb…)".

    This is deploy-time only: the initramfs mounts images/<hex> by digest regardless of the V1/V2 label, so existing 1.16.x deployments and rollbacks keep booting. But it blocks downgrades and any mixed-version setup, e.g. the common pattern where a separate tools stage with the distro's bootc runs ukify. bootc install of such an image likely goes through the same path (not tested).

Suggested fix: treat bare composefs=<hex> as format-ambiguous (accept a match in either the V1 or V2 expected set) and only enforce the format for the explicit composefs.digest=vN-… form, plus a test case for a composefs=<v1 digest>-only UKI. The docs' bridge section probably also needs to distinguish 1.16.0–1.16.3 (V2 under composefs=) from 1.16.4+ (V1 under composefs=).

Tested with the packages-centos-10 artifact from run 35735900302 (merge of d986021 into main).

Generated-by: https://github.com/cgwalters/#llms

We want to support upgrades from older bootc.

bootc 1.16.0 stagers only understand the bare `composefs=` argument and
create only V2 images and state. A UKI built with a newly
regenerated initramfs carries the V1 digest followed by the V2 fallback, so
its first boot selects the V2 image and state. A subsequent upgrade by a
current client can then create and select the V1 image.

Image builders must regenerate the initramfs when updating bootc; this change
does not claim compatibility for stale initramfs artifacts. Explicit V2
remains available as a format control. Always emit both digests without
probing initramfs contents.

Preserve strict repository requirements when recovering a non-default boot
image; changing serialization must not relax integrity.

Assisted-by: AI
Signed-off-by: Colin Walters <walters@verbum.org>
Composefs installation must preserve kernel argument policy, validate
the target repository before writing boot artifacts, and inspect
external images from a target-backed staged copy. Keep the source
identity and initial UKI fs-verity policy authoritative across the
entire install path.

A bare `composefs=<hex>` UKI argument does not identify an EROFS
format, even though composefs-rs documents it as V2 shorthand: bootc
1.16.3 sealed a V2 digest there, but 1.16.4 through 1.16.13 seal V1. Accept a
bare digest matching a boot image of either format and only enforce the
format for the self-describing `composefs.digest=vN-...` form; otherwise
switching to any image sealed by those releases fails.

Generated-by: AI
Signed-off-by: Colin Walters <walters@verbum.org>
The compatibility claim depends on regenerated current initramfs
artifacts and on keeping strict and missing-verity fixtures distinct.
Make the opt-in tests assert those boundaries and inspect the installed
target rather than the running installer.

Generated-by: AI
Signed-off-by: Colin Walters <walters@verbum.org>
The previous text read like an internal test log (test plan numbers,
TMT variables, evidence lists) and repeated the same compatibility
caveat several times. Describe what users need instead: the V1/V2
kernel arguments, what the bare composefs= argument has meant across
releases, how to upgrade from 1.16, and what is known not to work yet.
The only tested upgrade path remains bootc 1.16.0.

Generated-by: AI
Signed-off-by: Colin Walters <walters@verbum.org>
The separate-/var install test should exercise the format selected by
the test plan rather than silently forcing V1. Forward the environment
selection while retaining V1 as the standalone default.

Generated-by: AI
Signed-off-by: Colin Walters <walters@verbum.org>
There's a very complex dependency chain between `systemd-tmpfiles-setup.service`,
`systemd-random-seed.service` and `systemd-tpm2-setup.service`
among others. Basically we need `/var/lib` to be created, which
will be done by the tmpfiles setup, but random-seed will normally
do it too, but it can't because Fedora SELinux denies it.

A surprising discovery is that this happens to be papered over
for the OSTree backend by `ostree-remount.service`.

Add a test case that no services fail. However this check
needs to currently skip due to
https://bugzilla.redhat.com/show_bug.cgi?id=2507393.

Assisted-by: AI

Signed-off-by: Colin Walters <walters@verbum.org>
The manifest tracks the v0.20.0 tag, but the stale revision-based lock entry makes locked Cargo operations fail. Regenerate the entry so dependency resolution agrees with the declared source.

Assisted-by: AI
Signed-off-by: Colin Walters <walters@verbum.org>
@cgwalters
cgwalters force-pushed the adapt-composefs-rs-v1-erofs-2 branch from d986021 to 51ae138 Compare September 22, 2026 18:51
@cgwalters
cgwalters enabled auto-merge (rebase) September 23, 2026 10:38
@Johan-Liebert1

Copy link
Copy Markdown
Member

The old client already parses composefs.digest= first and stages V1 directly

how would the older client parse V1 cmdline arg?

@cgwalters

Copy link
Copy Markdown
Collaborator Author

how would the older client parse V1 cmdline arg?

It just parses composefs= which can historically now be v2 or v1 - where did you see that text?

@Johan-Liebert1

Johan-Liebert1 commented Sep 23, 2026 •

Copy link
Copy Markdown
Member

In the copilot review

The documented 1.16 bridge path doesn't happen for ≥1.16.4. The old client already parses composefs.digest= first and stages V1 directly; the V2 fallback is never used and the V2 image isn't created. test-49-composefs-1-16-bridge pins 1.16.0, which predates the V1 default, so it can't catch the 1.16.4+ behavior.

It just parses composefs= which can historically now be v2 or v1

okay, kinda makes sense then

@Johan-Liebert1 Johan-Liebert1 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Looks good to me now. @cgwalters I've disabled auto merge in case you want to change something here

@cgwalters
cgwalters merged commit 41049cc into bootc-dev:main Sep 23, 2026
81 of 97 checks passed
cgwalters-bot added a commit to cgwalters-forge/composefs-rs that referenced this pull request Sep 25, 2026
Release 0.7.0 disabled this job until bootc main caught up with the
composefs-rs API. That has now happened: bootc-dev/bootc#2248 (the
adapt-composefs-rs-v1-erofs-2 branch we were pinned to) is merged, and
bootc main depends on composefs-rs by git tag v0.9.2, so the [patch]
override in bootc/Justfile applies to our workspace version again.

Test against bootc main as before. This also drops the if: never(),
which isn't a valid expression function: the workflow file failed to
parse, so every push to any branch recorded a failed run with no jobs
instead of skipping it.

Generated-by: AI
cgwalters-bot added a commit to cgwalters-forge/composefs-rs that referenced this pull request Sep 25, 2026
Release 0.7.0 disabled this job until bootc main caught up with the
composefs-rs API. That has now happened: bootc-dev/bootc#2248 (the
adapt-composefs-rs-v1-erofs-2 branch we were pinned to) is merged, and
bootc main depends on composefs-rs by git tag v0.9.2, so the [patch]
override in bootc/Justfile applies to our workspace version again.

Test against bootc main as before. This also drops the if: never(),
which isn't a valid expression function: the workflow file failed to
parse, so every push to any branch recorded a failed run with no jobs
instead of skipping it.

Generated-by: AI
cgwalters-bot added a commit to cgwalters-forge/composefs-rs that referenced this pull request Sep 25, 2026
Release 0.7.0 disabled this job until bootc main caught up with the
composefs-rs API. That has now happened: bootc-dev/bootc#2248 (the
adapt-composefs-rs-v1-erofs-2 branch we were pinned to) is merged, and
bootc main depends on composefs-rs by git tag v0.9.2, so the [patch]
override in bootc/Justfile applies to our workspace version again.

Test against bootc main as before. This also drops the if: never(),
which isn't a valid expression function: the workflow file failed to
parse, so every push to any branch recorded a failed run with no jobs
instead of skipping it.

Generated-by: AI
cgwalters-bot added a commit to cgwalters-forge/composefs-rs that referenced this pull request Sep 25, 2026
Release 0.7.0 disabled this job until bootc main caught up with the
composefs-rs API. That has now happened: bootc-dev/bootc#2248 (the
adapt-composefs-rs-v1-erofs-2 branch we were pinned to) is merged, and
bootc main depends on composefs-rs by git tag v0.9.2, so the [patch]
override in bootc/Justfile applies to our workspace version again.

Test against bootc main as before. This also drops the if: never(),
which isn't a valid expression function: the workflow file failed to
parse, so every push to any branch recorded a failed run with no jobs
instead of skipping it.

Generated-by: AI
Signed-off-by: Colin Walters <walters@verbum.org>
cgwalters-bot added a commit to cgwalters-forge/composefs-rs that referenced this pull request Sep 25, 2026
Release 0.7.0 disabled this job until bootc main caught up with the
composefs-rs API. That has now happened: bootc-dev/bootc#2248 (the
adapt-composefs-rs-v1-erofs-2 branch we were pinned to) is merged, and
bootc main depends on composefs-rs by git tag v0.9.2, so the [patch]
override in bootc/Justfile applies to our workspace version again.

Test against bootc main as before. This also drops the if: never(),
which isn't a valid expression function: the workflow file failed to
parse, so every push to any branch recorded a failed run with no jobs
instead of skipping it.

Generated-by: AI
Signed-off-by: Colin Walters <walters@verbum.org>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/documentation Updates to the documentation area/install Issues related to `bootc install` ci/merge Run full CI suite (all OSes) — equivalent to merge queue ci/tier-1 Run CI for tier-1 OS (centos-10) only

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants