test: add failing test for evictSourceBlob unlink-vs-store TOCTOU

Adds an instrumentation seam (evictSourceBlobTestHook, fired after the
row-deletion transaction commits and before the content file is
unlinked) and a test that pauses eviction there while a concurrent
StoreSource for identical content bytes races it. Currently red: the
store completes immediately instead of being excluded, which is
exactly the window the review flagged between evictSourceBlob's commit
and its unlink.
This commit is contained in:
2026-08-09 00:45:39 +00:00
parent ea7621de29
commit 90b2f6fa66
3 changed files with 121 additions and 0 deletions

View File

@@ -76,6 +76,19 @@ type Cache struct {
// In-memory cache of variant metadata (content type, size) to avoid reading .meta files
metaCache map[VariantKey]variantMeta
// contentLocks serializes StoreSource and evictSourceBlob per
// content hash, closing the race window between an eviction's row
// deletion and its file unlink against a concurrent store of
// identical content.
contentLocks *contentLock
// evictSourceBlobTestHook, when set, is invoked by evictSourceBlob
// after its row-deletion transaction commits and before the
// content file is unlinked. It exists solely so tests can
// deterministically pause inside that window to exercise
// concurrent stores against it; production code leaves it nil.
evictSourceBlobTestHook func(ContentHash)
}
// NewCache creates a new cache instance.
@@ -94,6 +107,7 @@ func NewCache(db *sql.DB, config CacheConfig) (*Cache, error) {
evictionStop: make(chan struct{}),
evictionDone: make(chan struct{}),
metaCache: make(map[VariantKey]variantMeta),
contentLocks: newContentLock(),
}
if c.disabled {