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.
This commit is contained in:
Houmgaor
2026-02-16 18:19:18 +01:00
parent 9b69564c49
commit a7b0deaa42
4 changed files with 44 additions and 22 deletions

View File

@@ -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 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 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 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 ### Security

View File

@@ -539,7 +539,7 @@ func TestDiffItemStacks_GeneratesNewWarehouseID(t *testing.T) {
} }
// Reset RNG for consistent test // Reset RNG for consistent test
token.RNG = token.NewRNG() token.RNG = token.NewSafeRand()
result := DiffItemStacks(old, update) result := DiffItemStacks(old, update)
if len(result) != 1 { if len(result) != 1 {

View File

@@ -2,10 +2,37 @@ package token
import ( import (
"math/rand" "math/rand"
"sync"
"time" "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 // Generate returns an alphanumeric token of specified length
func Generate(length int) string { func Generate(length int) string {
@@ -16,8 +43,3 @@ func Generate(length int) string {
} }
return string(b) return string(b)
} }
// NewRNG returns a new NewRNG generator
func NewRNG() *rand.Rand {
return rand.New(rand.NewSource(time.Now().UnixNano()))
}

View File

@@ -1,7 +1,6 @@
package token package token
import ( import (
"math/rand"
"testing" "testing"
"time" "time"
) )
@@ -134,10 +133,10 @@ func TestGenerate_Distribution(t *testing.T) {
} }
} }
func TestNewRNG(t *testing.T) { func TestNewSafeRand(t *testing.T) {
rng := NewRNG() rng := NewSafeRand()
if rng == nil { if rng == nil {
t.Fatal("NewRNG() returned nil") t.Fatal("NewSafeRand() returned nil")
} }
// Test that it produces different values on subsequent calls // Test that it produces different values on subsequent calls
@@ -154,7 +153,7 @@ func TestNewRNG(t *testing.T) {
} }
} }
if same { 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 // Create two RNGs at different times and verify they produce different sequences
rng1 := NewRNG() rng1 := NewSafeRand()
time.Sleep(1 * time.Millisecond) // Ensure different seed time.Sleep(1 * time.Millisecond) // Ensure different seed
rng2 := NewRNG() rng2 := NewSafeRand()
val1 := rng1.Intn(1000000) val1 := rng1.Intn(1000000)
val2 := rng2.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() b.ResetTimer()
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
_ = NewRNG() _ = NewSafeRand()
} }
} }
func BenchmarkRNG_Intn(b *testing.B) { func BenchmarkRNG_Intn(b *testing.B) {
rng := NewRNG() rng := NewSafeRand()
b.ResetTimer() b.ResetTimer()
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
_ = rng.Intn(62) _ = rng.Intn(62)
@@ -294,7 +293,7 @@ func BenchmarkRNG_Intn(b *testing.B) {
} }
func BenchmarkRNG_Uint32(b *testing.B) { func BenchmarkRNG_Uint32(b *testing.B) {
rng := NewRNG() rng := NewSafeRand()
b.ResetTimer() b.ResetTimer()
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
_ = rng.Uint32() _ = rng.Uint32()
@@ -334,7 +333,7 @@ func TestGenerate_ConsistentCharacterSet(t *testing.T) {
} }
func TestRNG_Type(t *testing.T) { func TestRNG_Type(t *testing.T) {
// Verify RNG is of type *rand.Rand // Verify RNG is of type *SafeRand
var _ *rand.Rand = RNG var _ *SafeRand = RNG
var _ *rand.Rand = NewRNG() var _ *SafeRand = NewSafeRand()
} }