Fix[entropy]: Implement rand pool to remediate splitmix64 race condition
This commit is contained in:
parent
85943b8762
commit
6f8c0b1a41
|
@ -8,13 +8,52 @@ import (
|
|||
"time"
|
||||
|
||||
"nullprogram.com/x/rng"
|
||||
|
||||
"git.tcp.direct/kayos/common/pool"
|
||||
)
|
||||
|
||||
type randPool struct {
|
||||
sync.Pool
|
||||
}
|
||||
|
||||
func (p *randPool) Get() *rand.Rand {
|
||||
return p.Pool.Get().(*rand.Rand)
|
||||
}
|
||||
|
||||
func (p *randPool) Put(r *rand.Rand) {
|
||||
p.Pool.Put(r)
|
||||
}
|
||||
|
||||
var (
|
||||
lolXD = randPool{
|
||||
Pool: sync.Pool{
|
||||
New: func() interface{} {
|
||||
sm64 := new(rng.SplitMix64)
|
||||
sm64.Seed(GetCryptoSeed())
|
||||
prng := rand.New(sm64) //nolint:gosec
|
||||
return prng
|
||||
},
|
||||
},
|
||||
}
|
||||
hardLocc = &sync.Mutex{}
|
||||
sharedRand *rand.Rand
|
||||
getSharedRand = &sync.Once{}
|
||||
)
|
||||
|
||||
func setSharedRand() {
|
||||
hardLocc.Lock()
|
||||
sharedRand = lolXD.Get()
|
||||
hardLocc.Unlock()
|
||||
}
|
||||
|
||||
func AcquireRand() *rand.Rand {
|
||||
return lolXD.Get()
|
||||
}
|
||||
|
||||
func ReleaseRand(r *rand.Rand) {
|
||||
lolXD.Put(r)
|
||||
}
|
||||
|
||||
// RandomStrChoice returns a random item from an input slice of strings.
|
||||
func RandomStrChoice(choice []string) string {
|
||||
if len(choice) > 0 {
|
||||
|
@ -24,50 +63,51 @@ func RandomStrChoice(choice []string) string {
|
|||
}
|
||||
|
||||
// GetCryptoSeed returns a random int64 derived from crypto/rand.
|
||||
// This can be used as a seed for the math/rand package.
|
||||
// This can be used as a seed for various PRNGs.
|
||||
func GetCryptoSeed() int64 {
|
||||
var seed int64
|
||||
_ = binary.Read(crip.Reader, binary.BigEndian, &seed)
|
||||
return seed
|
||||
}
|
||||
|
||||
// GetOptimizedRand returns a pointer to a *new* rand.Rand which uses crypto/rand to seed a splitmix64 rng.
|
||||
// GetOptimizedRand returns a pointer to a *new* rand.Rand which uses GetCryptoSeed to seed an rng.SplitMix64.
|
||||
// Does not use the global/shared instance of a splitmix64 rng, but instead creates a new one.
|
||||
func GetOptimizedRand() *rand.Rand {
|
||||
r := new(rng.SplitMix64)
|
||||
r.Seed(GetCryptoSeed())
|
||||
return rand.New(r)
|
||||
return rand.New(r) //nolint:gosec
|
||||
}
|
||||
|
||||
// GetSharedRand returns a pointer to our shared optimized rand.Rand which uses crypto/rand to seed a splitmix64 rng.
|
||||
// WARNING - RACY - This is not thread safe, and should only be used in a single-threaded context.
|
||||
func GetSharedRand() *rand.Rand {
|
||||
getSharedRand.Do(func() {
|
||||
sharedRand = GetOptimizedRand()
|
||||
setSharedRand()
|
||||
})
|
||||
return sharedRand
|
||||
}
|
||||
|
||||
// RNGUint32 returns a random uint32 using crypto/rand and splitmix64.
|
||||
func RNGUint32() uint32 {
|
||||
getSharedRand.Do(func() {
|
||||
sharedRand = GetOptimizedRand()
|
||||
})
|
||||
return sharedRand.Uint32()
|
||||
r := lolXD.Get()
|
||||
ui := r.Uint32()
|
||||
lolXD.Put(r)
|
||||
return ui
|
||||
}
|
||||
|
||||
/*
|
||||
RNG returns integer with a maximum amount of 'n' using a global/shared instance of a splitmix64 rng.
|
||||
- Benchmark_FastRandStr5-24 25205089 47.03 ns/op
|
||||
- Benchmark_FastRandStr5-24 25205089 47.03 ns/op
|
||||
- Benchmark_FastRandStr25-24 7113620 169.8 ns/op
|
||||
- Benchmark_FastRandStr55-24 3520297 340.7 ns/op
|
||||
- Benchmark_FastRandStr500-24 414966 2837 ns/op
|
||||
- Benchmark_FastRandStr55555-24 3717 315229 ns/op
|
||||
*/
|
||||
func RNG(n int) int {
|
||||
getSharedRand.Do(func() {
|
||||
sharedRand = GetOptimizedRand()
|
||||
})
|
||||
return sharedRand.Intn(n)
|
||||
r := lolXD.Get()
|
||||
i := r.Intn(n)
|
||||
lolXD.Put(r)
|
||||
return i
|
||||
}
|
||||
|
||||
// OneInA generates a random number with a maximum of 'million' (input int).
|
||||
|
@ -88,22 +128,29 @@ func RandSleepMS(n int) {
|
|||
const charset = "abcdefghijklmnopqrstuvwxyz1234567890"
|
||||
const charsetWithUpper = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890"
|
||||
|
||||
var strBufs = pool.NewStringFactory()
|
||||
|
||||
// RandStr generates a random alphanumeric string with a max length of size.
|
||||
// Alpha charset used is a-z all lowercase.
|
||||
func RandStr(size int) string {
|
||||
buf := make([]byte, size)
|
||||
for i := 0; i != size; i++ {
|
||||
buf[i] = charset[GetOptimizedRand().Uint32()%uint32(len(charset))]
|
||||
}
|
||||
return string(buf)
|
||||
return randStr(charset, size)
|
||||
}
|
||||
|
||||
// RandStrWithUpper generates a random alphanumeric string with a max length of size.
|
||||
// Alpha charset used is a-Z mixed case.
|
||||
func RandStrWithUpper(size int) string {
|
||||
buf := make([]byte, size)
|
||||
for i := 0; i != size; i++ {
|
||||
buf[i] = charsetWithUpper[uint32(RNG(62))%uint32(len(charsetWithUpper))]
|
||||
}
|
||||
return string(buf)
|
||||
return randStr(charsetWithUpper, size)
|
||||
}
|
||||
|
||||
func randStr(chars string, size int) string {
|
||||
buf := strBufs.Get()
|
||||
r := lolXD.Get()
|
||||
for i := 0; i != size; i++ {
|
||||
ui32 := int(r.Uint32())
|
||||
_, _ = buf.WriteRune(rune(chars[ui32%len(chars)]))
|
||||
}
|
||||
lolXD.Put(r)
|
||||
s := buf.String()
|
||||
strBufs.MustPut(buf)
|
||||
return s
|
||||
}
|
||||
|
|
|
@ -8,7 +8,8 @@ import (
|
|||
|
||||
var dupCount = 0
|
||||
|
||||
func check[T comparable](zero T, one T, t *testing.T) {
|
||||
func check[T comparable](t *testing.T, zero T, one T) {
|
||||
t.Helper()
|
||||
if zero == one {
|
||||
dupCount++
|
||||
t.Errorf("hit a duplicate! %v == %v", zero, one)
|
||||
|
@ -17,11 +18,14 @@ func check[T comparable](zero T, one T, t *testing.T) {
|
|||
}
|
||||
|
||||
func Test_RNG(t *testing.T) {
|
||||
t.Parallel()
|
||||
// for coverage
|
||||
sharedRand = GetSharedRand()
|
||||
setSharedRand()
|
||||
RandSleepMS(5)
|
||||
hardLocc.Lock()
|
||||
sharedRand = nil
|
||||
getSharedRand = &sync.Once{}
|
||||
hardLocc.Unlock()
|
||||
// - - - - - -
|
||||
if OneInA(1000000) {
|
||||
println(string([]byte{
|
||||
|
@ -31,13 +35,14 @@ func Test_RNG(t *testing.T) {
|
|||
}))
|
||||
}
|
||||
|
||||
for n := 0; n != 500; n++ {
|
||||
check(RNG(55555), RNG(55555), t)
|
||||
check(RNGUint32(), RNGUint32(), t)
|
||||
for n := 0; n != 55555; n++ {
|
||||
check(t, RNG(123454321), RNG(123454321))
|
||||
check(t, RNGUint32(), RNGUint32())
|
||||
}
|
||||
}
|
||||
|
||||
func Test_OneInA(t *testing.T) {
|
||||
t.Parallel()
|
||||
for n := 0; n < 100; n++ {
|
||||
yes := ""
|
||||
if OneInA(1) {
|
||||
|
@ -49,42 +54,48 @@ func Test_OneInA(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func randStrChecks(zero, one string, t *testing.T, intendedLength int) {
|
||||
func randStrChecks(t *testing.T, zero, one string, intendedLength int) {
|
||||
t.Helper()
|
||||
if len(zero) != len(one) {
|
||||
t.Fatalf("RandStr output length inconsistency, len(zero) is %d but wanted len(one) which is %d", len(zero), len(one))
|
||||
}
|
||||
if len(zero) != intendedLength || len(one) != intendedLength {
|
||||
t.Fatalf("RandStr output length inconsistency, len(zero) is %d and len(one) is %d, but both should have been 55", len(zero), len(one))
|
||||
t.Fatalf(
|
||||
"RandStr output length inconsistency, "+
|
||||
"len(zero) is %d and len(one) is %d, but both should have been 55", len(zero), len(one))
|
||||
}
|
||||
check(zero, one, t)
|
||||
check(t, zero, one)
|
||||
}
|
||||
|
||||
func Test_RandStr(t *testing.T) {
|
||||
t.Parallel()
|
||||
for n := 0; n != 500; n++ {
|
||||
zero := RandStr(55)
|
||||
one := RandStr(55)
|
||||
t.Logf("Random0: %s Random1: %s", zero, one)
|
||||
randStrChecks(zero, one, t, 55)
|
||||
randStrChecks(t, zero, one, 55)
|
||||
}
|
||||
t.Logf("[SUCCESS] RandStr had no collisions")
|
||||
}
|
||||
|
||||
func Test_RandStrWithUpper(t *testing.T) {
|
||||
t.Parallel()
|
||||
for n := 0; n != 500; n++ {
|
||||
zero := RandStrWithUpper(15)
|
||||
one := RandStrWithUpper(15)
|
||||
t.Logf("Random0: %s Random1: %s", zero, one)
|
||||
randStrChecks(zero, one, t, 15)
|
||||
randStrChecks(t, zero, one, 15)
|
||||
}
|
||||
t.Logf("[SUCCESS] RandStr had no collisions")
|
||||
}
|
||||
|
||||
func Test_RandStr_Entropy(t *testing.T) {
|
||||
t.Parallel()
|
||||
var totalScore = 0
|
||||
for n := 0; n != 500; n++ {
|
||||
zero := RandStr(55)
|
||||
one := RandStr(55)
|
||||
randStrChecks(zero, one, t, 55)
|
||||
randStrChecks(t, zero, one, 55)
|
||||
zeroSplit := strings.Split(zero, "")
|
||||
oneSplit := strings.Split(one, "")
|
||||
var similarity = 0
|
||||
|
@ -93,10 +104,10 @@ func Test_RandStr_Entropy(t *testing.T) {
|
|||
continue
|
||||
}
|
||||
similarity++
|
||||
// t.Logf("[-] zeroSplit[%d] is the same as oneSplit[%d] (%s)", i, i, char)
|
||||
}
|
||||
if similarity*4 > 55 {
|
||||
t.Errorf("[ENTROPY FAILURE] more than a quarter of the string is the same!\n zero: %s \n one: %s \nTotal similar: %d",
|
||||
t.Errorf("[ENTROPY FAILURE] more than a quarter of the string is the same!\n "+
|
||||
"zero: %s \n one: %s \nTotal similar: %d",
|
||||
zero, one, similarity)
|
||||
}
|
||||
// t.Logf("[ENTROPY] Similarity score (lower is better): %d", similarity)
|
||||
|
@ -106,6 +117,7 @@ func Test_RandStr_Entropy(t *testing.T) {
|
|||
}
|
||||
|
||||
func Test_RandomStrChoice(t *testing.T) {
|
||||
t.Parallel()
|
||||
if RandomStrChoice([]string{}) != "" {
|
||||
t.Fatalf("RandomStrChoice returned a value when given an empty slice")
|
||||
}
|
||||
|
@ -113,12 +125,13 @@ func Test_RandomStrChoice(t *testing.T) {
|
|||
for n := 0; n != 500; n++ {
|
||||
slice = append(slice, RandStr(555))
|
||||
}
|
||||
check(RandomStrChoice(slice), RandomStrChoice(slice), t)
|
||||
check(t, RandomStrChoice(slice), RandomStrChoice(slice))
|
||||
}
|
||||
|
||||
func Test_RNGUint32(t *testing.T) {
|
||||
t.Parallel()
|
||||
// start globals fresh, just for coverage.
|
||||
sharedRand = GetOptimizedRand()
|
||||
setSharedRand()
|
||||
getSharedRand = &sync.Once{}
|
||||
RNGUint32()
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue