Skip to content

Commit 78df4a1

Browse files
angles-n-daemonsyuzefovich
authored andcommitted
security: fix memory accounting issue in client certificate cache
The client certificate cache is a simple utility cache which keeps track of the nearest certificate expiration times by user. It does this by reading in certificates as they are used, and then updating the gauge associated with the user so that our operators can alert on expiring certificates. It has been identified that there is a memory accounting issue with this utility class, in that if we see the same certificate multiple times, we report that our cache is growing for each one. In reality however, though we may change the reference in our cache, the old value will be cleaned up, and memory should remain relatively low. This PR both fixes the accounting bug, and adds a limit so that the client certificate cache can't negatively affect SQL operation. Epic: none Fixes: #151040 Release note (bug fix): Fixes a memory accounting issue with the client certificate cache, where we were accidentally reporting multiple allocations for the same memory.
1 parent 33a92c1 commit 78df4a1

File tree

2 files changed

+31
-6
lines changed

2 files changed

+31
-6
lines changed

pkg/security/clientcert/cert_expiry_cache.go

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,13 @@ var ClientCertExpirationCacheCapacity = settings.RegisterIntSetting(
3333
1000,
3434
settings.WithPublic)
3535

36+
var certCacheMemLimit = settings.RegisterByteSizeSetting(
37+
settings.ApplicationLevel,
38+
"security.client_cert.cache_memory_limit",
39+
"memory limit for the client certificate expiration cache",
40+
1<<29, // 512MiB
41+
)
42+
3643
// certInfo holds information about a certificate, including its expiration
3744
// time and the last time it was seen.
3845
type certInfo struct {
@@ -89,8 +96,9 @@ func NewClientCertExpirationCache(
8996
}
9097

9198
c.mu.cache = make(map[string]map[string]certInfo)
99+
limit := certCacheMemLimit.Get(&st.SV)
92100
c.mon = mon.NewMonitorInheritWithLimit(
93-
"client-expiration-cache", 0 /* limit */, parentMon, true, /* longLiving */
101+
"client-expiration-cache", limit, parentMon, true, /* longLiving */
94102
)
95103
c.mu.acc = c.mon.MakeBoundAccount()
96104
c.mon.StartNoReserved(ctx, parentMon)
@@ -158,16 +166,19 @@ func (c *ClientCertExpirationCache) MaybeUpsert(
158166
if _, ok := c.mu.cache[user]; !ok {
159167
err := c.mu.acc.Grow(ctx, 2*GaugeSize)
160168
if err != nil {
161-
log.Ops.Warningf(ctx, "no memory available to cache cert expiry: %v", err)
169+
log.Warningf(ctx, "no memory available to cache cert expiry: %v", err)
162170
return
163171
}
164172
c.mu.cache[user] = map[string]certInfo{}
165173
}
166174

167-
err := c.mu.acc.Grow(ctx, CertInfoSize)
168-
if err != nil {
169-
log.Warningf(ctx, "no memory available to cache cert expiry: %v", err)
170-
c.evictLocked(ctx, user, serial)
175+
// if the serial hasn't been seen, report it in the memory accounting.
176+
if _, ok := c.mu.cache[user][serial]; !ok {
177+
err := c.mu.acc.Grow(ctx, CertInfoSize)
178+
if err != nil {
179+
log.Warningf(ctx, "no memory available to cache cert expiry: %v", err)
180+
return
181+
}
171182
}
172183

173184
// insert / update the certificate expiration time.

pkg/security/clientcert/cert_expiry_cache_test.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -371,6 +371,7 @@ func TestAllocationTracking(t *testing.T) {
371371
cache.MaybeUpsert(ctx, "user1", "serial2", 120)
372372
cache.MaybeUpsert(ctx, "user2", "serial3", 65)
373373
clock.Advance(time.Minute)
374+
require.Equal(t, (3*certInfoSize)+(4*gaugeSize), account.Used())
374375
cache.Purge(ctx)
375376
cache.MaybeUpsert(ctx, "user2", "serial4", 75)
376377
require.Equal(t, certInfoSize+(2*gaugeSize), account.Used())
@@ -383,9 +384,22 @@ func TestAllocationTracking(t *testing.T) {
383384
cache.MaybeUpsert(ctx, "user1", "serial2", 120)
384385
cache.MaybeUpsert(ctx, "user2", "serial3", 65)
385386
cache.MaybeUpsert(ctx, "user2", "serial4", 75)
387+
require.Equal(t, (4*certInfoSize)+(4*gaugeSize), account.Used())
386388
cache.Clear()
387389
require.Equal(t, int64(0), account.Used())
388390
})
391+
392+
t.Run("overwriting an existing certificate does not change the reported memory allocation", func(t *testing.T) {
393+
cache := newCache(ctx, clock, stopper)
394+
account := cache.Account()
395+
require.Equal(t, int64(0), account.Used())
396+
cache.MaybeUpsert(ctx, "user1", "serial1", 100)
397+
require.Equal(t, certInfoSize+(2*gaugeSize), account.Used())
398+
cache.MaybeUpsert(ctx, "user1", "serial1", 100)
399+
require.Equal(t, certInfoSize+(2*gaugeSize), account.Used())
400+
cache.MaybeUpsert(ctx, "user1", "serial1", 100)
401+
require.Equal(t, certInfoSize+(2*gaugeSize), account.Used())
402+
})
389403
}
390404

391405
func TestGetExpiration(t *testing.T) {

0 commit comments

Comments
 (0)