Skip to content

initramfs: Enforce fs-verity for strict composefs boots - #2629

Merged
cgwalters merged 2 commits into
bootc-dev:mainfrom
jmarrero:advisory-fix-1
Nov 6, 2026
Merged

cgwalters merged 2 commits into
bootc-dev:mainfrom
jmarrero:advisory-fix-1

Conversation

@jmarrero

@jmarrero jmarrero commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

On the composefs backend, the initramfs let the repository's meta.json decide whether fs-verity was enforced. meta.json is an unsigned file on the same filesystem as the images, so a strict composefs=<digest> command line could end up mounting an image without fs-verity, and an image under images/refs/<digest> could stand in for the pinned digest.

With this change the kernel command line decides, not the repository:

  • A strict composefs=<digest> sets IntegrityPolicy::Explicit(Enabled); one with the ? marker sets Explicit(Disabled). An explicit policy is final for the repository handle.
  • The image is mounted with Repository::mount_digest, which only looks at images/<digest> and requires the measured fs-verity digest to match. images/refs/ is never consulted for a boot, with or without the marker.
  • With ?, missing fs-verity is tolerated, but a digest that can be measured still has to match.
  • Installations whose meta.json lacks fs-verity while their image and objects carry it keep booting.

Repository::set_insecure now returns a Result, so the remaining callers propagate it; their behavior is unchanged.

This bumps composefs-ctl to 0.10.0, the release that provides mount_digest and the integrity policy (composefs/composefs-rs#439).

Tests

  • A unit test checks that a strict mount rejects an image without fs-verity and that an image only present under images/refs/ is not found in either mode. It runs unprivileged on tmpfs.
  • A new booted composefs-verity TMT test, for the composefs backend only, builds disposable repositories and checks strict and relaxed command lines against meta.json with and without fs-verity. The cases are: an untampered image, an image stripped of fs-verity, image and meta.json both stripped, a tampered data object, images/<digest> repointed at another verified image, and an image only present under images/refs/. It skips itself when /var/tmp does not support fs-verity.

Compatibility

A system with a strict command line whose image has no fs-verity now stops in bootc-root-setup when booting the new deployment ("fs-verity is not supported by filesystem"). Such a system booted before only because meta.json turned enforcement off.

From reading the code at the release tags, bootc could only produce that combination when all of these hold:

  • composefs backend, with a sealed UKI or an unsealed BLS entry;
  • installed by 1.16.3 through 1.16.14;
  • installed with install to-filesystem or install to-existing-root;
  • root on ext4 created without -O verity.

A boot test of that case is reported in #2629 (comment).

Not affected: the ostree backend, xfs installs (they carry the ? marker since v1.14.0), and install to-disk on ext4 (bootc creates it with -O verity).

To recover, add ? to the BLS entry, or rebuild the UKI with bootc container ukify --allow-missing-verity. The previous deployment carries its own initramfs and is expected to still boot when selected by hand; that has not been tested yet.

Which installs historically got a strict command line, including ones made by other installers, is still being discussed in the comments below.

Generated-by: AI

Copilot AI balanced review requested due to automatic review settings November 6, 2026 13:58
@bootc-bot
bootc-bot Bot requested a review from henrywang November 6, 2026 13:59

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔵 Needs a closer look

Changes to the early-boot integrity trust boundary and an unreleased composefs dependency require human security review and privileged boot validation across supported kernels and legacy repositories that could not be performed here.

No open issues 🎉

What changed in this PR

This PR hardens composefs boot integrity by making the kernel command line—not mutable repository metadata—control fs-verity enforcement in the initramfs.

Changes:

  • Mount images by verified digest with an explicit integrity policy, excluding named-reference fallback.
  • Add unit and integration coverage for tampered images, metadata, data objects, and relaxed mounts.
  • Pin composefs to the upstream implementation and propagate errors from updated policy APIs.
File Description
tmt/​tests/​tests.fmf Registers the verity integration test for image-mode hosts.
tmt/​plans/​integration.fmf Adds a composefs-only verity test plan.
crates/​tests-integration/​src/​booted/​composefs_verity.rs Tests integrity enforcement using disposable repositories.
crates/​tests-integration/​src/​booted.rs Registers the composefs-verity subcommand.
crates/​tests-integration/​README.md Documents suite usage, coverage, and isolation.
crates/​tests-integration/​Cargo.toml Adds dependencies for exercising the mount helper.
crates/​lib/​src/​store/​mod.rs Propagates errors when relaxing repository policy.
crates/​lib/​src/​bootc_composefs/​repo.rs Updates repository policy calls to handle errors.
crates/​lib/​src/​bootc_composefs/​boot.rs Handles policy-setting errors during boot setup.
crates/​initramfs/​src/​lib.rs Enforces digest-based mounting and adds rejection tests.
crates/​initramfs/​Cargo.toml Adds temporary-directory support for unit tests.
Cargo.toml Temporarily pins composefs to the upstream digest-mount implementation.
Cargo.lock Records the pinned composefs source and dependency updates.

Review effort: Balanced

Comment thread Cargo.toml Outdated
# Repository::mount_digest, until it is released. Replace with a bump of the
# composefs-ctl version above once that happens.
[patch.crates-io]
composefs-ctl = { git = "https://github.com/composefs/composefs-rs", rev = "b43c311b2c32ca3c1bb18c2c612df84ad5a96dcc" }

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.

It's released right?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fable output:

Yes. The latest push (c53c2fc) updates this: the temporary [patch.crates-io] pin is gone and the dependency is now composefs-ctl = "0.10.0" from crates.io. Thank you for releasing composefs-rs 0.10.0!

@cgwalters

cgwalters commented Nov 6, 2026 •

Copy link
Copy Markdown
Member

Also from the previous PR

Compatibility consequence, please state it in the commit message and the advisory text. A strict
cmdline with an image that has no fs-verity now fails in bootc-root-setup instead of silently booting
unverified. Current code can't produce that combination (validate_repository_policy refuses it), but
1.16.13/1.16.14 installs on a filesystem without fs-verity, such as XFS, would have gotten an
insecure repository from the pre-flight bug and therefore a strict cmdline with no verity anywhere.
If such systems exist, they will drop to the emergency shell after this update. Recovery is to add
the ? marker, either by editing the BLS entry or rebuilding the UKI with --allow-missing-verity. This
can't be avoided safely: treating "filesystem not supported" as acceptable

This needs investigation for sure.

If this is true...then what we may need to do is change the logic here to detect whether the commandline came from a location that has verification (UKIs, especially with secure boot)

@jmarrero

This comment was marked as outdated.

@jmarrero-bot

This comment was marked as outdated.

@cgwalters

This comment was marked as outdated.

A strict composefs command line must not inherit an insecure repository
policy from meta.json. Verify the opened image against the requested
digest and mount that same file descriptor with verity required. This
preserves valid boots from older installations with unprotected metadata
and also verifies images resolved through named refs.

Add unit and TMT coverage for stripped image verity, metadata replacement,
wrong image refs, tampered data objects, and explicitly relaxed mounts.
The mount suite passes with the fix and rejects the original behavior.

Generated-by: AI
Signed-off-by: Joseph Marrero Corchado <jmarrero@redhat.com>
composefs-rs now provides Repository::mount_digest, which takes the
expected digest as a typed value, only looks at images/<digest> and
requires the measured fs-verity digest to match before mounting. That
is the check the previous commit open-coded around open_image; use the
library API instead so there is one implementation of it, shared with
composefs-setup-root.

composefs-rs also records whether a repository's fs-verity requirement
was detected from meta.json or chosen by the caller, and an explicit
choice is final for the handle. The initramfs sets
`IntegrityPolicy::Explicit(Enabled)` for a strict `composefs=` digest
and `Explicit(Disabled)` for one with the `?` marker, then mounts by
digest either way. With the marker a missing fs-verity digest is
tolerated, but one that can be measured still has to match.

The name-based mount() no longer runs in the initramfs, so images/refs/
is never consulted for a boot, with or without the marker. An image
that only exists under images/refs/<digest> is now reported as not
found rather than as a digest mismatch, so the boot moves on to the
next command line candidate the same way it does for a missing image.

`Repository::set_insecure` now returns a `Result`, so the remaining
callers propagate it. They all run on a freshly opened repository,
where the detected policy may still be replaced, so their behavior is
unchanged.

The booted verity test lists each case's expected result in a table
per mode, matches on the typed CompareVerityError::DigestMismatch
rather than on the message, and gains a case where images/<digest> is
repointed at another verified image, which both modes must reject as a
digest mismatch.

Bump composefs-ctl to 0.10.0, the release that carries these APIs
(composefs/composefs-rs#439).

Generated-by: AI
Signed-off-by: Joseph Marrero Corchado <jmarrero@redhat.com>
@cgwalters-bot

This comment was marked as outdated.

@jmarrero-bot

This comment was marked as outdated.

@cgwalters-bot

This comment was marked as outdated.

@cgwalters

Copy link
Copy Markdown
Member

Filed #2632 related to this

name: &str,
allow_missing_fsverity: bool,
) -> Result<OwnedFd> {
let digest = Sha512HashValue::from_hex(name).context("Parsing composefs digest")?;

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.

There's also something I don't like here, we should be pushing up the requirement for name = digest earlier I think. Actually I think this code should be tightly coupled to the kernel cmdline parsing instead of us extracitng it.

But I know that's larger work that can be a followup

@cgwalters
cgwalters merged commit 00574d5 into bootc-dev:main Nov 6, 2026
54 checks passed
@jmarrero-bot jmarrero-bot moved this from In Progress to Done in Workstream Nov 6, 2026
@jmarrero-bot

jmarrero-bot commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

Summary of the compatibility investigation for this PR, reconciled with @cgwalters and @cgwalters-bot. Full per-tag evidence and logs: gist.

Executive summary

This change breaks one set of existing installs. We accepted that, and recovery is easy and tested.

Affected: composefs installs made by bootc 1.16.0 through 1.16.14 with install to-filesystem or install to-existing-root (including system-reinstall-bootc), onto an ext4 filesystem created without -O verity, for example one made by Anaconda or taken from a cloud image.

  • Those installs got a strict composefs= argument (no ?), but no fs-verity.
  • They booted only because 1.16.x's initramfs didn't enforce fs-verity on a repository created without it, and 1.16.x always created it that way.
  • With this change, the new deployment stops in bootc-root-setup with "fs-verity is not supported by filesystem". This applies to BLS entries and to strict UKIs.

Two independent VM runs reproduced this: @cgwalters-bot with 1.16.13 and 1.16.14 (cgwalters-devspace-sandbox#33), and ours with 1.16.0 and 1.16.14.

Not affected:

  • XFS installs, which get ? since 1.14.0.
  • install to-disk, which formats ext4 with -O verity.
  • image-builder and bootc-image-builder, which format ext4 with verity: true.
  • The ostree backend.
  • Installs by bootc ≤1.15.2 or ≥1.17.0, which refuse a strict install onto ext4 without verity.

btrfs is not affected: 1.16.x installs there also lack fs-verity on meta.json, but every object, including the composefs image, gets fs-verity, so they keep booting (results).

Recovery (boot-tested):

  • Select the previous deployment in the boot menu. It boots with its own initramfs. Nothing falls back automatically.
  • Add ? right after composefs= in the new entry. From the GRUB editor this lasts one boot; editing the entry file makes it permanent, and later upgrades keep the ?.
  • Or make that edit on the booted entry before upgrading, and the failure never happens.

Still to do:

  • a release note naming the affected set and the recovery steps;
  • the pre-upgrade check below;
  • a clearer initramfs error for this case, coming as a follow-up PR.

Follow-ups:

Before upgrading: am I affected?

Run as root (/sysroot/composefs is mode 0700):

args=$(tr ' ' '\n' </proc/cmdline | grep -E '^composefs(\.digest)?=') || { echo "unaffected: not booted with composefs"; exit 0; }
for arg in $args; do
  val=${arg#*=}
  case $val in \?*) echo "unaffected: '?' set, fs-verity is optional"; exit 0;; esac
  img=$(readlink -e /sysroot/composefs/images/${val##*:}) || continue
  if lsattr "$img" 2>/dev/null | cut -d' ' -f1 | grep -q V; then echo "unaffected: strict, and the image has fs-verity"; exit 0; fi
  echo "AFFECTED: strict $arg but $img has no fs-verity ($(findmnt -no FSTYPE /sysroot) root)"; exit 1
done
echo "unknown: no composefs image found for $args"; exit 2

Results:

  • It printed AFFECTED on the 1.16.0 and 1.16.14 ext4 installs (systemd-boot BLS, strict UKI).
  • It printed unaffected on to-disk ext4, XFS, and the ? installs.
  • It printed unaffected on the btrfs installs. The previous version read only the first composefs argument, and wrongly printed AFFECTED on a 1.16.0 btrfs system already switched to initramfs: Enforce fs-verity for strict composefs boots #2629's UKI: 1.16.0's bootc writes only the image that composefs= names, not the one composefs.digest= names. This version tries each argument; its AFFECTED path was rechecked only against a stub root, not on a new ext4 install.

For ext4 alone, tune2fs -l <root device> | grep features (no verity) is a rougher check.

Recovery

  • Before upgrading (the check prints AFFECTED):

    1. Add ? right after composefs= in the booted entry. For systemd-boot that's /boot/loader/entries/*.conf on the ESP; for GRUB, with /boot on the root filesystem, it's /sysroot/boot/loader/entries/*.conf.
    2. Reboot.
    3. Upgrade.

    This gives up fs-verity enforcement, which these systems never had.

  • After a failed boot:

    1. Pick the previous entry: for systemd-boot, press a key while it starts; for GRUB, Shift, Esc or F4.
    2. Add ? to the new entry's file and reboot. From GRUB you can instead add ? once in the editor.
  • To get enforcement: reinstall onto ext4 made with -O verity. Turning the ext4 feature on afterwards wouldn't by itself give the objects already stored fs-verity (not tested).

  • UKIs: the command line is inside the UKI, so ? can't be added there. Select the previous UKI, then upgrade to an image whose UKI is built with bootc container ukify --allow-missing-verity. The second step is untested (see Details).

The suggested initramfs error reads:

This boot entry requires fs-verity (its composefs= argument has no '?'), but the root filesystem does not support it (for example ext4 created without '-O verity'). To boot, select the previous deployment in the boot menu, or add '?' right after 'composefs=' (or 'composefs.digest=') in this entry's kernel command line, which boots without fs-verity enforcement. To keep enforcement, reinstall onto a filesystem with fs-verity enabled

It is added only when the command line is strict and measuring fs-verity fails with "not supported by filesystem".

Details

Who has a strict command line without working fs-verity, boot tests, follow-up proposals

Who has a strict command line without working fs-verity

For BLS, bootc chooses the policy at install time from the filesystem type. It doesn't probe whether verity works:

  • ext4 and btrfs get a strict composefs=;
  • XFS gets ? (since 1.14.0);
  • --allow-missing-verity forces ?.

Upgrades copy the booted setting (since 1.14.0), so it stays fixed after install. A UKI carries its policy in its embedded command line, set when the UKI is built.

bootc that ran the install ext4 without verity, strict command line
≤1.15.2 install fails (object writes need fs-verity)
1.16.0–1.16.14 install succeeds: meta.json and objects stored without fs-verity
1.17.0+ and #2629 install fails, even with --allow-missing-verity

Only 1.16.0, 1.16.14 and #2629's head were boot-tested; the other rows come from the source.

On ext4 with verity, 1.16.x installs still got fs-verity on their objects, so they keep booting after #2629.

Boot tests

Setup:

  • Images were built with the repository's just build (composefs variant) at v1.16.0, v1.16.14 and initramfs: Enforce fs-verity for strict composefs boots #2629's head (c53c2fc).
  • They were installed with bootc install to-filesystem onto disks partitioned by hand, with root made by plain mkfs.ext4.
  • VMs ran under qemu: OVMF for systemd-boot, legacy BIOS for GRUB.
Case Result
1.16.0 or 1.16.14 install, ext4 without verity (BLS, and a strict UKI on 1.16.14) strict composefs=; 0 of 200 sampled objects have fs-verity; boots
bootc switch to #2629, reboot emergency shell, FS_IOC_MEASURE_VERITY: fs-verity is not supported by filesystem (BLS and UKI)
select the previous entry by hand (systemd-boot BLS and UKI, GRUB) boots the 1.16.x deployment; bootc status then lists the failing one as rollback
add ? in the GRUB editor boots the new deployment once; the entry on disk is unchanged
add ? to the entry file boots; the next bootc switch (by #2629's bootc) writes composefs.digest=?… and that boots too
add ? to the booted entry, reboot, then upgrade with 1.16.14 the new entry has ? and boots
#2629: to-disk ext4, XFS, and ? on ext4 with verity all boot; on ext4 with verity the overlay is mounted verity=require, with ? or without
#2629: to-filesystem onto ext4 without verity, --allow-missing-verity install fails at enabling verity on meta.json
suggested error commit on an affected system prints the recovery text quoted above, then the original error chain; a normal strict ext4-with-verity install boots unchanged (tested with the commit's first revision, which differs only in the message text)

Not tested:

  • recovering a strict UKI by upgrading to a relaxed one: our relaxed and strict test UKIs had the same composefs digest, so bootc refused the switch;
  • 1.16.x installs onto ext4 with verity (to-disk, image-builder) upgraded to initramfs: Enforce fs-verity for strict composefs boots #2629. That they keep booting comes from the source; only initramfs: Enforce fs-verity for strict composefs boots #2629's own installs on ext4 with verity were booted;
  • editing the entry file under GRUB, and the pre-upgrade edit under GRUB (the upgrade itself fails there, see below);
  • btrfs;
  • install to-existing-root itself, which runs the same code path as to-filesystem;
  • GRUB on UEFI;
  • greenboot;
  • Secure Boot (sealed) UKIs.

On legacy-BIOS GRUB, #2629's bootc can't upgrade at all ("SystemNotUEFI"). That's #2605 (proposed fix in #2602), unrelated to this change.

Follow-ups (proposals, not part of #2629)

  • ? by default for all BLS installs (@cgwalters).

    • What changes: today --allow-missing-verity exists only as a CLI flag (InstallComposefsOpts), and [install] has no matching field. The proposal is an [install] field plus a CLI flag to opt into a strict BLS command line, with ? by default.
    • Storage with fs-verity loses nothing: objects still get fs-verity, and the overlay is still mounted verity=require (tested above).
    • UKIs are unaffected: their builder chooses the policy.
    • Caveats: the provisional repository would have to follow the requested policy, because today a relaxed install onto ext4 without verity still fails. A new default also doesn't change existing entries.
  • Update hooks (#640).

    • What it would have allowed: code from the new image, run by the old bootc before staging, could have added ? and fixed meta.json on affected systems.
    • Limit: it only helps once the bootc doing the upgrade supports it, so it can't help 1.16.x systems upgrading now.
  • Pinned-version upgrade tests (#2632): the cases above (old bootc onto ext4 without verity, upgrade, recovery) are candidates.

  • Digest check: require name = digest earlier, coupled to the kernel cmdline parsing rather than extracted afterwards (review comment).

  • Telling UKI from BLS boots in the initramfs: this would allow relaxing only unsigned command lines. We found no reliable signal:

    • systemd-stub's StubInfo proves a UKI boot when present.
    • Its absence doesn't prove BLS, because the write is best-effort and aboot has a signed command line with no stub.
    • systemd-boot reports a .conf entry ID for bootc's UKIs too.

    We dropped this once the break was accepted; notes are in the evidence file.

Generated-by: https://github.com/jmarrero-bot#llms

@jmarrero-bot

jmarrero-bot commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

btrfs is not affected. 1.16.x installs on btrfs also get a strict composefs= and a meta.json without fs-verity. But on btrfs, composefs-rs enables fs-verity on every object, including the composefs image, and this change doesn't look at meta.json. All of these installs boot after upgrading to this PR's head (c53c2fc).

1.16.x install on btrfs objects with fs-verity boots after the upgrade
BLS to-filesystem, 1.16.0 / 1.16.3 / 1.16.14 (@cgwalters-bot) all yes
strict UKI to-filesystem, 1.16.0 / 1.16.14 all yes
to-existing-root onto Fedora Cloud 44, 1.16.0 / 1.16.14 all yes*

* This is separate from this change. Composefs to-existing-root doesn't work as installed on Fedora Cloud's layout, where /boot is a btrfs subvolume, with 1.16.0, 1.16.14 and this PR's head alike. The GRUB entry paths lack /boot/, and bootc status can't find the entries. We worked around both to test booting. It's worth covering if #2633 adds to-existing-root on Fedora Cloud's btrfs layout.

I've updated the check script in the summary above: the first version could wrongly print AFFECTED on a 1.16.0 system already switched to a UKI built by this PR.

Details and logs: gist comment.

Generated-by: https://github.com/jmarrero-bot#llms

jmarrero-bot added a commit to jmarrero-forge/bootc that referenced this pull request Nov 7, 2026
`just test-upgrade` starts from whatever bootc the published base image
ships that day, so it never covers an upgrade from a release we know
users run, such as the 1.16.x behind bootc-dev#2629. This builds a given release
(a tag, or the latest one) from its own sources onto the current base
image, installs it with that release's bootc via bcvk to-disk, then
reuses the readonly plan's upgrade step to move to the local build.

The release is built with its own install-buildroot and build-rpm,
which have kept their interface since 1.15, rather than with its own
Dockerfile: that fetches things which have since moved, and shares a
cargo cache mount that can hand it another version's build.

Before upgrading, the readonly plan now also checks that the release
under test runs the system, records which build it was, and on
composefs runs the pre-upgrade check from bootc-dev#2629, which must find a
to-disk install unaffected. The composefs smoke test checks loader.conf
only on installs by bootc 1.17.4 or later, which take it from the image.

Part of bootc-dev#2632.

Generated-by: AI
jmarrero-bot added a commit to jmarrero-forge/bootc that referenced this pull request Nov 7, 2026
bootc 1.16.0 through 1.16.14 install with `to-filesystem` onto ext4
made without fs-verity, with a strict composefs argument, and since the
change in bootc-dev#2629 the upgraded deployment fails to boot. A tmt plan can't
wait for a boot that fails, so `--installer=to-filesystem` runs that VM
itself: bcvk creates it, its disk is replaced by a to-filesystem install
onto a stock mkfs.ext4, and the in-VM steps in pinned-to-filesystem.nu
run over SSH.

For 1.16.x this checks that the pre-upgrade check says AFFECTED, that
the upgraded boot stops in bootc-root-setup with the expected error on
the console, and both documented recoveries: booting the previous entry
and then adding '?' to the new one (which the next upgrade keeps), and
adding '?' before upgrading. Other releases must refuse the install.
The expected console lines are one constant, to tighten once the
clearer initramfs error lands.

Part of bootc-dev#2632.

Generated-by: AI
jmarrero-bot added a commit to jmarrero-forge/bootc that referenced this pull request Nov 7, 2026
Each job is one `just test-upgrade-pinned` row, picked from the cases of
the bootc-dev#2629 investigation (listed in the workflow's header): ostree and
composefs to-disk installs from 1.15.2, the first and last 1.16 and the
latest release, and the to-filesystem rows. A full run should take
about a dozen runner-hours, so it runs weekly, on demand, and on PRs
labeled ci/upgrade-pinned rather than on every PR. The installer axis
is where image-builder and Anaconda installs are meant to go.

Part of bootc-dev#2632.

Generated-by: AI
@jmarrero-bot jmarrero-bot moved this from Done to In Review in Workstream Nov 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants