Skip to content

Materialize per-layer artifacts with explicit whiteout handling - #456

Open
chruffins wants to merge 2 commits into
mainfrom
hypeship/layer-artifacts
Open

Materialize per-layer artifacts with explicit whiteout handling#456
chruffins wants to merge 2 commits into
mainfrom
hypeship/layer-artifacts

Conversation

@chruffins

@chruffins chruffins commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

tldr

Content-addressed per-layer artifact store at images/layers/<layer-blob-digest>/. Pulls and composition (next PR in the stack) share it. Native layer artifacts are Linux-root-only; rootless Linux and macOS use the merged-rootfs path instead, with no image-content change.

what this adds

  1. Materialization (materializeLayerArtifact) — unpacks a layer blob from the existing shared OCI cache with umoci, converts to erofs, installs layer.erofs + an artifact.json record atomically. Same layer shared across images materializes once; concurrent callers share one build (singleflight), with a manager-level concurrency bound.
  2. Whiteouts as overlayfs — on supported Linux hosts, .wh.<name> becomes a 0:0 char device and .wh..wh..opq becomes a trusted.overlay.opaque xattr. This is the standard representation required by later overlayfs stacking; it is not attempted where the host cannot create it.
  3. Platform fallback contract — a startup probe verifies Linux mknod and trusted overlay xattr support on the layer-store filesystem. Rootless Linux, macOS, and unsupported filesystems skip per-layer artifact materialization and compose OCI layers into one rootfs with DirRootfs, which applies whiteouts in userspace. This preserves image semantics while giving up per-layer sharing on those hosts.
  4. Hardened extraction — umoci confines entries, blob digest + diff ID verified, stream capped at 100 GiB, detached builds bounded by a 1h deadline.
  5. Disk accounting — the layer store, including records and in-progress extraction/install trees, is counted in TotalOCICacheBytes (conservative: blob + artifact of the same layer may transiently coexist). Active builds keep totals uncached, and disk-usage scans retry if invalidated while a scan is in progress.

review status: done

Two independent review passes (correctness + code-quality). Findings and how they are handled:

  • Hung blob read could wedge the singleflight key forever → 1h deadline on detached builds
  • Cache blob integrity was never verified → unpack now checks the compressed digest
  • Rootless/macOS whiteout extraction could not produce valid overlayfs artifacts → unsupported hosts now use the merged-rootfs fallback
  • Disk-usage invalidation could return stale totals → scans retry after a concurrent invalidation
  • Same-size artifact corruption was reusable → records now include and verify an artifact checksum
  • Detached layer builds were unbounded across distinct digests → a manager-level semaphore limits them
  • Compressed blob verification could miss decoder-unconsumed suffixes → the raw blob is hashed independently
  • Layer-build cleanup could race disk accounting → cleanup completes before the build is marked inactive
  • Conflicting descriptor encodings could share one artifact path → records store the normalized encoding and reject mismatches
  • Dead/simplification cleanup: composeOnDiskFormat + whiteoutPrefix moved to tests, multiCloser deleted, blob path via paths.OCICacheBlob

Verified empirically on a dev host: mkfs.erofs 1.8.10 and mkfs.ext4 both preserve whiteout char devices and opaque xattrs through ExportRootfs on the supported Linux-root path. Depends on host erofs-utils version; check it in the pull-integration PR.

reviewer focus (3 things)

  1. layerBuildTimeout = 1h is a judgment call — flag if hypeman has a convention for bounding background work
  2. Unpack now hard-fails on blob digest mismatch — new error if any path intentionally writes unverified blobs
  3. PR #458 wires the store into production. It skips layer materialization on unsupported hosts and retains blob-based merged-rootfs composition as the fallback.

validation

Full lib/images + lib/paths green on a rootless host; capability-gated whiteout/device tests pass or skip based on an actual mknod + trusted-xattr probe. Tests cover: materialize/reuse/rebuild, corrupt-record recovery, same-size artifact corruption, invalid digest rejection (sha256:.., sha256:a/b), whiteout/opaque output and cross-layer semantics, symlink confinement and replacement, hardlinks, cancellation, zstd + docker media types, concurrent singleflight (8 callers, one build), trailing-padding diff IDs. The exported erofs whiteout round trip moves to the composition PR, which is where the host erofs-utils version gets checked.

CI note: the test workflow is flaky on this branch — failures are unrelated integration tests (TestEgressProxyRewritesHTTPSHeaders timeout, docker/network tests needing nginx:alpine), failing before the latest commits too. Layer-artifact tests pass consistently.

next step

Review the focus items above, then approve. PR #458 is the production caller and lifecycle follow-up.


Note

Medium Risk
New layer unpack and filesystem materialization touches tar extraction and external mkfs tools; incorrect whiteout or path handling could affect future image composition, though pull behavior is unchanged until wired in.

Overview
Introduces a content-addressed per-layer artifact store under images/layers/<blob-digest>/, keyed by compressed layer digest plus format (erofs/ext4). materializeLayerArtifact reads blobs from the existing OCI cache, unpacks with umoci into a temp dir, converts to the default disk format, and installs layer.* plus an artifact.*.json record atomically; duplicate work is deduped with singleflight and valid on-disk records are reused.

Whiteouts are modeled explicitly: artifact extraction uses umoci OverlayfsRootfs (character-device whiteouts and opaque xattrs) so stacked layers can be mounted later; compose-oriented unpacking uses DirRootfs. Layer unpack supports gzip/zstd, verifies diff IDs, caps unpacked size at 100 GiB, and honors context cancellation via a contextReader and CommandContext for mkfs.erofs.

Disk accounting now walks layer.* files (skipping .unpack-* temps) and folds that into TotalOCICacheBytes / resource admission alongside OCI blobs. New path helpers live in lib/paths. The materialization API is implemented and tested; production wiring is in follow-up PR #458, which skips it on unsupported hosts.

Reviewed by Cursor Bugbot for commit d4ed105. Configure here.

Comment thread lib/images/layer_artifact.go
Comment thread lib/images/layer_artifact.go Outdated
Comment thread lib/images/layer_artifact.go
Comment thread lib/images/layer_artifact.go Outdated
Comment thread lib/images/layer_artifact.go
@chruffins
chruffins force-pushed the hypeship/layer-artifacts branch from 2ea330e to 57b4c9c Compare August 26, 2026 18:44
@chruffins
chruffins force-pushed the hypeship/layer-artifacts branch from 57b4c9c to d2068dc Compare August 26, 2026 18:45
@chruffins
chruffins force-pushed the hypeship/layer-artifacts branch from a08da7b to 6e93aa4 Compare August 26, 2026 18:52
@chruffins
chruffins force-pushed the hypeship/layer-artifacts branch 2 times, most recently from 4a6cbdf to 85d7ec5 Compare August 26, 2026 18:54
@chruffins
chruffins force-pushed the hypeship/layer-artifacts branch from 85d7ec5 to 70c6422 Compare August 26, 2026 18:55
@chruffins
chruffins force-pushed the hypeship/layer-artifacts branch from de55693 to e8b5a05 Compare August 26, 2026 18:58
@chruffins
chruffins force-pushed the hypeship/layer-artifacts branch from e8b5a05 to 0374d70 Compare August 26, 2026 19:26
@chruffins
chruffins force-pushed the hypeship/layer-artifacts branch 2 times, most recently from 93d7a10 to 335b70d Compare August 26, 2026 19:31
@chruffins
chruffins force-pushed the hypeship/layer-artifacts branch from 3ad12a5 to eb2b97b Compare August 26, 2026 19:47
@chruffins
chruffins force-pushed the hypeship/layer-artifacts branch from 68bf31c to d883208 Compare August 26, 2026 22:22
Comment thread lib/images/layer_artifact.go Outdated
@chruffins
chruffins force-pushed the hypeship/layer-artifacts branch from 04e6138 to 36ccb28 Compare August 31, 2026 18:38
@chruffins
chruffins force-pushed the hypeship/layer-artifacts branch from 36ccb28 to 06f8b54 Compare August 31, 2026 21:35
@chruffins
chruffins force-pushed the hypeship/layer-artifacts branch from 06f8b54 to 041b07a Compare August 31, 2026 21:38
@chruffins
chruffins force-pushed the hypeship/layer-artifacts branch from 041b07a to aa3a5ea Compare August 31, 2026 21:56
@chruffins
chruffins force-pushed the hypeship/layer-artifacts branch 4 times, most recently from fde98d0 to a0d2aba Compare August 31, 2026 23:13
@chruffins
chruffins force-pushed the hypeship/layer-artifacts branch 8 times, most recently from 14cee93 to 4c270b0 Compare September 2, 2026 13:35
@chruffins
chruffins marked this pull request as ready for review September 2, 2026 13:36
@chruffins
chruffins force-pushed the hypeship/layer-artifacts branch from 4c270b0 to fcd3558 Compare September 2, 2026 13:45
Base automatically changed from hypeship/manifest-layer-model to main September 2, 2026 18:01
@chruffins
chruffins force-pushed the hypeship/layer-artifacts branch from 5248289 to 328ebcf Compare September 2, 2026 18:01
@chruffins

Copy link
Copy Markdown
Contributor Author

@cursor review

@cursor cursor Bot 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.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Want higher recall? High effort reviews run extra passes and find more bugs. A team admin can switch effort levels in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit d4ed105. Configure here.

Comment thread lib/images/layer_artifact.go
@chruffins
chruffins force-pushed the hypeship/layer-artifacts branch from cb96be2 to 7b8f9ef Compare September 3, 2026 16:55
@chruffins
chruffins force-pushed the hypeship/layer-artifacts branch from 59eb8fb to f18a718 Compare September 4, 2026 17:53
@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown
-->

✱ stlc build

go code · compare

Your SDK build was successful.

generate ✅bootstrap ✅format ✅

116 files generated at 3fa860f (pushed)

go get github.com/kernel/hypeman-go-staging@3fa860fd1cac61f71b74d3baf2e6b398ece85e42
python code · compare

Your SDK build was successful.

generate ✅bootstrap ✅format ✅

232 files generated at 45bd3a5 (pushed)

typescript code · compare

Your SDK build was successful.

generate ✅bootstrap ✅format ✅

138 files generated at 858d366 (pushed)

Diagnostics: 💡 0 new / 5 total note
LevelCodeMessageTargets
Build metadata
Buildbd_76KQ6X9y-mellow-corn
Timestamp2026-09-10T21:07:40.224Z
stlc8413509
Spec hashea1386c23f61
Config hash659c3687c3f0

This comment is auto-generated by stlc and is kept up to date as you push.
If you push new commits, re-run this workflow to update this comment.
Last updated: 2026-09-10 21:08:10 UTC

Adds a content-addressed layer store under images/layers/<blob digest>/.
A layer is unpacked from the shared OCI cache with umoci, converted to the
default disk format, and installed as layer.<format> plus an artifact
record, atomically. One layer shared across images materializes once, and
callers racing on the same layer share a single build through
singleflight, bounded by a manager-level semaphore and a one hour
deadline so a hung read cannot wedge the key forever.

Whiteouts use the representation overlayfs stacking requires: .wh.<name>
becomes a 0:0 character device and .wh..wh..opq becomes a
trusted.overlay.opaque xattr. A startup probe checks the host can create
both; rootless Linux, macOS and filesystems without trusted xattrs skip
per-layer artifacts and compose OCI layers into one rootfs with the
userspace whiteout path, which keeps image contents identical.

Extraction confines every entry to the destination, verifies the
compressed blob digest and the diff ID, and caps the decompressed stream.
Layer artifact bytes are counted with the OCI cache in
TotalOCICacheBytes.
GetBreakdown swallowed per-subsystem errors and reported a zero for the
category that failed, which reads as free disk. It now returns the error.
@chruffins
chruffins force-pushed the hypeship/layer-artifacts branch from 1e65916 to 6157365 Compare September 11, 2026 13:42
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