Add API to automatically generate matching EROFS given a digest - #367
Merged
Conversation
There was a problem hiding this comment.
Pull request overview
This PR adds support for recovering (and caching) a bootable EROFS image that matches a known expected digest by searching across EROFS format versions and xattr-filtering modes, addressing bootc-dev/bootc#2334’s “find the original digest despite changed defaults / repo-fixed format config” scenario.
Changes:
- Introduce
XattrFiltering+OciTransformOptionsand thread these options through OCI filesystem construction/transform paths. - Cache boot images per
(format version, xattr filtering mode)and addfind_matching_boot_image()to search/generate/persist the matching artifact for an expected digest. - Add an explicit
commit_image_version()API and defineFormatVersion::BOOT_VERSIONSto support the search space.
Reviewed changes
Copilot reviewed 16 out of 16 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| crates/composefs/src/generic_tree.rs | Adds xattr filtering mode enum/options and applies it in transform_for_oci(), plus unit tests. |
| crates/composefs/src/fs.rs | Threads OciTransformOptions into read_container_root(). |
| crates/composefs/src/filesystem_ops.rs | Adds commit_image_version() to commit non-default EROFS format versions. |
| crates/composefs/src/erofs/format.rs | Adds FormatVersion::BOOT_VERSIONS constant for boot-relevant versions. |
| crates/composefs/Cargo.toml | Adds strum dependency for enum parsing/display/variant enumeration. |
| crates/composefs-oci/src/test_util.rs | Updates helpers to pass OCI transform options through. |
| crates/composefs-oci/src/oci_image.rs | Stores boot image refs as a map keyed by named-ref key; adds mode-aware accessors. |
| crates/composefs-oci/src/lib.rs | Introduces per-mode boot ref keys, splits boot refs from layer refs, and updates config read/write APIs accordingly. |
| crates/composefs-oci/src/image.rs | Threads OciTransformOptions into create_filesystem() and expands tests for mode behavior. |
| crates/composefs-oci/src/delta.rs | Updates config write call site for new boot-images-map parameter. |
| crates/composefs-oci/src/boot.rs | Adds find_matching_boot_image() and mode-aware boot image generation/caching semantics + tests. |
| crates/composefs-oci/Cargo.toml | Adds optional strum dep under the boot feature. |
| crates/composefs-integration-tests/src/tests/varlink.rs | Updates varlink pull call signature to include new parameters. |
| crates/composefs-integration-tests/src/tests/cli.rs | Adds integration coverage for --xattrs keep-user-xattrs and expected-digest recovery behavior. |
| crates/composefs-ctl/src/varlink.rs | Extends varlink API: adds xattrs/expected_digest handling, validation, and richer boot-image reporting. |
| crates/composefs-ctl/src/lib.rs | Adds CLI flags for xattr filtering and expected-digest boot-image recovery flow. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+76
to
+85
| /// Error parsing a [`XattrFiltering`] value from a string. | ||
| #[derive(Debug, Clone, PartialEq, Eq, Error)] | ||
| #[error("invalid xattr filtering mode {0:?} (expected \"allowlist-only\" or \"keep-user-xattrs\")")] | ||
| pub struct XattrFilteringParseError(String); | ||
|
|
||
| impl XattrFilteringParseError { | ||
| fn not_found(s: &str) -> Self { | ||
| Self(s.to_owned()) | ||
| } | ||
| } |
Comment on lines
+319
to
+326
| pub fn boot_image_ref_for_mode( | ||
| &self, | ||
| version: FormatVersion, | ||
| mode: XattrFiltering, | ||
| ) -> Option<&ObjectID> { | ||
| self.boot_image_refs | ||
| .get(crate::boot_image_ref_key(version, mode).as_str()) | ||
| } |
Comment on lines
664
to
+668
| let oc = open_config(repo, config_digest, verity)?; | ||
| Ok(match version.epoch() { | ||
| FormatEpoch::Epoch1 => oc.boot_image_ref_v1, | ||
| FormatEpoch::Epoch2 => oc.boot_image_ref, | ||
| }) | ||
| Ok(oc | ||
| .boot_image_refs | ||
| .get(&*boot_image_ref_key(version, mode)) | ||
| .cloned()) |
Johan-Liebert1
left a comment
Collaborator
There was a problem hiding this comment.
Overall look okay, but a few comments
| b"\x02\x00\x00\x02\x00\x20\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"; | ||
| assert_eq!(cap_val.as_ref(), expected_cap); | ||
|
|
||
| for (mode, keep_user_xattr) in [ |
Collaborator
There was a problem hiding this comment.
Missing two combinations here, is that intentional?
Historically, transform_for_oci() unconditionally stripped every xattr except a fixed container allowlist, discarding legitimate `user.*` xattrs along with host-leaked ones (like build-time security.selinux). This can cause composefs digest mismatches when the allowlist or filtering logic changes across composefs-rs versions, since images built with different versions can end up with different content even though the source layers didn't change. Add composefs::generic_tree::XattrFiltering, an enum selecting between the historical AllowlistOnly behavior and a new KeepUserXattrs mode that additionally preserves `user.*` xattrs. It derives strum's Display/EnumString (kebab-case: allowlist-only, keep-user-xattrs) so it works directly as a clap value, and VariantArray so callers can iterate every mode without a hand-maintained list; behind the varlink feature it also derives Serialize/Deserialize for use as a wire parameter. transform_for_oci() and the OCI filesystem/boot-image builders now take an explicit &OciTransformOptions carrying the mode, and cfsctl gains a --xattrs flag (on the OCI-to-filesystem and compute-id paths) so callers can pick a mode instead of being stuck with the default. Boot images are cached per mode instead of one shared slot: AllowlistOnly keeps using the existing composefs.image.boot[.v1] named-ref key (no back-compat concerns for the common case), while other modes get composefs.image.boot[.v1].xattrs=<mode>, built from XattrFiltering's Display impl. OciImage and OpenConfig grow a single boot_image_refs map (extracted by key prefix) in place of the old fixed pair of V1/V2 Option fields, so AllowlistOnly and KeepUserXattrs boot images can be generated, cached, and garbage-collected side by side without evicting or masking each other; remove_boot_image now clears every cached mode rather than just the default one. Since a UKI embeds the composefs digest of the boot image it was built against, and that digest depends on which mode built the image, add find_matching_boot_image(): it tries every mode in turn (generating or cache-hitting the boot image for each) until one produces a digest matching the caller's expectation, so callers like bootc can recover from a default-mode change across composefs-rs versions without hard-coding retry logic or enumerating modes themselves. Assisted-by: AI Signed-off-by: Colin Walters <walters@verbum.org>
`find_matching_boot_image()` in composefs-oci needs to probe every EROFS format version a repository could plausibly have produced for a boot image, not just the one it's currently configured for -- a repository's `FormatConfig` is fixed once `meta.json` is written, so it can never spontaneously start generating a version it wasn't originally set up for. `FormatVersion::BOOT_VERSIONS` gives the two versions worth trying (V1, V2; the legacy V0 is never produced for boot images). `commit_image_version()` mirrors `commit_image()` but takes the version explicitly instead of deriving it from `repository.format_config()`, so a recovered non-default-version image can actually be persisted into the repo. Prep for the boot-image format-version search in composefs-oci. Assisted-by: AI Signed-off-by: Colin Walters <walters@verbum.org>
…_image `find_matching_boot_image()` already tried every `XattrFiltering` mode to recover a boot image digest embedded in an old UKI, because the default xattr-filtering mode has changed across releases. But it only ever built/compared against a single, fixed `FormatVersion` (whatever `repo.erofs_version()` currently resolves to), leaving the same class of bug unfixed for the EROFS on-disk format version axis. This matters concretely for bootc-dev/bootc#2334: a repository initialized by an older composefs-rs/bootc build may be permanently locked to `FormatConfig::single(V2)` -- format config is validated immutable once a repo's meta.json is written. If a newer build's default format version changes to V1 and seals a UKI with a V1 digest, that digest can never be found by looking at this repo's cached boot-image refs, no matter how many times the boot image is regenerated through the normal commit path, since the repo will only ever commit V2. Recovering requires building the filesystem tree once and computing the V1 image ID directly via the low-level, config-independent `FileSystem::compute_image_id()`. Restructure the search as mode-outer / version-inner, since rebuilding the tree is the expensive part: for each xattr mode, first check the cheap cached named-ref lookups for every `FormatVersion::BOOT_VERSIONS` entry, and only if none hit, build the tree once and compute the image ID directly for whichever versions weren't already covered by the cache. A match found this way is persisted back into the repo (writing the already-computed image bytes, plus a config/manifest rewrite) so the next lookup is a cache hit. `BootImageMatch::Found` now also carries the matched `FormatVersion` alongside the `XattrFiltering` mode; `NotFound` carries the total number of (mode, version) combinations that were attempted, since the per-attempt digests aren't useful for a caller building an error message (they're already known not to match, by construction). Assisted-by: AI Signed-off-by: Colin Walters <walters@verbum.org>
find_matching_boot_image (added in the prior two commits) can recompute a boot image digest for any (xattr filtering mode, EROFS format version) combination, which is exactly what's needed to recover from the scenario in bootc-dev/bootc#2334: a UKI embeds a composefs digest computed with whatever defaults were current at build time, and after an upgrade changes those defaults (or locks the repository to a newer format version), the digest can no longer be reproduced by a plain `--bootable` pull. Wire this up as `cfsctl oci pull --bootable --expected-digest <hex>` and as a new `expected_digest` parameter on the varlink `pull` method. Instead of generating the boot image with the default mode and the repository's format version, every combination is searched until one matches. `--expected-digest` requires `--bootable` and is rejected together with `xattrs` on the varlink side, since searching for an unknown mode and pinning one are contradictory requests. The varlink `Completed` reply gains `boot_image_mode` and `boot_image_format_version` fields (populated on both the recovery and the normal bootable-generate paths) so callers can tell which combination actually produced the digest without having to guess. Assisted-by: AI Signed-off-by: Colin Walters <walters@verbum.org>
Exercise the CLI end to end against the bootc-dev/bootc#2334 scenario: pull a bootable image into a V2-locked repository, separately compute the boot image digest an older bootc build would have embedded (a V1 image, via --erofs-version), then confirm --expected-digest recovers by finding the (mode, version) combination that reproduces it. Also cover the "no combination matches" and "requires --bootable" failure paths. create_bootable_oci_layout builds the minimal fixture transform_for_boot needs (empty boot/ and sysroot/ directories, some content under usr/) without any real kernel/UKI content, since find_matching_boot_image only cares about the resulting composefs digest. Assisted-by: AI Signed-off-by: Colin Walters <walters@verbum.org>
cgwalters
force-pushed
the
xattr-filter-mode
branch
from
July 29, 2026 20:33
410d1cb to
e9724f7
Compare
Johan-Liebert1
approved these changes
Jul 30, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This is intending to address bootc-dev/bootc#2334