From 97e7b90f40d4ae1e1e07a50f194f812ca8d18834 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 15 Apr 2026 19:47:01 +0000 Subject: [PATCH] fix: guard against nil chunk in bucket.Get to prevent panic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When cache data is loaded from a corrupted file, map entries may point to chunk indices that are allocated but nil (beyond the saved chunksLen). The existing bounds check verified chunkIdx against len(chunks) but did not verify the chunk itself was non-nil before slicing it, which could cause a runtime panic instead of being counted as a corruption miss. Add a nil check for the chunk immediately after retrieval from the chunks slice, before any slice expressions are evaluated. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- fastcache.go | 6 ++++-- fastcache_test.go | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/fastcache.go b/fastcache.go index 9358a2b..c977bf4 100644 --- a/fastcache.go +++ b/fastcache.go @@ -393,9 +393,12 @@ func (b *bucket) Get(dst, k []byte, h uint64, returnDst bool) ([]byte, bool) { goto end } chunk := chunks[chunkIdx] + if chunk == nil { + atomic.AddUint64(&b.corruptions, 1) + goto end + } idx %= chunkSize if idx+4 >= chunkSize { - // Corrupted data during the load from file. Just skip it. atomic.AddUint64(&b.corruptions, 1) goto end } @@ -404,7 +407,6 @@ func (b *bucket) Get(dst, k []byte, h uint64, returnDst bool) ([]byte, bool) { valLen := (uint64(kvLenBuf[2]) << 8) | uint64(kvLenBuf[3]) idx += 4 if idx+keyLen+valLen >= chunkSize { - // Corrupted data during the load from file. Just skip it. atomic.AddUint64(&b.corruptions, 1) goto end } diff --git a/fastcache_test.go b/fastcache_test.go index 24dce7e..ea5c3cb 100644 --- a/fastcache_test.go +++ b/fastcache_test.go @@ -6,6 +6,8 @@ import ( "sync" "testing" "time" + + xxhash "github.com/cespare/xxhash/v2" ) func TestCacheSmall(t *testing.T) { @@ -222,6 +224,38 @@ func testCacheGetSet(c *Cache, itemsCount int) error { return nil } +func TestBucketGetNilChunkNoCorruptionPanic(t *testing.T) { + var b bucket + b.Init(64 * 1024) + + k := []byte("testkey") + v := []byte("testvalue") + h := xxhash.Sum64(k) + b.Set(k, v, h) + + got, found := b.Get(nil, k, h, true) + if !found || string(got) != "testvalue" { + t.Fatalf("expected to find key before corruption; found=%v val=%q", found, got) + } + + b.mu.Lock() + mapVal := b.m[h] + chunkIdx := (mapVal & ((1 << bucketSizeBits) - 1)) / chunkSize + b.chunks[chunkIdx] = nil + b.mu.Unlock() + + got, found = b.Get(nil, k, h, true) + if found { + t.Fatalf("expected miss for nil chunk, but got found=true val=%q", got) + } + + var s Stats + b.UpdateStats(&s) + if s.Corruptions != 1 { + t.Fatalf("expected 1 corruption; got %d", s.Corruptions) + } +} + func TestCacheResetUpdateStatsSetConcurrent(t *testing.T) { c := New(12334)