Browse code

image: cache image identity signature lookups

Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com>

Kevin Alvarez authored on 2026/02/26 00:31:11
Showing 5 changed files
1 1
new file mode 100644
... ...
@@ -0,0 +1,41 @@
0
+package identitycache
1
+
2
+import (
3
+	"context"
4
+	"time"
5
+
6
+	imagetypes "github.com/moby/moby/api/types/image"
7
+)
8
+
9
+// Entry contains a persisted image signature cache record.
10
+type Entry struct {
11
+	CachedAt  time.Time
12
+	ExpiresAt time.Time
13
+	Signature *imagetypes.SignatureIdentity
14
+}
15
+
16
+// Backend is a persistent storage backend for image signature cache entries.
17
+type Backend interface {
18
+	Load(ctx context.Context, cacheKey string, now time.Time) (Entry, bool, error)
19
+	Store(ctx context.Context, cacheKey string, entry Entry, now time.Time) error
20
+	Close() error
21
+}
22
+
23
+type nopBackend struct{}
24
+
25
+// NewNopBackend returns a backend that never persists or returns entries.
26
+func NewNopBackend() Backend {
27
+	return nopBackend{}
28
+}
29
+
30
+func (nopBackend) Load(context.Context, string, time.Time) (Entry, bool, error) {
31
+	return Entry{}, false, nil
32
+}
33
+
34
+func (nopBackend) Store(context.Context, string, Entry, time.Time) error {
35
+	return nil
36
+}
37
+
38
+func (nopBackend) Close() error {
39
+	return nil
40
+}
0 41
new file mode 100644
... ...
@@ -0,0 +1,180 @@
0
+package identitycache
1
+
2
+import (
3
+	"context"
4
+	"crypto/rand"
5
+	"encoding/json"
6
+	"math/big"
7
+	"os"
8
+	"path/filepath"
9
+	"sync"
10
+	"time"
11
+
12
+	boltdb "github.com/moby/buildkit/util/db"
13
+	"github.com/moby/buildkit/util/db/boltutil"
14
+	bolt "go.etcd.io/bbolt"
15
+)
16
+
17
+var bboltCacheBucket = []byte("image-identity-cache-v1")
18
+
19
+const (
20
+	// 15m matches the shortest cache TTL (imageIdentityErrorCacheTTL), so
21
+	// prune won't lag far behind shortest-lived entries.
22
+	pruneIntervalMin    = 15 * time.Minute
23
+	pruneIntervalSpread = 15 * time.Minute
24
+)
25
+
26
+type boltBackend struct {
27
+	db        boltdb.DB
28
+	closeOnce sync.Once
29
+	closeErr  error
30
+	stopPrune chan struct{}
31
+	pruneDone chan struct{}
32
+}
33
+
34
+// NewBoltDBBackend creates a bbolt-backed persistent cache backend.
35
+func NewBoltDBBackend(root string) (Backend, error) {
36
+	if root == "" {
37
+		return NewNopBackend(), nil
38
+	}
39
+	cacheDir := filepath.Join(root, "image")
40
+	if err := os.MkdirAll(cacheDir, 0o700); err != nil {
41
+		return nil, err
42
+	}
43
+	db, err := boltutil.SafeOpen(filepath.Join(cacheDir, "identity-cache.db"), 0o600, nil)
44
+	if err != nil {
45
+		return nil, err
46
+	}
47
+	b := &boltBackend{db: db}
48
+	if err := b.db.Update(func(tx *bolt.Tx) error {
49
+		_, err := tx.CreateBucketIfNotExists(bboltCacheBucket)
50
+		return err
51
+	}); err != nil {
52
+		_ = db.Close()
53
+		return nil, err
54
+	}
55
+	b.startPrune()
56
+	return b, nil
57
+}
58
+
59
+func (b *boltBackend) Load(_ context.Context, cacheKey string, now time.Time) (Entry, bool, error) {
60
+	var (
61
+		entry   Entry
62
+		payload []byte
63
+	)
64
+	err := b.db.View(func(tx *bolt.Tx) error {
65
+		bucket := tx.Bucket(bboltCacheBucket)
66
+		if bucket == nil {
67
+			return nil
68
+		}
69
+		value := bucket.Get([]byte(cacheKey))
70
+		if value == nil {
71
+			return nil
72
+		}
73
+		payload = append([]byte(nil), value...)
74
+		return nil
75
+	})
76
+	if err != nil {
77
+		return Entry{}, false, err
78
+	}
79
+	if len(payload) == 0 {
80
+		return Entry{}, false, nil
81
+	}
82
+	if err := json.Unmarshal(payload, &entry); err != nil {
83
+		_ = b.delete(cacheKey)
84
+		return Entry{}, false, nil
85
+	}
86
+	if now.After(entry.ExpiresAt) {
87
+		if err := b.delete(cacheKey); err != nil {
88
+			return Entry{}, false, err
89
+		}
90
+		return Entry{}, false, nil
91
+	}
92
+	return entry, true, nil
93
+}
94
+
95
+func (b *boltBackend) Store(_ context.Context, cacheKey string, entry Entry, _ time.Time) error {
96
+	payload, err := json.Marshal(entry)
97
+	if err != nil {
98
+		return err
99
+	}
100
+	return b.db.Update(func(tx *bolt.Tx) error {
101
+		bucket, err := tx.CreateBucketIfNotExists(bboltCacheBucket)
102
+		if err != nil {
103
+			return err
104
+		}
105
+		return bucket.Put([]byte(cacheKey), payload)
106
+	})
107
+}
108
+
109
+func (b *boltBackend) Close() error {
110
+	if b == nil || b.db == nil {
111
+		return nil
112
+	}
113
+	b.closeOnce.Do(func() {
114
+		if b.stopPrune != nil {
115
+			close(b.stopPrune)
116
+		}
117
+		if b.pruneDone != nil {
118
+			<-b.pruneDone
119
+		}
120
+		b.closeErr = b.db.Close()
121
+	})
122
+	return b.closeErr
123
+}
124
+
125
+func (b *boltBackend) delete(cacheKey string) error {
126
+	return b.db.Update(func(tx *bolt.Tx) error {
127
+		bucket := tx.Bucket(bboltCacheBucket)
128
+		if bucket == nil {
129
+			return nil
130
+		}
131
+		return bucket.Delete([]byte(cacheKey))
132
+	})
133
+}
134
+
135
+func (b *boltBackend) startPrune() {
136
+	b.stopPrune = make(chan struct{})
137
+	b.pruneDone = make(chan struct{})
138
+	go func() {
139
+		defer close(b.pruneDone)
140
+		timer := time.NewTimer(nextPruneDelay())
141
+		defer timer.Stop()
142
+		for {
143
+			select {
144
+			case <-b.stopPrune:
145
+				return
146
+			case <-timer.C:
147
+				_ = b.pruneExpiredEntries(time.Now().UTC())
148
+				timer.Reset(nextPruneDelay())
149
+			}
150
+		}
151
+	}()
152
+}
153
+
154
+func nextPruneDelay() time.Duration {
155
+	n, err := rand.Int(rand.Reader, big.NewInt(int64(pruneIntervalSpread)))
156
+	if err != nil {
157
+		return pruneIntervalMin
158
+	}
159
+	return pruneIntervalMin + time.Duration(n.Int64())
160
+}
161
+
162
+func (b *boltBackend) pruneExpiredEntries(now time.Time) error {
163
+	return b.db.Update(func(tx *bolt.Tx) error {
164
+		bucket := tx.Bucket(bboltCacheBucket)
165
+		if bucket == nil {
166
+			return nil
167
+		}
168
+		cursor := bucket.Cursor()
169
+		for key, value := cursor.First(); key != nil; key, value = cursor.Next() {
170
+			var entry Entry
171
+			if err := json.Unmarshal(value, &entry); err != nil || now.After(entry.ExpiresAt) {
172
+				if err := cursor.Delete(); err != nil {
173
+					return err
174
+				}
175
+			}
176
+		}
177
+		return nil
178
+	})
179
+}
... ...
@@ -4,34 +4,121 @@ import (
4 4
 	"cmp"
5 5
 	"context"
6 6
 	"encoding/json"
7
+	"net"
8
+	"net/url"
7 9
 	"slices"
8 10
 	"strings"
11
+	"sync"
9 12
 	"time"
10 13
 
11 14
 	"github.com/containerd/containerd/v2/core/content"
15
+	c8dimages "github.com/containerd/containerd/v2/core/images"
12 16
 	"github.com/containerd/containerd/v2/core/remotes"
13 17
 	"github.com/containerd/containerd/v2/pkg/labels"
14 18
 	"github.com/containerd/log"
19
+	"github.com/containerd/platforms"
15 20
 	"github.com/distribution/reference"
16 21
 	imagetypes "github.com/moby/moby/api/types/image"
22
+	"github.com/moby/moby/v2/daemon/containerd/identitycache"
17 23
 	"github.com/moby/moby/v2/daemon/internal/builder-next/exporter"
18 24
 	policyimage "github.com/moby/policy-helpers/image"
19 25
 	policytypes "github.com/moby/policy-helpers/types"
20 26
 	"github.com/opencontainers/go-digest"
21 27
 	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
22 28
 	"github.com/pkg/errors"
29
+	"golang.org/x/sync/singleflight"
23 30
 )
24 31
 
32
+const (
33
+	imageIdentityCacheTTL      = 48 * time.Hour
34
+	imageIdentityErrorCacheTTL = 15 * time.Minute
35
+	imageIdentityWarmupTimeout = 2 * time.Minute
36
+)
37
+
38
+type imageIdentityCacheEntry = identitycache.Entry
39
+
40
+type imageIdentityState struct {
41
+	flight     singleflight.Group
42
+	cacheMu    sync.Mutex
43
+	cache      map[string]imageIdentityCacheEntry
44
+	cacheStore identitycache.Backend
45
+}
46
+
25 47
 func (i *ImageService) imageIdentity(ctx context.Context, desc ocispec.Descriptor, multi *multiPlatformSummary) (*imagetypes.Identity, error) {
48
+	return i.imageIdentityWithCachePolicy(ctx, desc, multi, true)
49
+}
50
+
51
+func (i *ImageService) imageIdentityFromCache(ctx context.Context, desc ocispec.Descriptor, multi *multiPlatformSummary) (*imagetypes.Identity, error) {
52
+	return i.imageIdentityWithCachePolicy(ctx, desc, multi, false)
53
+}
54
+
55
+func (i *ImageService) imageIdentityWithCachePolicy(ctx context.Context, desc ocispec.Descriptor, multi *multiPlatformSummary, computeOnCacheMiss bool) (*imagetypes.Identity, error) {
26 56
 	info, err := i.content.Info(ctx, desc.Digest)
27 57
 	if err != nil {
28 58
 		return nil, err
29 59
 	}
30
-	identity := &imagetypes.Identity{}
31 60
 
61
+	identity := imageIdentityFromLabels(ctx, info.Labels)
62
+	bestDigest, bestPlatform := imageIdentityBestMatch(multi)
63
+	cacheKey := imageIdentityCacheKey(desc.Digest.String(), bestDigest, bestPlatform)
64
+	signature, ok, err := i.imageSignatureIdentityFromCache(ctx, cacheKey)
65
+	if err != nil {
66
+		log.G(ctx).WithError(err).WithField("image", desc.Digest).Debug("failed to load image identity cache entry")
67
+	}
68
+	if !ok && computeOnCacheMiss {
69
+		v, err, _ := i.identity.flight.Do(cacheKey, func() (any, error) {
70
+			if cached, ok, err := i.imageSignatureIdentityFromCache(ctx, cacheKey); err == nil && ok {
71
+				return cached, nil
72
+			} else if err != nil {
73
+				log.G(ctx).WithError(err).WithField("image", desc.Digest).Debug("failed to refresh image identity cache entry")
74
+			}
75
+
76
+			computedSignature, hasTransientVerificationError := i.computeSignatureIdentity(ctx, desc, multi)
77
+			ttl := imageIdentityCacheTTL
78
+			if hasTransientVerificationError {
79
+				// signature verification errors can be temporary (e.g. no network),
80
+				// so cache these for a shorter period
81
+				ttl = imageIdentityErrorCacheTTL
82
+			}
83
+			if err := i.updateImageIdentityCache(ctx, cacheKey, computedSignature, ttl); err != nil {
84
+				log.G(ctx).WithError(err).WithField("image", desc.Digest).Debug("failed to update image identity cache entry")
85
+			}
86
+
87
+			return computedSignature, nil
88
+		})
89
+		if err != nil {
90
+			return nil, err
91
+		}
92
+		cachedSignature, ok := v.(*imagetypes.SignatureIdentity)
93
+		if !ok {
94
+			return nil, errors.Errorf("unexpected cached signature identity type %T", v)
95
+		}
96
+		signature = cachedSignature
97
+	}
98
+
99
+	if signature != nil {
100
+		identity.Signature = append(identity.Signature, *signature)
101
+	}
102
+
103
+	if len(identity.Build) == 0 && len(identity.Pull) == 0 && len(identity.Signature) == 0 {
104
+		return nil, nil
105
+	}
106
+
107
+	return identity, nil
108
+}
109
+
110
+func imageIdentityBestMatch(multi *multiPlatformSummary) (bestDigest string, bestPlatform string) {
111
+	if multi == nil || multi.Best == nil {
112
+		return "", ""
113
+	}
114
+	return multi.Best.Target().Digest.String(), platforms.FormatAll(multi.BestPlatform)
115
+}
116
+
117
+func imageIdentityFromLabels(ctx context.Context, labelsByDigest map[string]string) *imagetypes.Identity {
118
+	identity := &imagetypes.Identity{}
32 119
 	seenRepos := make(map[string]struct{})
33 120
 
34
-	for k, v := range info.Labels {
121
+	for k, v := range labelsByDigest {
35 122
 		if ref, ok := strings.CutPrefix(k, exporter.BuildRefLabel); ok {
36 123
 			var val exporter.BuildRefLabelValue
37 124
 			if err := json.Unmarshal([]byte(v), &val); err == nil {
... ...
@@ -64,26 +151,185 @@ func (i *ImageService) imageIdentity(ctx context.Context, desc ocispec.Descripto
64 64
 		}
65 65
 	}
66 66
 
67
-	if multi.Best != nil {
68
-		si, err := i.signatureIdentity(ctx, desc, multi.Best, multi.BestPlatform)
69
-		if err != nil {
70
-			log.G(ctx).WithError(err).Error("failed to validate image signature")
67
+	slices.SortFunc(identity.Build, func(a, b imagetypes.BuildIdentity) int {
68
+		return cmp.Compare(a.Ref, b.Ref)
69
+	})
70
+
71
+	return identity
72
+}
73
+
74
+func (i *ImageService) computeSignatureIdentity(ctx context.Context, desc ocispec.Descriptor, multi *multiPlatformSummary) (*imagetypes.SignatureIdentity, bool) {
75
+	if multi == nil || multi.Best == nil {
76
+		return nil, false
77
+	}
78
+
79
+	signatureIdentity, err := i.signatureIdentity(ctx, desc, multi.Best, multi.BestPlatform)
80
+	if err != nil {
81
+		log.G(ctx).WithError(err).Error("failed to validate image signature")
82
+		return nil, signatureVerificationErrorIsTransient(err)
83
+	}
84
+
85
+	// verification errors are represented as a payload field. Treat only
86
+	// network-like errors as transient so deterministic verification failures
87
+	// remain cached with the normal TTL
88
+	hasTransientVerificationError := signatureIdentity != nil && signatureVerificationMessageIsTransient(signatureIdentity.Error)
89
+	return signatureIdentity, hasTransientVerificationError
90
+}
91
+
92
+func signatureVerificationErrorIsTransient(err error) bool {
93
+	if err == nil {
94
+		return false
95
+	}
96
+	if errors.Is(err, context.DeadlineExceeded) || errors.Is(err, context.Canceled) {
97
+		return true
98
+	}
99
+
100
+	var netErr net.Error
101
+	if errors.As(err, &netErr) {
102
+		if netErr.Timeout() {
103
+			return true
71 104
 		}
72
-		if si != nil {
73
-			identity.Signature = append(identity.Signature, *si)
105
+	}
106
+
107
+	var dnsErr *net.DNSError
108
+	if errors.As(err, &dnsErr) {
109
+		return true
110
+	}
111
+
112
+	var urlErr *url.Error
113
+	if errors.As(err, &urlErr) {
114
+		return signatureVerificationErrorIsTransient(urlErr.Err)
115
+	}
116
+
117
+	return signatureVerificationMessageIsTransient(err.Error())
118
+}
119
+
120
+func signatureVerificationMessageIsTransient(msg string) bool {
121
+	msg = strings.ToLower(msg)
122
+	// TODO: replace message-based transient detection with structured error
123
+	//  classification from policy-helpers / signature verification (e.g. typed
124
+	//  retryable errors), so cache TTL decisions do not depend on string
125
+	//  matching.
126
+	for _, transient := range []string{
127
+		"context deadline exceeded",
128
+		"i/o timeout",
129
+		"tls handshake timeout",
130
+		"no such host",
131
+		"temporary failure in name resolution",
132
+		"connection refused",
133
+		"connection reset by peer",
134
+		"network is unreachable",
135
+		"dial tcp",
136
+	} {
137
+		if strings.Contains(msg, transient) {
138
+			return true
74 139
 		}
75 140
 	}
141
+	return false
142
+}
76 143
 
77
-	// return nil if there is no identity information
78
-	if len(identity.Build) == 0 && len(identity.Pull) == 0 && len(identity.Signature) == 0 {
79
-		return nil, nil
144
+func imageIdentityCacheKey(imageDigest, bestDigest, bestPlatform string) string {
145
+	return strings.Join([]string{imageDigest, bestDigest, bestPlatform}, "|")
146
+}
147
+
148
+func (i *ImageService) imageSignatureIdentityFromCache(ctx context.Context, cacheKey string) (*imagetypes.SignatureIdentity, bool, error) {
149
+	now := time.Now()
150
+
151
+	i.identity.cacheMu.Lock()
152
+	if cached, ok := i.identity.cache[cacheKey]; ok {
153
+		if now.After(cached.ExpiresAt) {
154
+			delete(i.identity.cache, cacheKey)
155
+		} else {
156
+			i.identity.cacheMu.Unlock()
157
+			return cloneSignatureIdentity(cached.Signature), true, nil
158
+		}
80 159
 	}
160
+	i.identity.cacheMu.Unlock()
81 161
 
82
-	slices.SortFunc(identity.Build, func(a, b imagetypes.BuildIdentity) int {
83
-		return cmp.Compare(a.Ref, b.Ref)
84
-	})
162
+	if i.identity.cacheStore == nil {
163
+		return nil, false, nil
164
+	}
165
+	cached, ok, err := i.identity.cacheStore.Load(ctx, cacheKey, now)
166
+	if err != nil {
167
+		return nil, false, err
168
+	}
169
+	if !ok {
170
+		return nil, false, nil
171
+	}
85 172
 
86
-	return identity, nil
173
+	i.identity.cacheMu.Lock()
174
+	if i.identity.cache == nil {
175
+		i.identity.cache = map[string]imageIdentityCacheEntry{}
176
+	}
177
+	i.identity.cache[cacheKey] = cached
178
+	i.identity.cacheMu.Unlock()
179
+	return cloneSignatureIdentity(cached.Signature), true, nil
180
+}
181
+
182
+func (i *ImageService) updateImageIdentityCache(ctx context.Context, cacheKey string, signature *imagetypes.SignatureIdentity, ttl time.Duration) error {
183
+	if ttl <= 0 {
184
+		return nil
185
+	}
186
+
187
+	now := time.Now()
188
+	entry := imageIdentityCacheEntry{
189
+		CachedAt:  now,
190
+		ExpiresAt: now.Add(ttl),
191
+		Signature: cloneSignatureIdentity(signature),
192
+	}
193
+
194
+	i.identity.cacheMu.Lock()
195
+	if i.identity.cache == nil {
196
+		i.identity.cache = map[string]imageIdentityCacheEntry{}
197
+	}
198
+	i.identity.cache[cacheKey] = entry
199
+	pruneImageIdentityCacheEntries(i.identity.cache, now)
200
+	i.identity.cacheMu.Unlock()
201
+
202
+	if i.identity.cacheStore == nil {
203
+		return nil
204
+	}
205
+	return i.identity.cacheStore.Store(ctx, cacheKey, entry, now)
206
+}
207
+
208
+func cloneSignatureIdentity(s *imagetypes.SignatureIdentity) *imagetypes.SignatureIdentity {
209
+	if s == nil {
210
+		return nil
211
+	}
212
+	out := *s
213
+	out.Timestamps = slices.Clone(s.Timestamps)
214
+	out.Warnings = slices.Clone(s.Warnings)
215
+	if s.Signer != nil {
216
+		signer := *s.Signer
217
+		out.Signer = &signer
218
+	}
219
+	return &out
220
+}
221
+
222
+func pruneImageIdentityCacheEntries(entries map[string]imageIdentityCacheEntry, now time.Time) {
223
+	for key, entry := range entries {
224
+		if now.After(entry.ExpiresAt) {
225
+			delete(entries, key)
226
+		}
227
+	}
228
+}
229
+
230
+func (i *ImageService) warmImageIdentityCache(ctx context.Context, img c8dimages.Image) {
231
+	if i.policyVerifier == nil {
232
+		return
233
+	}
234
+	go func() {
235
+		warmCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), imageIdentityWarmupTimeout)
236
+		defer cancel()
237
+		multi, err := i.multiPlatformSummary(warmCtx, img, matchAnyWithPreference(platforms.Default(), nil))
238
+		if err != nil {
239
+			log.G(warmCtx).WithError(err).WithField("image", img.Name).Debug("failed to build image identity cache in background")
240
+			return
241
+		}
242
+		if _, err := i.imageIdentity(warmCtx, img.Target, multi); err != nil {
243
+			log.G(warmCtx).WithError(err).WithField("image", img.Name).Debug("failed to build image identity cache in background")
244
+		}
245
+	}()
87 246
 }
88 247
 
89 248
 func (i *ImageService) signatureIdentity(ctx context.Context, desc ocispec.Descriptor, img *ImageManifest, platform ocispec.Platform) (*imagetypes.SignatureIdentity, error) {
... ...
@@ -101,6 +347,10 @@ func (i *ImageService) signatureIdentity(ctx context.Context, desc ocispec.Descr
101 101
 		return nil, nil
102 102
 	}
103 103
 
104
+	if i.policyVerifier == nil {
105
+		return nil, nil
106
+	}
107
+
104 108
 	v, err := i.policyVerifier()
105 109
 	if err != nil {
106 110
 		return nil, err
... ...
@@ -15,6 +15,7 @@ import (
15 15
 	"github.com/containerd/log"
16 16
 	"github.com/containerd/platforms"
17 17
 	"github.com/moby/moby/v2/daemon/container"
18
+	"github.com/moby/moby/v2/daemon/containerd/identitycache"
18 19
 	daemonevents "github.com/moby/moby/v2/daemon/events"
19 20
 	dimages "github.com/moby/moby/v2/daemon/images"
20 21
 	"github.com/moby/moby/v2/daemon/internal/distribution"
... ...
@@ -42,6 +43,7 @@ type ImageService struct {
42 42
 	refCountMounter     snapshotter.Mounter
43 43
 	idMapping           user.IdentityMapping
44 44
 	policyVerifier      func() (*policyverifier.Verifier, error)
45
+	identity            imageIdentityState
45 46
 
46 47
 	// defaultPlatformOverride is used in tests to override the host platform.
47 48
 	defaultPlatformOverride platforms.MatchComparer
... ...
@@ -51,6 +53,7 @@ type ImageServiceConfig struct {
51 51
 	Client                 *containerd.Client
52 52
 	Containers             container.Store
53 53
 	Snapshotter            string
54
+	IdentityCacheBackend   identitycache.Backend
54 55
 	RegistryHosts          docker.RegistryHosts
55 56
 	Registry               distribution.RegistryResolver
56 57
 	EventsService          *daemonevents.Events
... ...
@@ -76,6 +79,15 @@ func NewService(config ImageServiceConfig) *ImageService {
76 76
 		refCountMounter: config.RefCountMounter,
77 77
 		idMapping:       config.IDMapping,
78 78
 		policyVerifier:  config.PolicyVerifierProvider,
79
+		identity: imageIdentityState{
80
+			cache: make(map[string]imageIdentityCacheEntry),
81
+			cacheStore: func() identitycache.Backend {
82
+				if config.IdentityCacheBackend != nil {
83
+					return config.IdentityCacheBackend
84
+				}
85
+				return identitycache.NewNopBackend()
86
+			}(),
87
+		},
79 88
 	}
80 89
 }
81 90
 
... ...
@@ -132,6 +144,9 @@ func (i *ImageService) GetLayerMountID(cid string) (string, error) {
132 132
 // Cleanup resources before the process is shutdown.
133 133
 // called from daemon.go Daemon.Shutdown()
134 134
 func (i *ImageService) Cleanup() error {
135
+	if i.identity.cacheStore != nil {
136
+		return i.identity.cacheStore.Close()
137
+	}
135 138
 	return nil
136 139
 }
137 140
 
... ...
@@ -53,6 +53,7 @@ import (
53 53
 	"github.com/moby/moby/v2/daemon/config"
54 54
 	"github.com/moby/moby/v2/daemon/container"
55 55
 	ctrd "github.com/moby/moby/v2/daemon/containerd"
56
+	"github.com/moby/moby/v2/daemon/containerd/identitycache"
56 57
 	"github.com/moby/moby/v2/daemon/containerd/migration"
57 58
 	"github.com/moby/moby/v2/daemon/events"
58 59
 	_ "github.com/moby/moby/v2/daemon/graphdriver/register" // register graph drivers
... ...
@@ -1273,11 +1274,17 @@ func NewDaemon(ctx context.Context, config *config.Config, pluginStore *plugin.S
1273 1273
 		if err := configureKernelSecuritySupport(&cfgStore.Config, driverName); err != nil {
1274 1274
 			return nil, err
1275 1275
 		}
1276
+		identityCacheBackend, err := identitycache.NewBoltDBBackend(config.Root)
1277
+		if err != nil {
1278
+			log.G(ctx).WithError(err).Warn("failed to initialize image identity bbolt cache backend")
1279
+			identityCacheBackend = identitycache.NewNopBackend()
1280
+		}
1276 1281
 		d.usesSnapshotter = true
1277 1282
 		d.imageService = ctrd.NewService(ctrd.ImageServiceConfig{
1278 1283
 			Client:                 d.containerdClient,
1279 1284
 			Containers:             d.containers,
1280 1285
 			Snapshotter:            driverName,
1286
+			IdentityCacheBackend:   identityCacheBackend,
1281 1287
 			RegistryHosts:          d.RegistryHosts,
1282 1288
 			Registry:               d.registryService,
1283 1289
 			EventsService:          d.EventsService,