Skip to content

[RFC] feature: Carry sysext and confext images in COSI files - #789

Draft
Paco Huelsz (frhuelsz) wants to merge 15 commits into
mainfrom
user/frhuelsz/cosi-extensions
Draft

Paco Huelsz (frhuelsz) wants to merge 15 commits into
mainfrom
user/frhuelsz/cosi-extensions

Conversation

@frhuelsz

@frhuelsz Paco Huelsz (frhuelsz) commented Sep 9, 2026 •

Copy link
Copy Markdown
Contributor

🔍 Description

RFC for carrying sysext and confext images inside COSI. Each entry requires an id matching the DDI's SYSEXT_ID or CONFEXT_ID, reuses ImageFile, and may omit path for Trident-managed placement.

🤔 Rationale

Bundle extensions with the OS and identify updates by (kind, id) rather than by filename or hash.

📝 Checks

📌 Follow-ups

🗒️ Notes

Older Trident versions silently omit bundled extensions; see the RFC's versioning section.

Paco Huelsz (frhuelsz) and others added 2 commits August 28, 2026 18:04
Proposes an optional `extensions` section in COSI metadata (revision 1.3)
holding `sysexts` and `confexts` arrays. Each entry reuses the existing
`ImageFile` object to reference a ZSTD-compressed DDI under
`images/extensions/`, plus an optional destination path mirroring the Host
Configuration `Extension` object.

Covers the metadata schema, tar layout, versioning and forward-compatibility,
Trident-side consumption (effective extension set across the extensions,
osconfig and selinux subsystems), A/B update and rollback semantics, the
Host Configuration interaction, and validation.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
1 pipeline(s) were filtered out due to trigger conditions.
There may be pipelines that require an authorized user to comment /azp run to run.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
An extension-only change to a COSI changes the metadata hash, which forces an
A/B update via ab_update_required() before any subsystem is consulted. Adds an
OS content digest recorded in Host Status, and a requested-then-verified
Runtime Update path gated on digest equality, with refusal as the only failure
mode. Rejects an extension-only COSI variant and records the weaker rollback
guarantee of the runtime path.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@frhuelsz

Copy link
Copy Markdown
Contributor Author

Added a section on extension-only updates, and revised the scope accordingly.

The gap was real. select_servicing_type short-circuits: ab_update_required() returns true whenever os.image.sha384 differs, before any subsystem is consulted. The metadata hash covers the extensions section, so with the RFC as originally written, changing only a bundled extension would have forced a full A/B update and a reboot. That made bundling strictly more disruptive than the current Host Configuration route.

An extension-only COSI is the wrong shape. The metadata root requires images, disk with at least one gptRegions entry, bootloader and osPackages, so a COSI without partition images is not representable, and making it so would require those fields to become optional. Every consumer of os.image — filesystem source resolution, derive_host_configuration, ESP detection, verity setup — assumes the image describes a complete OS. The distinction is drawn on content equality instead.

Mechanism. An OS content digest over the parts of the metadata that determine the deployed OS (images[] and disk.gptRegions[] hashes, disk geometry, bootloader, osArch, osRelease), excluding extensions, recorded in the Host Status. Two COSIs with equal digests deploy an identical OS and differ only in their extensions. Trident cannot determine this otherwise: it retains the URL and metadata hash of the applied image, not the metadata, and re-fetching the previous COSI to diff it is not dependable.

The path is requested and then verified, never inferred. It proceeds only when a Runtime Update is explicitly requested, the digests match, and the recorded image is the deployed one. Otherwise Trident refuses with a structured error naming what differs. It must not promote the operation to an A/B update, and must not apply the extension change while leaving other changes unapplied. Refusal is the only acceptable failure, which is what keeps a partially-applied state unreachable.

Rollback is weaker on this path, and that is why it is not automatic. set_up_extensions skips removal of the superseded image only on Clean Install and A/B Update, so a Runtime Update replaces files in the running slot. Rollback re-runs the subsystem with the specs reversed and re-acquires the previous extension image from its source, which for a bundled extension is the previous COSI. A runtime rollback therefore requires the previous COSI to remain reachable; an A/B rollback requires nothing. That trade should be made by the operator, not inferred from a property of two images.

Three questions added to the open list:

  • What surface requests an extension-only Runtime Update? There is none today; forceAbUpdate is the nearest precedent.
  • What exactly does the OS content digest cover? Excluding too much permits a Runtime Update between images differing in ways Trident does not model; including too much rejects legitimate cases.
  • Should the digest be published in the COSI metadata rather than computed? Computing it needs no writer cooperation and cannot be misdeclared, but publishing would let a writer state intent directly.

…ruction

Replaces the OS content digest with a structured summary of the deployed
image's metadata, recorded in the Host Status, so Trident can distinguish a
partition change from an extension-only change from a no-op, and report which
partition differs. Falls back to the current os.image.sha384 comparison where
no summary is recorded.

Adds the construction requirement: filesystem images are not reproducible, so
an extension-only COSI must be built by copying an existing COSI's region
images verbatim rather than by rebuilding. An unreproducible build degrades to
a redundant A/B update, never a skipped one.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@frhuelsz

Copy link
Copy Markdown
Contributor Author

Revised, on both points.

Storing the metadata. Replaced the single digest with a structured summary of the deployed image's metadata, recorded in the Host Status: for each entry in images[] and disk.gptRegions[], the image.sha384, uncompressedSize and the entry's identity (partition number, mount point, fsUuid, partType, verity root hash); plus osArch, osRelease, disk geometry and bootloader.

Excluded, with reasons: extensions (the subject of the comparison), osPackages (validated but never acted on), compression.maxWindowLog (governs decompression, and a new extension may legitimately raise it), and id (identifies the file, not its content).

A record rather than a digest, since the digest is derivable from it and the record additionally lets Trident report which partition differs. Not the full metadata, since osPackages is the bulk of it and has no bearing on the decision.

This gives the three outcomes, including the no-op case, which the previous revision missed:

Summary Extension set Result
Equal Equal No-op.
Equal Differs Extension-only change; a Runtime Update suffices.
Differs Any A/B update.

os.image.sha384 changes whenever any part of the metadata changes, including id and the ordering of osPackages, so a materially identical image currently forces an A/B update.

The summary carries its own schema version. Where it is absent or of an unrecognised version, Trident falls back to comparing os.image.sha384. That confines a change to ab_update_required() — a core decision — to hosts that have a recorded summary, and gives existing hosts an unchanged upgrade path.

Construction, and the reproducibility problem. This is the more important of the two, and it invalidates the obvious approach. Filesystem UUIDs, inode timestamps, superblock creation and mount times and allocation order all vary between builds, and ZSTD output varies with compression level and library version. Rebuilding from the same source will not generally produce the same images.

So requiring reproducible rebuilds is the wrong approach. The build operation is not "rebuild the image identically with a different extension set", it is "copy an existing COSI, replacing its extension set". Region images and their metadata entries are copied verbatim from the input COSI; no filesystem is rebuilt and no region image is recompressed. Only the extension tar members, the extensions section, compression.maxWindowLog where the new extensions need a larger window, and id change.

Equality is then guaranteed by construction rather than hoped for through reproducibility, and the operation is a tar rewrite rather than an image build. This reframes the writer requirement from a reproducible-builds effort into a bounded COSI-to-COSI rewrite, and it is now listed under Dependencies.

Where a COSI is rebuilt from source instead, its region images differ, the summary differs, and Trident selects an A/B update. That is the safe outcome: the mechanism degrades to current behaviour rather than misclassifying a changed OS as unchanged. The failure mode of an unreproducible build is a redundant A/B update, never a skipped one.

Open questions updated accordingly: the exact coverage of the summary (version is currently excluded), record against digest, and whether the no-op outcome belongs in this RFC at all, given it is a behavioural change independent of extensions.

Paco Huelsz (frhuelsz) and others added 6 commits October 8, 2026 10:28
- Add a deployed extension inventory in Host Status; the Host Configuration
  cannot represent bundled extensions, so spec_old does not describe what is
  deployed.
- Give effective entries a source discriminator; update_host_configuration
  errors when a processed extension has no matching Host Configuration entry,
  and compressed and raw sha384 values must never be compared against one
  another.
- Record three prerequisite defects: in-place replacement deletes the newly
  installed image when ID and destination are unchanged; runtime finalize and
  rollback build their context with image: None so staged payloads must be
  durable; extension placement is validated against the Host Configuration only
  and before the COSI is read.
- Correct the A/B rollback claim, which held only for Host Configuration
  entries, and drop the unsupported read-only assertion.
- SELinux check must move to a hook running on every servicing type; it sits in
  configure(), which returns early for runtime updates.
- Split metadata-time from payload-time validation; omitted paths cannot be
  collision-checked statically, and there is no load-time member existence
  check since tar entries are discovered lazily.
- Warn on extension-release ID mismatch only; systemd accepts a VERSION_ID
  mismatch when SYSEXT_LEVEL matches.
- Note that an omission by an older reader does not self-heal, and that a no-op
  must still persist the accepted image.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Inventory and image summary need current/pending records, mirroring
  spec/spec_old, or finalize cannot reconstruct the target set and rollback
  cannot reconstruct the previous one.
- Scope the summary comparison to the image's servicing requirement;
  select_servicing_type still takes the maximum across subsystems, so an
  extension-only runtime update also requires no other subsystem to need A/B.
- Disk streaming places extensions but osconfig returns early on
  is_stream_image, so the merge units are never enabled.
- Replace the blanket verity rejection: the root-verity /etc overlay is backed
  by var/lib/trident-overlay on an A/B volume, so /etc/extensions is a
  supported destination on clean install and A/B update.
- Stage on the destination filesystem; the fixed /var/lib/extensions/.staging
  assumes /var is writable and makes cross-filesystem placement non-atomic.
- The permitted-directory allow-list is Trident policy, not a format property;
  keep it out of the schema and cover it in Rust validation tests.
- Move the servicing-type request surface out of Open Questions, where it
  contradicted its listing as a prerequisite.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Include fsType in the deployed image summary; it drives resize decisions, so
  two images differing only in fsType must not compare equal.
- Promotion of pending records follows the servicing type: rebooting servicing
  reaches *Finalized before reboot and commits only after booting the expected
  root, so the pending record must survive the reboot and be discarded when the
  boot check fails.
- ManualRollbackChainItem carries only kind, spec, active volume and install
  index, so it must be extended or manual rollback cannot restore the bundled
  extension set.
- Remove the stale Future Possibility that restated the now-required inventory.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- A manual runtime rollback has neither the target COSI nor the superseded
  payload, so inventory metadata alone cannot restore a bundled extension.
  Either retain payloads for the depth of the rollback chain, or refuse the
  operation; restoring nothing silently is not acceptable.
- ManualRollbackRuntime promotes its pending record on completion.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The runtime path was a stated requirement but absent from the exit criteria and
marked deferrable in the plan. Add it to the exit criteria and state that step 5
may land separately but is required for the RFC to be complete.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Streaming places extension files but osconfig returns early on is_stream_image,
so the merge units are never enabled. Refuse to stream a COSI carrying
extensions rather than leaving files that never merge; record enabling the
units on a streamed install as follow-up work.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@frhuelsz

Paco Huelsz (frhuelsz) commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor Author

Revised over six rounds against the code. Summary of what changed and why.

Design gaps

Problem Fix
The deployed bundled set had no representation; spec_old holds only what an operator asked for, so Trident could not tell what to remove, detect collisions, or rebuild state when finalize runs separately Deployed extension inventory in Host Status, current and pending
Pending records promoted at the wrong point for rebooting servicing, which commits only after booting the expected root Promotion follows the servicing type
ManualRollbackChainItem carries only kind, spec, active volume and install index; even extended, a manual runtime rollback has no payload Carry inventory and summary; retain payloads for the rollback chain, or refuse
update_host_configuration fails when a processed extension has no matching Host Configuration entry Source discriminator on effective entries
ExtensionData.sha384 cannot hold both compressed and raw semantics Compare only within a source

Incorrect claims

  • The A/B rollback guarantee was unconditional. validate_extension_images_locations is Host-Configuration-only static validation running before the COSI is read, so bundled destinations are invisible to it. It also does not check read-only, which the RFC had asserted.
  • Rejecting verity-backed destinations was wrong: the root-verity /etc overlay is backed by var/lib/trident-overlay on an A/B volume.
  • Reading the effective set does not fix SELinux. The check sits in configure(), which returns early for Runtime Updates.
  • There is no load-time member-existence check to extend; Cosi::new discovers entries lazily.
  • Destination collisions are not fully detectable from metadata, since an omitted path resolves using the name inside the DDI.
  • The extension-release warning would have flagged valid extensions; systemd accepts a VERSION_ID mismatch when SYSEXT_LEVEL matches. Narrowed to ID.
  • The summary table is the image's requirement, not the outcome; select_servicing_type still maxes across subsystems.
  • fsType was missing from the summary and drives resize decisions.
  • An old reader's omission does not self-heal, and a no-op returns before persisting Host Status.

Prerequisites found in existing code

  • set_up_extensions deletes the newly installed image when an extension keeps its ID and destination but changes content. Pre-existing and reachable today; routine once extensions are bundled.
  • Runtime finalize and rollback build their context with image: None, so staged payloads must be durable.
  • Staging uses a fixed /var/lib/extensions/.staging with a non-atomic cross-filesystem fallback.

Scope

Disk streaming is out: it would place the files but osconfig returns early on is_stream_image, so the merge units are never enabled. Trident refuses rather than leaving files that never merge. The permitted-directory allow-list is Trident policy, not a property of the format, so it stays out of the schema. Extension-only Runtime Update is now an exit criterion.

Paco Huelsz (frhuelsz) and others added 2 commits October 8, 2026 11:44
Review findings had been folded in as prose, leaving most points stated once
and then re-justified. Cuts about a quarter of the text with no change to any
decision, constraint or cited behaviour.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Prose down from 6691 to 3907 words across two passes. Drops the redundant
consumer diagram and trims the example to one entry per array. No decision,
constraint or cited behaviour changed.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Keep path optional for Trident-managed placement, describe current defaults and the unresolved slotted-policy selection from RFC 0838. Remove repeated rationale while retaining schema, update, rollback and validation rules.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add a required per-entry id matching the DDI's SYSEXT_ID or CONFEXT_ID, with duplicate and mismatch validation. Match extension updates by kind and ID; use the compressed hash to detect replacement.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

This branch has not been deployed

No deployments
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.

1 participant