Skip to content

Commit d68a8d0

Browse files
zx2c4gopherbot
authored andcommitted
crypto/rand: batch and buffer calls to getrandom/getentropy
We're using bufio to batch reads of /dev/urandom to 4k, but we weren't doing the same on newer platforms with getrandom/getentropy. Since the overhead is the same for these -- one syscall -- we should batch reads of these into the same 4k buffer. While we're at it, we can simplify a lot of the constant dispersal. This also adds a new test case to make sure the buffering works as desired. Change-Id: I7297d4aa795c00712e6484b841cef8650c2be4ef Reviewed-on: https://go-review.googlesource.com/c/go/+/370894 Reviewed-by: Filippo Valsorda <[email protected]> Run-TryBot: Jason Donenfeld <[email protected]> Auto-Submit: Jason Donenfeld <[email protected]> TryBot-Result: Gopher Robot <[email protected]> Reviewed-by: Ian Lance Taylor <[email protected]>
1 parent e858c14 commit d68a8d0

File tree

8 files changed

+120
-89
lines changed

8 files changed

+120
-89
lines changed

src/crypto/rand/rand_batched_test.go

Lines changed: 43 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,24 @@
22
// Use of this source code is governed by a BSD-style
33
// license that can be found in the LICENSE file.
44

5-
//go:build linux || freebsd || dragonfly || solaris
5+
//go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris
66

77
package rand
88

99
import (
1010
"bytes"
11+
"encoding/binary"
12+
"errors"
13+
prand "math/rand"
1114
"testing"
1215
)
1316

1417
func TestBatched(t *testing.T) {
15-
fillBatched := batched(func(p []byte) bool {
18+
fillBatched := batched(func(p []byte) error {
1619
for i := range p {
1720
p[i] = byte(i)
1821
}
19-
return true
22+
return nil
2023
}, 5)
2124

2225
p := make([]byte, 13)
@@ -29,16 +32,49 @@ func TestBatched(t *testing.T) {
2932
}
3033
}
3134

35+
func TestBatchedBuffering(t *testing.T) {
36+
var prandSeed [8]byte
37+
Read(prandSeed[:])
38+
prand.Seed(int64(binary.LittleEndian.Uint64(prandSeed[:])))
39+
40+
backingStore := make([]byte, 1<<23)
41+
prand.Read(backingStore)
42+
backingMarker := backingStore[:]
43+
output := make([]byte, len(backingStore))
44+
outputMarker := output[:]
45+
46+
fillBatched := batched(func(p []byte) error {
47+
n := copy(p, backingMarker)
48+
backingMarker = backingMarker[n:]
49+
return nil
50+
}, 731)
51+
52+
for len(outputMarker) > 0 {
53+
max := 9200
54+
if max > len(outputMarker) {
55+
max = len(outputMarker)
56+
}
57+
howMuch := prand.Intn(max + 1)
58+
if !fillBatched(outputMarker[:howMuch]) {
59+
t.Fatal("batched function returned false")
60+
}
61+
outputMarker = outputMarker[howMuch:]
62+
}
63+
if !bytes.Equal(backingStore, output) {
64+
t.Error("incorrect batch result")
65+
}
66+
}
67+
3268
func TestBatchedError(t *testing.T) {
33-
b := batched(func(p []byte) bool { return false }, 5)
69+
b := batched(func(p []byte) error { return errors.New("failure") }, 5)
3470
if b(make([]byte, 13)) {
35-
t.Fatal("batched function should have returned false")
71+
t.Fatal("batched function should have returned an error")
3672
}
3773
}
3874

3975
func TestBatchedEmpty(t *testing.T) {
40-
b := batched(func(p []byte) bool { return false }, 5)
76+
b := batched(func(p []byte) error { return errors.New("failure") }, 5)
4177
if !b(make([]byte, 0)) {
42-
t.Fatal("empty slice should always return true")
78+
t.Fatal("empty slice should always return successful")
4379
}
4480
}

src/crypto/rand/rand_dragonfly.go

Lines changed: 0 additions & 9 deletions
This file was deleted.

src/crypto/rand/rand_freebsd.go

Lines changed: 0 additions & 9 deletions
This file was deleted.

src/crypto/rand/rand_getentropy.go

Lines changed: 2 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,25 +6,9 @@
66

77
package rand
88

9-
import (
10-
"internal/syscall/unix"
11-
)
9+
import "internal/syscall/unix"
1210

1311
func init() {
14-
altGetRandom = getEntropy
15-
}
16-
17-
func getEntropy(p []byte) (ok bool) {
1812
// getentropy(2) returns a maximum of 256 bytes per call
19-
for i := 0; i < len(p); i += 256 {
20-
end := i + 256
21-
if len(p) < end {
22-
end = len(p)
23-
}
24-
err := unix.GetEntropy(p[i:end])
25-
if err != nil {
26-
return false
27-
}
28-
}
29-
return true
13+
altGetRandom = batched(unix.GetEntropy, 256)
3014
}

src/crypto/rand/rand_batched.go renamed to src/crypto/rand/rand_getrandom.go

Lines changed: 23 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,25 +8,25 @@ package rand
88

99
import (
1010
"internal/syscall/unix"
11+
"runtime"
12+
"syscall"
1113
)
1214

13-
// maxGetRandomRead is platform dependent.
1415
func init() {
15-
altGetRandom = batched(getRandomBatch, maxGetRandomRead)
16-
}
17-
18-
// batched returns a function that calls f to populate a []byte by chunking it
19-
// into subslices of, at most, readMax bytes.
20-
func batched(f func([]byte) bool, readMax int) func([]byte) bool {
21-
return func(buf []byte) bool {
22-
for len(buf) > readMax {
23-
if !f(buf[:readMax]) {
24-
return false
25-
}
26-
buf = buf[readMax:]
27-
}
28-
return len(buf) == 0 || f(buf)
16+
var maxGetRandomRead int
17+
switch runtime.GOOS {
18+
case "linux", "android":
19+
// Per the manpage:
20+
// When reading from the urandom source, a maximum of 33554431 bytes
21+
// is returned by a single call to getrandom() on systems where int
22+
// has a size of 32 bits.
23+
maxGetRandomRead = (1 << 25) - 1
24+
case "freebsd", "dragonfly", "solaris":
25+
maxGetRandomRead = 1 << 8
26+
default:
27+
panic("no maximum specified for GetRandom")
2928
}
29+
altGetRandom = batched(getRandom, maxGetRandomRead)
3030
}
3131

3232
// If the kernel is too old to support the getrandom syscall(),
@@ -36,7 +36,13 @@ func batched(f func([]byte) bool, readMax int) func([]byte) bool {
3636
// If the kernel supports the getrandom() syscall, unix.GetRandom will block
3737
// until the kernel has sufficient randomness (as we don't use GRND_NONBLOCK).
3838
// In this case, unix.GetRandom will not return an error.
39-
func getRandomBatch(p []byte) (ok bool) {
39+
func getRandom(p []byte) error {
4040
n, err := unix.GetRandom(p, 0)
41-
return n == len(p) && err == nil
41+
if err != nil {
42+
return err
43+
}
44+
if n != len(p) {
45+
return syscall.EIO
46+
}
47+
return nil
4248
}

src/crypto/rand/rand_linux.go

Lines changed: 0 additions & 14 deletions
This file was deleted.

src/crypto/rand/rand_solaris.go

Lines changed: 0 additions & 10 deletions
This file was deleted.

src/crypto/rand/rand_unix.go

Lines changed: 52 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ import (
1515
"io"
1616
"os"
1717
"sync"
18-
"sync/atomic"
1918
"syscall"
2019
"time"
2120
)
@@ -30,19 +29,69 @@ func init() {
3029
type reader struct {
3130
f io.Reader
3231
mu sync.Mutex
33-
used int32 // atomic; whether this reader has been used
32+
used bool // whether this reader has been used
3433
}
3534

3635
// altGetRandom if non-nil specifies an OS-specific function to get
3736
// urandom-style randomness.
3837
var altGetRandom func([]byte) (ok bool)
3938

39+
// batched returns a function that calls f to populate a []byte by chunking it
40+
// into subslices of, at most, readMax bytes, buffering min(readMax, 4096)
41+
// bytes at a time.
42+
func batched(f func([]byte) error, readMax int) func([]byte) bool {
43+
bufferSize := 4096
44+
if bufferSize > readMax {
45+
bufferSize = readMax
46+
}
47+
fullBuffer := make([]byte, bufferSize)
48+
var buf []byte
49+
return func(out []byte) bool {
50+
// First we copy any amount remaining in the buffer.
51+
n := copy(out, buf)
52+
out, buf = out[n:], buf[n:]
53+
54+
// Then, if we're requesting more than the buffer size,
55+
// generate directly into the output, chunked by readMax.
56+
for len(out) >= len(fullBuffer) {
57+
read := len(out) - (len(out) % len(fullBuffer))
58+
if read > readMax {
59+
read = readMax
60+
}
61+
if f(out[:read]) != nil {
62+
return false
63+
}
64+
out = out[read:]
65+
}
66+
67+
// If there's a partial block left over, fill the buffer,
68+
// and copy in the remainder.
69+
if len(out) > 0 {
70+
if f(fullBuffer[:]) != nil {
71+
return false
72+
}
73+
buf = fullBuffer[:]
74+
n = copy(out, buf)
75+
out, buf = out[n:], buf[n:]
76+
}
77+
78+
if len(out) > 0 {
79+
panic("crypto/rand batching failed to fill buffer")
80+
}
81+
82+
return true
83+
}
84+
}
85+
4086
func warnBlocked() {
4187
println("crypto/rand: blocked for 60 seconds waiting to read random data from the kernel")
4288
}
4389

4490
func (r *reader) Read(b []byte) (n int, err error) {
45-
if atomic.CompareAndSwapInt32(&r.used, 0, 1) {
91+
r.mu.Lock()
92+
defer r.mu.Unlock()
93+
if !r.used {
94+
r.used = true
4695
// First use of randomness. Start timer to warn about
4796
// being blocked on entropy not being available.
4897
t := time.AfterFunc(time.Minute, warnBlocked)
@@ -51,8 +100,6 @@ func (r *reader) Read(b []byte) (n int, err error) {
51100
if altGetRandom != nil && altGetRandom(b) {
52101
return len(b), nil
53102
}
54-
r.mu.Lock()
55-
defer r.mu.Unlock()
56103
if r.f == nil {
57104
f, err := os.Open(urandomDevice)
58105
if err != nil {

0 commit comments

Comments
 (0)