Skip to content

Commit af8a5b1

Browse files
committed
GODRIVER-2349 Seed all pseudorandom number generators with a crypto-secure random number. (#889)
1 parent dac4668 commit af8a5b1

File tree

6 files changed

+118
-15
lines changed

6 files changed

+118
-15
lines changed

internal/randutil/randutil.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,16 @@
1+
// Copyright (C) MongoDB, Inc. 2022-present.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License"); you may
4+
// not use this file except in compliance with the License. You may obtain
5+
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
6+
17
// Package randutil provides common random number utilities.
28
package randutil
39

410
import (
11+
crand "crypto/rand"
12+
"fmt"
13+
"io"
514
"math/rand"
615
"sync"
716
)
@@ -52,3 +61,17 @@ func (lr *LockedRand) Shuffle(n int, swap func(i, j int)) {
5261
lr.r.Shuffle(n, swap)
5362
lr.mu.Unlock()
5463
}
64+
65+
// CryptoSeed returns a random int64 read from the "crypto/rand" random number generator. It is
66+
// intended to be used to seed pseudorandom number generators at package initialization. It panics
67+
// if it encounters any errors.
68+
func CryptoSeed() int64 {
69+
var b [8]byte
70+
_, err := io.ReadFull(crand.Reader, b[:])
71+
if err != nil {
72+
panic(fmt.Errorf("failed to read 8 bytes from a \"crypto/rand\".Reader: %v", err))
73+
}
74+
75+
return (int64(b[0]) << 0) | (int64(b[1]) << 8) | (int64(b[2]) << 16) | (int64(b[3]) << 24) |
76+
(int64(b[4]) << 32) | (int64(b[5]) << 40) | (int64(b[6]) << 48) | (int64(b[7]) << 56)
77+
}

internal/randutil/randutil_test.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// Copyright (C) MongoDB, Inc. 2022-present.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License"); you may
4+
// not use this file except in compliance with the License. You may obtain
5+
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
6+
7+
package randutil
8+
9+
import (
10+
"testing"
11+
12+
"github.com/stretchr/testify/require"
13+
)
14+
15+
func TestCryptoSeed(t *testing.T) {
16+
seeds := make(map[int64]bool)
17+
for i := 1; i < 1000000; i++ {
18+
s := CryptoSeed()
19+
require.False(t, seeds[s], "CryptoSeed returned a duplicate value %d", s)
20+
seeds[s] = true
21+
}
22+
}

x/mongo/driver/connstring/connstring.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import (
2424
)
2525

2626
// random is a package-global pseudo-random number generator.
27-
var random = randutil.NewLockedRand(rand.NewSource(time.Now().UnixNano()))
27+
var random = randutil.NewLockedRand(rand.NewSource(randutil.CryptoSeed()))
2828

2929
// ParseAndValidate parses the provided URI into a ConnString object.
3030
// It check that all values are valid.

x/mongo/driver/topology/topology.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ var ErrServerSelectionTimeout = errors.New("server selection timeout")
5050
type MonitorMode uint8
5151

5252
// random is a package-global pseudo-random number generator.
53-
var random = randutil.NewLockedRand(rand.NewSource(time.Now().UnixNano()))
53+
var random = randutil.NewLockedRand(rand.NewSource(randutil.CryptoSeed()))
5454

5555
// These constants are the available monitoring modes.
5656
const (

x/mongo/driver/uuid/uuid.go

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,25 +9,24 @@ package uuid // import "go.mongodb.org/mongo-driver/x/mongo/driver/uuid"
99
import (
1010
"io"
1111
"math/rand"
12-
"time"
1312

1413
"go.mongodb.org/mongo-driver/internal/randutil"
1514
)
1615

1716
// UUID represents a UUID.
1817
type UUID [16]byte
1918

20-
// random is a package-global pseudo-random number generator.
21-
var random = randutil.NewLockedRand(rand.NewSource(time.Now().UnixNano()))
19+
// A source is a UUID generator that reads random values from a randutil.LockedRand.
20+
// It is safe to use from multiple goroutines.
21+
type source struct {
22+
random *randutil.LockedRand
23+
}
2224

23-
// New returns a random UUIDv4. It uses a "math/rand" pseudo-random number generator seeded with the
24-
// package initialization time.
25-
//
26-
// New should not be used to generate cryptographically-secure random UUIDs.
27-
func New() (UUID, error) {
25+
// new returns a random UUIDv4 with bytes read from the source's random number generator.
26+
func (s *source) new() (UUID, error) {
2827
var uuid [16]byte
2928

30-
_, err := io.ReadFull(random, uuid[:])
29+
_, err := io.ReadFull(s.random, uuid[:])
3130
if err != nil {
3231
return [16]byte{}, err
3332
}
@@ -37,6 +36,26 @@ func New() (UUID, error) {
3736
return uuid, nil
3837
}
3938

39+
// newGlobalSource returns a source that uses a "math/rand" pseudo-random number generator seeded
40+
// with a cryptographically-secure random number. It is intended to be used to initialize the
41+
// package-global UUID generator.
42+
func newGlobalSource() *source {
43+
return &source{
44+
random: randutil.NewLockedRand(rand.NewSource(randutil.CryptoSeed())),
45+
}
46+
}
47+
48+
// globalSource is a package-global pseudo-random UUID generator.
49+
var globalSource = newGlobalSource()
50+
51+
// New returns a random UUIDv4. It uses a "math/rand" pseudo-random number generator seeded with a
52+
// cryptographically-secure random number at package initialization.
53+
//
54+
// New should not be used to generate cryptographically-secure random UUIDs.
55+
func New() (UUID, error) {
56+
return globalSource.new()
57+
}
58+
4059
// Equal returns true if two UUIDs are equal.
4160
func Equal(a, b UUID) bool {
4261
return a == b

x/mongo/driver/uuid/uuid_test.go

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,56 @@
1+
// Copyright (C) MongoDB, Inc. 2022-present.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License"); you may
4+
// not use this file except in compliance with the License. You may obtain
5+
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
6+
17
package uuid
28

39
import (
10+
"sync"
411
"testing"
512

6-
"github.com/stretchr/testify/assert"
13+
"github.com/stretchr/testify/require"
714
)
815

916
func TestNew(t *testing.T) {
1017
m := make(map[UUID]bool)
11-
for i := 1; i < 100; i++ {
18+
for i := 1; i < 1000000; i++ {
1219
uuid, err := New()
13-
assert.NoError(t, err, "New error")
14-
assert.False(t, m[uuid], "New returned a duplicate UUID %v", uuid)
20+
require.NoError(t, err, "New error")
21+
require.False(t, m[uuid], "New returned a duplicate UUID %v", uuid)
1522
m[uuid] = true
1623
}
1724
}
25+
26+
// GODRIVER-2349
27+
// Test that initializing many package-global UUID sources concurrently never leads to any duplicate
28+
// UUIDs being generated.
29+
func TestGlobalSource(t *testing.T) {
30+
// Create a slice of 1,000 sources and initialize them in 1,000 separate goroutines. The goal is
31+
// to emulate many separate Go driver processes starting at the same time and initializing the
32+
// uuid package at the same time.
33+
sources := make([]*source, 1000)
34+
var wg sync.WaitGroup
35+
for i := range sources {
36+
wg.Add(1)
37+
go func(i int) {
38+
defer wg.Done()
39+
sources[i] = newGlobalSource()
40+
}(i)
41+
}
42+
wg.Wait()
43+
44+
// Read 1,000 UUIDs from each source and assert that there is never a duplicate value, either
45+
// from the same source or from separate sources.
46+
const iterations = 1000
47+
uuids := make(map[UUID]bool, len(sources)*iterations)
48+
for i := 0; i < iterations; i++ {
49+
for j, s := range sources {
50+
uuid, err := s.new()
51+
require.NoError(t, err, "new() error")
52+
require.Falsef(t, uuids[uuid], "source %d returned a duplicate UUID on iteration %d: %v", j, i, uuid)
53+
uuids[uuid] = true
54+
}
55+
}
56+
}

0 commit comments

Comments
 (0)