Skip to content

Fix data race on chunks shared by DedupQueue#372

Merged
folbricht merged 1 commit into
masterfrom
dedupqueue-chunk-race
Jul 9, 2026
Merged

Fix data race on chunks shared by DedupQueue#372
folbricht merged 1 commit into
masterfrom
dedupqueue-chunk-race

Conversation

@folbricht

Copy link
Copy Markdown
Owner

DedupQueue returns the same *Chunk pointer to every caller waiting on a coalesced GetChunk request. Chunk.Data() and Chunk.ID() are lazy memoizers that write the chunk's internal fields without synchronization, so two concurrent callers that trigger the storage-to-plain conversion race on c.data/c.id. Reachable today on a read-only chunk-server whose served format differs from the upstream store format (e.g. chunk-server -u over a compressed store), and it becomes the standard configuration once chunk encryption (#371) lands, since an encrypting server always has a converter mismatch with its store.

Fix: hand every caller a shallow copy of the chunk. The copies share the underlying read-only data/storage slices — no chunk data is duplicated — while the lazily-calculated fields become private to each caller. The master chunk held by the in-flight request is never handed out directly, so it stays untouched while waiters read it; the leader publishes it via markDone (channel close) before anyone copies it, which provides the necessary happens-before edge.

The added test requests the same chunk from 10 goroutines and materializes the plain data in each: without the fix it fails under -race (write/write race on c.data) and on the shared-pointer assertion; with the fix the full library suite passes under -race.

DedupQueue hands the same *Chunk pointer to every caller waiting on a
deduplicated GetChunk request. Chunk lazily materializes its plain data
and ID on first access without any locking, so concurrent callers that
trigger that conversion race on the chunk's internal fields. This is
reachable on a read-only chunk-server whenever the served format differs
from the store format, e.g. an uncompressed server backed by a
compressed store.

Give every caller, including the one that performed the upstream
request, a shallow copy of the chunk instead. The copies share the
underlying read-only data and storage slices, so no chunk data is
duplicated; only the lazily-calculated fields become private to each
caller. The chunk held by the in-flight request is never handed out
directly and stays untouched while waiters read from it.

The new test fails with -race (and on the shared-pointer assertion)
without this fix.
@folbricht folbricht merged commit 52f335f into master Jul 9, 2026
3 checks passed
folbricht added a commit that referenced this pull request Jul 10, 2026
WriteDedupQueue.GetChunk hands the chunk of an in-flight StoreChunk
request for the same ID to every waiting reader. The chunk published
in the request is the one the storing caller passed in and keeps
using, and Chunk lazily materializes its plain data without locking,
so concurrent readers race with each other and with the writer on the
chunk's internal fields.

Apply the same fix as for DedupQueue.GetChunk in #372: StoreChunk
publishes a shallow copy detached from the caller's chunk, and every
waiting reader receives its own copy of that. The copies share the
underlying read-only data, so no chunk data is duplicated.

The new test fails with -race (and on the shared-pointer assertion)
without this fix.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant