Skip to content

Commit ae2f8af

Browse files
giuseppeclaude
andcommitted
copy: detect zstd:chunked sentinel layer and skip full-digest mitigation during pull
When pulling an image, the storage destination's FilterLayers method detects the zstd:chunked sentinel layer, marks it as empty, and internally records that SkipMitigation() should be called to bypass unnecessary full-digest verification. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Giuseppe Scrivano <gscrivan@redhat.com>
1 parent 38ad280 commit ae2f8af

3 files changed

Lines changed: 82 additions & 13 deletions

File tree

image/copy/single.go

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -458,6 +458,19 @@ func (ic *imageCopier) copyLayers(ctx context.Context) ([]compressiontypes.Algor
458458
}
459459
manifestLayerInfos := man.LayerInfos()
460460

461+
// Let the destination filter manifest layer infos before copying.
462+
// Destinations that support it (e.g., c/storage) may detect storage-specific
463+
// markers and return indices of layers that should be skipped.
464+
type layerFilter interface {
465+
FilterLayers([]manifest.LayerInfo) []int
466+
}
467+
filteredLayers := set.New[int]()
468+
if f, ok := ic.c.dest.(layerFilter); ok {
469+
for _, idx := range f.FilterLayers(manifestLayerInfos) {
470+
filteredLayers.Add(idx)
471+
}
472+
}
473+
461474
// copyGroup is used to determine if all layers are copied
462475
copyGroup := sync.WaitGroup{}
463476

@@ -466,7 +479,10 @@ func (ic *imageCopier) copyLayers(ctx context.Context) ([]compressiontypes.Algor
466479
defer ic.c.concurrentBlobCopiesSemaphore.Release(1)
467480
defer copyGroup.Done()
468481
cld := copyLayerData{}
469-
if !ic.c.options.DownloadForeignLayers && ic.c.dest.AcceptsForeignLayerURLs() && len(srcLayer.URLs) != 0 {
482+
if manifestLayerInfos[index].EmptyLayer || filteredLayers.Contains(index) {
483+
cld.destInfo = srcLayer
484+
logrus.Debugf("Skipping layer %q", srcLayer.Digest)
485+
} else if !ic.c.options.DownloadForeignLayers && ic.c.dest.AcceptsForeignLayerURLs() && len(srcLayer.URLs) != 0 {
470486
// DiffIDs are, currently, needed only when converting from schema1.
471487
// In which case src.LayerInfos will not have URLs because schema1
472488
// does not support them.

image/storage/storage_dest.go

Lines changed: 48 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -57,15 +57,20 @@ type storageImageDestination struct {
5757
stubs.AlwaysSupportsSignatures
5858

5959
imageRef storageReference
60-
directory string // Temporary directory where we store blobs until Commit() time
61-
nextTempFileID atomic.Int32 // A counter that we use for computing filenames to assign to blobs
62-
manifest []byte // (Per-instance) manifest contents, or nil if not yet known.
63-
manifestMIMEType string // Valid if manifest != nil
64-
manifestDigest digest.Digest // Valid if manifest != nil
65-
untrustedDiffIDValues []digest.Digest // From config’s RootFS.DiffIDs (not even validated to be valid digest.Digest!); or nil if not read yet
66-
signatures []byte // Signature contents, temporary
67-
signatureses map[digest.Digest][]byte // Instance signature contents, temporary
68-
metadata storageImageMetadata // Metadata contents being built
60+
directory string // Temporary directory where we store blobs until Commit() time
61+
nextTempFileID atomic.Int32 // A counter that we use for computing filenames to assign to blobs
62+
manifest []byte // (Per-instance) manifest contents, or nil if not yet known.
63+
manifestMIMEType string // Valid if manifest != nil
64+
manifestDigest digest.Digest // Valid if manifest != nil
65+
untrustedDiffIDValues []digest.Digest // From config’s RootFS.DiffIDs (not even validated to be valid digest.Digest!); or nil if not read yet
66+
// filteredLayerIndices are layer indices marked empty by FilterLayers.
67+
// When set, the full-digest mitigation is skipped for remaining layers,
68+
// and falling back from partial to ordinary layer download is refused
69+
// (because the mitigation would be the only safety net for that path).
70+
filteredLayerIndices []int
71+
signatures []byte // Signature contents, temporary
72+
signatureses map[digest.Digest][]byte // Instance signature contents, temporary
73+
metadata storageImageMetadata // Metadata contents being built
6974

7075
// Mapping from layer (by index) to the associated ID in the storage.
7176
// It's protected *implicitly* since `commitLayer()`, at any given
@@ -221,6 +226,17 @@ func (s *storageImageDestination) NoteOriginalOCIConfig(ociConfig *imgspecv1.Ima
221226
return nil
222227
}
223228

229+
// FilterLayers inspects the manifest layer infos and returns indices of layers
230+
// that should be skipped. For c/storage, this detects the zstd:chunked
231+
// sentinel layer and records that the full-digest mitigation can be skipped.
232+
func (s *storageImageDestination) FilterLayers(layerInfos []manifest.LayerInfo) []int {
233+
if len(layerInfos) > 0 && layerInfos[0].Digest == toc.ZstdChunkedSentinelDigest {
234+
s.filteredLayerIndices = []int{0}
235+
return s.filteredLayerIndices
236+
}
237+
return nil
238+
}
239+
224240
// PutBlobWithOptions writes contents of stream and returns data representing the result.
225241
// inputInfo.Digest can be optionally provided if known; if provided, and stream is read to the end without error, the digest MUST match the stream contents.
226242
// inputInfo.Size is the expected length of stream, if known.
@@ -396,6 +412,9 @@ func (s *storageImageDestination) PutBlobPartial(ctx context.Context, chunkAcces
396412
return private.UploadedBlob{}, fmt.Errorf("internal error: in PutBlobPartial, untrustedLayerDiffID returned errUntrustedLayerDiffIDNotYetAvailable")
397413
case errors.As(err, &diffIDUnknownErr):
398414
if inputTOCDigest != nil {
415+
if len(s.filteredLayerIndices) > 0 {
416+
return private.UploadedBlob{}, fmt.Errorf("zstd:chunked sentinel present, refusing fallback to ordinary layer download: %w", err)
417+
}
399418
return private.UploadedBlob{}, private.NewErrFallbackToOrdinaryLayerDownload(err)
400419
}
401420
untrustedDiffID = "" // A schema1 image or a non-TOC layer with no ambiguity, let it through
@@ -415,6 +434,10 @@ func (s *storageImageDestination) PutBlobPartial(ctx context.Context, chunkAcces
415434
defer func() {
416435
var perr chunked.ErrFallbackToOrdinaryLayerDownload
417436
if errors.As(retErr, &perr) {
437+
if len(s.filteredLayerIndices) > 0 {
438+
retErr = fmt.Errorf("zstd:chunked sentinel present, refusing fallback to ordinary layer download: %w", retErr)
439+
return
440+
}
418441
retErr = private.NewErrFallbackToOrdinaryLayerDownload(retErr)
419442
}
420443
}()
@@ -424,6 +447,16 @@ func (s *storageImageDestination) PutBlobPartial(ctx context.Context, chunkAcces
424447
return private.UploadedBlob{}, err
425448
}
426449
defer differ.Close()
450+
if len(s.filteredLayerIndices) > 0 {
451+
// Skipping the mitigation is safe because the sentinel layer guarantees
452+
// that only TOC-aware clients will process this image. With the mitigation
453+
// skipped, we also refuse any fallback to ordinary layer download (see the
454+
// checks above and the deferred error handler).
455+
logrus.Debugf("Sentinel detected for layer %s: skipping full-digest mitigation", srcInfo.Digest)
456+
if err := chunked.SkipMitigation(differ); err != nil {
457+
return private.UploadedBlob{}, err
458+
}
459+
}
427460

428461
out, err := s.imageRef.transport.store.PrepareStagedLayer(nil, differ)
429462
if err != nil {
@@ -795,7 +828,7 @@ func (s *storageImageDestination) computeID(m manifest.Manifest) (string, error)
795828
case *manifest.Schema1:
796829
// Build a list of the diffIDs we've generated for the non-throwaway FS layers
797830
for i, li := range layerInfos {
798-
if li.EmptyLayer {
831+
if li.EmptyLayer || slices.Contains(s.filteredLayerIndices, i) {
799832
continue
800833
}
801834
trusted, ok := s.trustedLayerIdentityDataLocked(i, li.Digest)
@@ -839,6 +872,9 @@ func (s *storageImageDestination) computeID(m manifest.Manifest) (string, error)
839872
tocIDInput := ""
840873
hasLayerPulledByTOC := false
841874
for i, li := range layerInfos {
875+
if li.EmptyLayer || slices.Contains(s.filteredLayerIndices, i) {
876+
continue
877+
}
842878
trusted, ok := s.trustedLayerIdentityDataLocked(i, li.Digest)
843879
if !ok { // We have already committed all layers if we get to this point, so the data must have been available.
844880
return "", fmt.Errorf("internal inconsistency: layer (%d, %q) not found", i, li.Digest)
@@ -1453,9 +1489,10 @@ func (s *storageImageDestination) CommitWithOptions(ctx context.Context, options
14531489

14541490
// Extract, commit, or find the layers.
14551491
for i, blob := range layerBlobs {
1492+
isFiltered := slices.Contains(s.filteredLayerIndices, i)
14561493
if stopQueue, err := s.commitLayer(i, addedLayerInfo{
14571494
digest: blob.Digest,
1458-
emptyLayer: blob.EmptyLayer,
1495+
emptyLayer: blob.EmptyLayer || isFiltered,
14591496
}, blob.Size); err != nil {
14601497
return err
14611498
} else if stopQueue {

image/storage/storage_src.go

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -352,10 +352,26 @@ func (s *storageImageSource) LayerInfosForCopy(ctx context.Context, instanceDige
352352
}
353353
slices.Reverse(physicalBlobInfos)
354354

355-
res, err := buildLayerInfosForCopy(man.LayerInfos(), physicalBlobInfos, gzipCompressedLayerType)
355+
manifestLayerInfos := man.LayerInfos()
356+
// The zstd:chunked sentinel layer was never stored physically;
357+
// strip it before matching against physical layers, then prepend its blob info to the result.
358+
var sentinelBlobInfo *types.BlobInfo
359+
if len(manifestLayerInfos) > 0 && manifestLayerInfos[0].Digest == toc.ZstdChunkedSentinelDigest {
360+
sentinelBlobInfo = &types.BlobInfo{
361+
Digest: manifestLayerInfos[0].Digest,
362+
Size: int64(len(toc.ZstdChunkedSentinelContent)),
363+
MediaType: manifestLayerInfos[0].MediaType,
364+
}
365+
manifestLayerInfos = manifestLayerInfos[1:]
366+
}
367+
368+
res, err := buildLayerInfosForCopy(manifestLayerInfos, physicalBlobInfos, gzipCompressedLayerType)
356369
if err != nil {
357370
return nil, fmt.Errorf("creating LayerInfosForCopy of image %q: %w", s.image.ID, err)
358371
}
372+
if sentinelBlobInfo != nil {
373+
res = append([]types.BlobInfo{*sentinelBlobInfo}, res...)
374+
}
359375
return res, nil
360376
}
361377

0 commit comments

Comments
 (0)