Skip to content

image: a digest-pinned ref only trusts a record that hashes back to it - #13

Merged
apostasie merged 1 commit into
mainfrom
claudio/20260913-digest-pinned-resolve
Sep 13, 2026
Merged

apostasie merged 1 commit into
mainfrom
claudio/20260913-digest-pinned-resolve

Conversation

@closer-claudio

@closer-claudio closer-claudio commented Sep 13, 2026

Copy link
Copy Markdown
Contributor

Resolve built the Image for a cached ref@platform from the record's two fields — digest and config — and for a digest-pinned ref never checked either against the ref. Under the default pull policy the record is the whole story (no registry), so an edited or misfiled record redirected debian@sha256:X to another rootfs with another entrypoint, offline, silently. The one thing the user pinned was the one thing the run did not verify.

Why not "compare the digests"

A multi-platform ref names an index; the record legitimately holds the per-platform manifest digest, not the index's. A plain comparison would refuse every multi-arch pinned ref — including forkcloser/curl's debian:…@sha256: invocation.

What this does

The record keeps the chain as raw bytes — index (when the ref named one), platform manifest, config blob — and a digest-pinned ref is resolved from bytes, not fields:

  • sha256(index) == ref, and the index lists sha256(manifest) for the platform — or, with no index, sha256(manifest) == ref;
  • the manifest's config descriptor == sha256(config blob);
  • the digest and config the Image carries are re-derived from those bytes.

Any broken link → ErrResolve naming what disagrees. Tag records are taken as-is, as before.

Records predating the chain still serve tags; for a pinned ref they re-resolve once under missing (the answer is recorded with its chain) and refuse under never, naming --pull=always.

Boundary: the flattened rootfs blob is a derived artifact and is trusted from the cache as before — the chain reaches the manifest, not the layers. The record can no longer be edited undetected; the store still can. Same boundary the content cache always had, now stated.

Verification

  • just test green. New tests: chain verified via index and via a direct manifest; five alterations refused (digest field swapped, whole chain swapped, manifest swapped under the right index, config blob swapped, config blob edited); a legacy record refused for a pinned ref under never with the hint, and still serving a tag.
  • just lint: every code lane green. The links lane is red in my sandbox onlythird_party/vz/README.mdlima-vm.io, untouched here, blocked by the sandbox network allowlist; CI has the network.

Context: found while assessing whether allowlisting ~/Library/Caches/ossein for agent sessions widens the trust boundary. Independent of #10; whichever merges second rebases (both touch resolution and Resolve).

@closer-claudio
closer-claudio force-pushed the claudio/20260913-digest-pinned-resolve branch 3 times, most recently from 3a535b0 to df123ca Compare September 13, 2026 19:17
Resolve built the Image for a cached ref@platform from the record's two
fields, digest and config, and for a digest-pinned ref never checked
either against the ref. Under the default pull policy that record is the
whole story — no registry is consulted — so a record that was edited, or
written for something else, redirected `debian@sha256:X` to another
rootfs with another entrypoint, offline, with nothing left to notice.
The digest the user pinned was the one thing the run did not verify.

A plain digest comparison would be wrong: a multi-platform ref names an
index, and the record legitimately holds the per-platform manifest's
digest, not the index's. So the record now keeps the chain as raw bytes —
the index the ref named (when it is one), the platform manifest, the
config blob — and a digest-pinned ref is resolved from bytes, not fields:
sha256(index) must equal the ref, the index must list sha256(manifest)
for the platform (or, with no index, the manifest must hash to the ref),
the manifest's config descriptor must equal sha256(config blob), and the
digest and config the Image carries are re-derived from those bytes. A
record that fails any link is refused with ErrResolve naming what
disagrees. A tag's record is taken as is, as before: a tag can name
anything.

Records written before the chain was kept still serve tags. For a pinned
ref they are re-resolved once under missing (the registry answer is
recorded with its chain) and refused under never, naming --pull=always.

What this does not do: the flattened rootfs blob is a derived artifact
and is trusted from the cache as before; the chain reaches the manifest,
not the layers. The record cannot be edited undetected any more; the
store still can. That is the boundary the content cache has always had.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: closer-claudio <claudio@farcloser.world>
@closer-claudio
closer-claudio force-pushed the claudio/20260913-digest-pinned-resolve branch from df123ca to 464a4ad Compare September 13, 2026 19:18
@apostasie
apostasie merged commit 9cd70f2 into main Sep 13, 2026
11 checks passed
@apostasie
apostasie deleted the claudio/20260913-digest-pinned-resolve branch September 13, 2026 23:39
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.

2 participants