From 7cb764d22f88abdb8209c28b1017882d6f355400 Mon Sep 17 00:00:00 2001 From: Tonis Tiigi Date: Thu, 5 Mar 2026 16:22:20 -0800 Subject: [PATCH] exporter: fix snapshot GC race during image unpack Use ApplyLayers instead of per-layer ApplyLayer loop to allow recursive parent rebuild when GC collects a parent snapshot between Stat and Prepare calls. Pre-lease the top chain ID snapshot before calling ApplyLayers so that GC cannot collect it during the Stat shortcut path which does not add snapshots to the lease. Fixes #6521 Signed-off-by: Tonis Tiigi --- exporter/containerimage/export.go | 36 +++++++++++++++++++++++++------ 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/exporter/containerimage/export.go b/exporter/containerimage/export.go index bafdf36ff9b8..918b55e7cf29 100644 --- a/exporter/containerimage/export.go +++ b/exporter/containerimage/export.go @@ -501,17 +501,33 @@ func (e *imageExporterInstance) unpackImage(ctx context.Context, img images.Imag ctrdSnapshotter, release := snapshot.NewContainerdSnapshotter(snapshotter) defer release() - var chain []digest.Digest - for _, layer := range layers { - if _, err := rootfs.ApplyLayer(ctx, layer, chain, ctrdSnapshotter, applier); err != nil { - return err + // Compute top chainID so we can add it to the lease before calling ApplyLayers + // as ApplyLayers may directly return after successful Stat call without applying + // layer to the lease and causing error if it gets deleted. + chainID := layersChainID(layers) + if leaseID, ok := leases.FromContext(ctx); ok { + r := leases.Resource{ + ID: chainID.String(), + Type: "snapshots/" + snapshotter.Name(), + } + if err := e.opt.LeaseManager.AddResource(ctx, leases.Lease{ID: leaseID}, r); err != nil { + return errors.Wrapf(err, "failed to lease snapshot %s", chainID) } - chain = append(chain, layer.Diff.Digest) + } + + // note that calling ApplyLayer in a loop here as alternative is not safe because + // single ApplyLayer does not have a safe way to ensure parents are not removed during unpack. + appliedChainID, err := rootfs.ApplyLayers(ctx, layers, ctrdSnapshotter, applier) + if err != nil { + return err + } + if appliedChainID != chainID { + return errors.Errorf("unexpected chain ID mismatch: %s != %s", appliedChainID, chainID) } var ( keyGCLabel = fmt.Sprintf("containerd.io/gc.ref.snapshot.%s", snapshotter.Name()) - valueGCLabel = identity.ChainID(chain).String() + valueGCLabel = chainID.String() ) cinfo := content.Info{ @@ -538,6 +554,14 @@ func getLayers(descs []ocispecs.Descriptor, manifest ocispecs.Manifest) ([]rootf return layers, nil } +func layersChainID(layers []rootfs.Layer) digest.Digest { + chain := make([]digest.Digest, len(layers)) + for i, l := range layers { + chain[i] = l.Diff.Digest + } + return identity.ChainID(chain) +} + func addAnnotations(m map[digest.Digest]map[string]string, desc ocispecs.Descriptor) { if desc.Annotations == nil { return