fix: close TOCTOU window between blob eviction commit and unlink
StoreSource now hashes content itself and holds the per-hash contentLock across the whole store (file write plus accounting row inserts); evictSourceBlob holds the same lock across its whole operation (row deletion transaction through file unlink). A concurrent store and eviction of identical content bytes can no longer interleave: either runs to completion before the other starts, so a fresh row can never be left pointing at a file the other side is mid-unlink on. ContentStorage gains StoreHashed for callers that need the hash before writing; Store is refactored to share the write-if-absent logic with it, with no change to its existing behavior or signature. internal/imgcache/eviction_test.go: TestEvictSourceBlobExcludesConcurrentStoreOfIdenticalContent proves it: pauses eviction (via evictSourceBlobTestHook) in the exact window between commit and unlink, asserts a concurrent StoreSource for identical content blocks rather than completing, then verifies no dangling reference and that the store's data survives once eviction releases the hash.
This commit is contained in:
@@ -291,7 +291,18 @@ type sourceReference struct {
|
||||
// removed together with all of its references, and database rows never
|
||||
// point at deleted files. The JSON metadata sidecars for the removed
|
||||
// rows are deleted afterwards.
|
||||
//
|
||||
// The whole operation holds the content hash's lock (the same one
|
||||
// StoreSource holds for its full store), so a concurrent store of
|
||||
// identical content bytes can never observe the file gone but a row
|
||||
// still present, or insert a fresh row between this transaction's
|
||||
// commit and the file unlink below: it either runs entirely before
|
||||
// this eviction starts, or is blocked until this eviction (row
|
||||
// deletion and unlink together) has fully completed.
|
||||
func (c *Cache) evictSourceBlob(ctx context.Context, contentHash ContentHash) error {
|
||||
unlock := c.contentLocks.Lock(string(contentHash))
|
||||
defer unlock()
|
||||
|
||||
references, err := c.sourceReferences(ctx, contentHash)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
Reference in New Issue
Block a user