From 5e4fd5234bfadcf81af36899486d35b95b00014b Mon Sep 17 00:00:00 2001 From: Vladimir Saraikin Date: Sat, 11 Jul 2026 20:23:32 +0200 Subject: [PATCH] fix: lock clock sequence in NewV6WithTime NewV6WithTime called getTime directly, bypassing timeMu, while every other caller (GetTime, used by NewV6/NewUUID) holds it. getTime reads and mutates the shared lasttime/clockSeq state, so concurrent NewV6WithTime calls raced and produced duplicate UUIDs. Take timeMu around getTime. --- version6.go | 5 +++++ version6_test.go | 44 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/version6.go b/version6.go index 17bbafe..1e49e0c 100644 --- a/version6.go +++ b/version6.go @@ -37,7 +37,12 @@ func NewV6() (UUID, error) { // are generating multiple UUIDs, it is recommended to increment the time. // If getTime fails to return the current NewV6WithTime returns Nil and an error. func NewV6WithTime(customTime *time.Time) (UUID, error) { + // getTime reads and mutates the shared clock sequence state, which is + // guarded by timeMu (see GetTime). Take the lock so concurrent generation + // stays race-free and keeps producing unique values. + timeMu.Lock() now, seq, err := getTime(customTime) + timeMu.Unlock() if err != nil { return Nil, err } diff --git a/version6_test.go b/version6_test.go index 690c09d..6764d1c 100644 --- a/version6_test.go +++ b/version6_test.go @@ -1,6 +1,7 @@ package uuid import ( + "sync" "testing" "time" ) @@ -66,6 +67,49 @@ func TestNewV6FromTimeGeneratesUniqueUUIDs(t *testing.T) { } } +func TestNewV6WithTimeConcurrentUnique(t *testing.T) { + // NewV6WithTime must take the clock-sequence lock so concurrent generation + // for the same timestamp stays race-free and keeps producing unique values. + // For a fixed timestamp the UUID varies only by the clock sequence, so up + // to 16384 calls must all be unique. Run with -race to also surface the + // underlying data race directly. + fixed := time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC) + + const goroutines = 8 + const perGoroutine = 2000 // 16000 total < 16384 clock-sequence values + + var wg sync.WaitGroup + var mu sync.Mutex + seen := make(map[UUID]struct{}, goroutines*perGoroutine) + dups := 0 + + for g := 0; g < goroutines; g++ { + wg.Add(1) + go func() { + defer wg.Done() + for i := 0; i < perGoroutine; i++ { + id, err := NewV6WithTime(&fixed) + if err != nil { + t.Errorf("NewV6WithTime returned unexpected error %v", err) + return + } + mu.Lock() + if _, ok := seen[id]; ok { + dups++ + } else { + seen[id] = struct{}{} + } + mu.Unlock() + } + }() + } + wg.Wait() + + if dups != 0 { + t.Errorf("got %d duplicate V6 UUIDs from concurrent NewV6WithTime calls", dups) + } +} + func BenchmarkNewV6WithTime(b *testing.B) { b.RunParallel(func(pb *testing.PB) { for pb.Next() {