From a7b0deaa425fffbb3c29122064df8879a31b703e Mon Sep 17 00:00:00 2001 From: Houmgaor Date: Mon, 16 Feb 2026 18:19:18 +0100 Subject: [PATCH] fix: resolve data race in token.RNG global Wrap *rand.Rand in a mutex-protected SafeRand type to make the global RNG safe for concurrent use across goroutines. The previous bare *rand.Rand caused data races detected by go test -race. --- CHANGELOG.md | 1 + common/mhfitem/mhfitem_test.go | 2 +- common/token/token.go | 34 ++++++++++++++++++++++++++++------ common/token/token_test.go | 29 ++++++++++++++--------------- 4 files changed, 44 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d485c1fa..37b8f92d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fixed save operation ordering - now saves data before session cleanup instead of after - Fixed stale transmog/armor appearance shown to other players - user binary cache now invalidated when plate data is saved - Fixed server crash when Discord relay receives messages with unsupported Shift-JIS characters (emoji, Lenny faces, cuneiform, etc.) +- Fixed data race in token.RNG global used concurrently across goroutines ### Security diff --git a/common/mhfitem/mhfitem_test.go b/common/mhfitem/mhfitem_test.go index c92e561eb..bf0c85d19 100644 --- a/common/mhfitem/mhfitem_test.go +++ b/common/mhfitem/mhfitem_test.go @@ -539,7 +539,7 @@ func TestDiffItemStacks_GeneratesNewWarehouseID(t *testing.T) { } // Reset RNG for consistent test - token.RNG = token.NewRNG() + token.RNG = token.NewSafeRand() result := DiffItemStacks(old, update) if len(result) != 1 { diff --git a/common/token/token.go b/common/token/token.go index decd16893..b4ccfe065 100644 --- a/common/token/token.go +++ b/common/token/token.go @@ -2,10 +2,37 @@ package token import ( "math/rand" + "sync" "time" ) -var RNG = NewRNG() +// SafeRand is a concurrency-safe wrapper around *rand.Rand. +type SafeRand struct { + mu sync.Mutex + rng *rand.Rand +} + +func NewSafeRand() *SafeRand { + return &SafeRand{ + rng: rand.New(rand.NewSource(time.Now().UnixNano())), + } +} + +func (sr *SafeRand) Intn(n int) int { + sr.mu.Lock() + v := sr.rng.Intn(n) + sr.mu.Unlock() + return v +} + +func (sr *SafeRand) Uint32() uint32 { + sr.mu.Lock() + v := sr.rng.Uint32() + sr.mu.Unlock() + return v +} + +var RNG = NewSafeRand() // Generate returns an alphanumeric token of specified length func Generate(length int) string { @@ -16,8 +43,3 @@ func Generate(length int) string { } return string(b) } - -// NewRNG returns a new NewRNG generator -func NewRNG() *rand.Rand { - return rand.New(rand.NewSource(time.Now().UnixNano())) -} diff --git a/common/token/token_test.go b/common/token/token_test.go index 4d7487492..602bccb15 100644 --- a/common/token/token_test.go +++ b/common/token/token_test.go @@ -1,7 +1,6 @@ package token import ( - "math/rand" "testing" "time" ) @@ -134,10 +133,10 @@ func TestGenerate_Distribution(t *testing.T) { } } -func TestNewRNG(t *testing.T) { - rng := NewRNG() +func TestNewSafeRand(t *testing.T) { + rng := NewSafeRand() if rng == nil { - t.Fatal("NewRNG() returned nil") + t.Fatal("NewSafeRand() returned nil") } // Test that it produces different values on subsequent calls @@ -154,7 +153,7 @@ func TestNewRNG(t *testing.T) { } } if same { - t.Error("NewRNG() produced same value 12 times in a row") + t.Error("NewSafeRand() produced same value 12 times in a row") } } } @@ -237,11 +236,11 @@ func TestGenerate_OnlyAlphanumeric(t *testing.T) { } } -func TestNewRNG_DifferentSeeds(t *testing.T) { +func TestNewSafeRand_DifferentSeeds(t *testing.T) { // Create two RNGs at different times and verify they produce different sequences - rng1 := NewRNG() + rng1 := NewSafeRand() time.Sleep(1 * time.Millisecond) // Ensure different seed - rng2 := NewRNG() + rng2 := NewSafeRand() val1 := rng1.Intn(1000000) val2 := rng2.Intn(1000000) @@ -278,15 +277,15 @@ func BenchmarkGenerate_Long(b *testing.B) { } } -func BenchmarkNewRNG(b *testing.B) { +func BenchmarkNewSafeRand(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _ = NewRNG() + _ = NewSafeRand() } } func BenchmarkRNG_Intn(b *testing.B) { - rng := NewRNG() + rng := NewSafeRand() b.ResetTimer() for i := 0; i < b.N; i++ { _ = rng.Intn(62) @@ -294,7 +293,7 @@ func BenchmarkRNG_Intn(b *testing.B) { } func BenchmarkRNG_Uint32(b *testing.B) { - rng := NewRNG() + rng := NewSafeRand() b.ResetTimer() for i := 0; i < b.N; i++ { _ = rng.Uint32() @@ -334,7 +333,7 @@ func TestGenerate_ConsistentCharacterSet(t *testing.T) { } func TestRNG_Type(t *testing.T) { - // Verify RNG is of type *rand.Rand - var _ *rand.Rand = RNG - var _ *rand.Rand = NewRNG() + // Verify RNG is of type *SafeRand + var _ *SafeRand = RNG + var _ *SafeRand = NewSafeRand() }