]> Cypherpunks repositories - gostls13.git/commitdiff
crypto/rand: batch and buffer calls to getrandom/getentropy
authorJason A. Donenfeld <Jason@zx2c4.com>
Fri, 10 Dec 2021 16:23:08 +0000 (17:23 +0100)
committerGopher Robot <gobot@golang.org>
Tue, 19 Apr 2022 22:44:58 +0000 (22:44 +0000)
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 <valsorda@google.com>
Run-TryBot: Jason Donenfeld <Jason@zx2c4.com>
Auto-Submit: Jason Donenfeld <Jason@zx2c4.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
Reviewed-by: Ian Lance Taylor <iant@google.com>
src/crypto/rand/rand_batched_test.go
src/crypto/rand/rand_dragonfly.go [deleted file]
src/crypto/rand/rand_freebsd.go [deleted file]
src/crypto/rand/rand_getentropy.go
src/crypto/rand/rand_getrandom.go [moved from src/crypto/rand/rand_batched.go with 56% similarity]
src/crypto/rand/rand_linux.go [deleted file]
src/crypto/rand/rand_solaris.go [deleted file]
src/crypto/rand/rand_unix.go

index 28e45aa689ec185976c02a4b69caf54983a7fc28..7a981e7892f1d1d161dd19c12c7bb0306e18f831 100644 (file)
@@ -2,21 +2,24 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-//go:build linux || freebsd || dragonfly || solaris
+//go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris
 
 package rand
 
 import (
        "bytes"
+       "encoding/binary"
+       "errors"
+       prand "math/rand"
        "testing"
 )
 
 func TestBatched(t *testing.T) {
-       fillBatched := batched(func(p []byte) bool {
+       fillBatched := batched(func(p []byte) error {
                for i := range p {
                        p[i] = byte(i)
                }
-               return true
+               return nil
        }, 5)
 
        p := make([]byte, 13)
@@ -29,16 +32,49 @@ func TestBatched(t *testing.T) {
        }
 }
 
+func TestBatchedBuffering(t *testing.T) {
+       var prandSeed [8]byte
+       Read(prandSeed[:])
+       prand.Seed(int64(binary.LittleEndian.Uint64(prandSeed[:])))
+
+       backingStore := make([]byte, 1<<23)
+       prand.Read(backingStore)
+       backingMarker := backingStore[:]
+       output := make([]byte, len(backingStore))
+       outputMarker := output[:]
+
+       fillBatched := batched(func(p []byte) error {
+               n := copy(p, backingMarker)
+               backingMarker = backingMarker[n:]
+               return nil
+       }, 731)
+
+       for len(outputMarker) > 0 {
+               max := 9200
+               if max > len(outputMarker) {
+                       max = len(outputMarker)
+               }
+               howMuch := prand.Intn(max + 1)
+               if !fillBatched(outputMarker[:howMuch]) {
+                       t.Fatal("batched function returned false")
+               }
+               outputMarker = outputMarker[howMuch:]
+       }
+       if !bytes.Equal(backingStore, output) {
+               t.Error("incorrect batch result")
+       }
+}
+
 func TestBatchedError(t *testing.T) {
-       b := batched(func(p []byte) bool { return false }, 5)
+       b := batched(func(p []byte) error { return errors.New("failure") }, 5)
        if b(make([]byte, 13)) {
-               t.Fatal("batched function should have returned false")
+               t.Fatal("batched function should have returned an error")
        }
 }
 
 func TestBatchedEmpty(t *testing.T) {
-       b := batched(func(p []byte) bool { return false }, 5)
+       b := batched(func(p []byte) error { return errors.New("failure") }, 5)
        if !b(make([]byte, 0)) {
-               t.Fatal("empty slice should always return true")
+               t.Fatal("empty slice should always return successful")
        }
 }
diff --git a/src/crypto/rand/rand_dragonfly.go b/src/crypto/rand/rand_dragonfly.go
deleted file mode 100644 (file)
index 8a36fea..0000000
+++ /dev/null
@@ -1,9 +0,0 @@
-// Copyright 2021 The Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-package rand
-
-// maxGetRandomRead is the maximum number of bytes to ask for in one call to the
-// getrandom() syscall. In DragonFlyBSD at most 256 bytes will be returned per call.
-const maxGetRandomRead = 1 << 8
diff --git a/src/crypto/rand/rand_freebsd.go b/src/crypto/rand/rand_freebsd.go
deleted file mode 100644 (file)
index 75f683c..0000000
+++ /dev/null
@@ -1,9 +0,0 @@
-// Copyright 2018 The Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-package rand
-
-// maxGetRandomRead is the maximum number of bytes to ask for in one call to the
-// getrandom() syscall. In FreeBSD at most 256 bytes will be returned per call.
-const maxGetRandomRead = 1 << 8
index 2bf2f520324438acf005b0364c85eaec01baa6f4..68f921b0fc3fbe4c7c996c8026d0e558edb79995 100644 (file)
@@ -6,25 +6,9 @@
 
 package rand
 
-import (
-       "internal/syscall/unix"
-)
+import "internal/syscall/unix"
 
 func init() {
-       altGetRandom = getEntropy
-}
-
-func getEntropy(p []byte) (ok bool) {
        // getentropy(2) returns a maximum of 256 bytes per call
-       for i := 0; i < len(p); i += 256 {
-               end := i + 256
-               if len(p) < end {
-                       end = len(p)
-               }
-               err := unix.GetEntropy(p[i:end])
-               if err != nil {
-                       return false
-               }
-       }
-       return true
+       altGetRandom = batched(unix.GetEntropy, 256)
 }
similarity index 56%
rename from src/crypto/rand/rand_batched.go
rename to src/crypto/rand/rand_getrandom.go
index 3e8e62038292040a38b4826eb3e8cc4448515f61..cb31a5687a5266e859ebff8d3cf97146b11020a1 100644 (file)
@@ -8,25 +8,25 @@ package rand
 
 import (
        "internal/syscall/unix"
+       "runtime"
+       "syscall"
 )
 
-// maxGetRandomRead is platform dependent.
 func init() {
-       altGetRandom = batched(getRandomBatch, maxGetRandomRead)
-}
-
-// batched returns a function that calls f to populate a []byte by chunking it
-// into subslices of, at most, readMax bytes.
-func batched(f func([]byte) bool, readMax int) func([]byte) bool {
-       return func(buf []byte) bool {
-               for len(buf) > readMax {
-                       if !f(buf[:readMax]) {
-                               return false
-                       }
-                       buf = buf[readMax:]
-               }
-               return len(buf) == 0 || f(buf)
+       var maxGetRandomRead int
+       switch runtime.GOOS {
+       case "linux", "android":
+               // Per the manpage:
+               //     When reading from the urandom source, a maximum of 33554431 bytes
+               //     is returned by a single call to getrandom() on systems where int
+               //     has a size of 32 bits.
+               maxGetRandomRead = (1 << 25) - 1
+       case "freebsd", "dragonfly", "solaris":
+               maxGetRandomRead = 1 << 8
+       default:
+               panic("no maximum specified for GetRandom")
        }
+       altGetRandom = batched(getRandom, maxGetRandomRead)
 }
 
 // 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 {
 // If the kernel supports the getrandom() syscall, unix.GetRandom will block
 // until the kernel has sufficient randomness (as we don't use GRND_NONBLOCK).
 // In this case, unix.GetRandom will not return an error.
-func getRandomBatch(p []byte) (ok bool) {
+func getRandom(p []byte) error {
        n, err := unix.GetRandom(p, 0)
-       return n == len(p) && err == nil
+       if err != nil {
+               return err
+       }
+       if n != len(p) {
+               return syscall.EIO
+       }
+       return nil
 }
diff --git a/src/crypto/rand/rand_linux.go b/src/crypto/rand/rand_linux.go
deleted file mode 100644 (file)
index 29809f6..0000000
+++ /dev/null
@@ -1,14 +0,0 @@
-// Copyright 2014 The Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-package rand
-
-// maxGetRandomRead is the maximum number of bytes to ask for in one call to the
-// getrandom() syscall. In linux at most 2^25-1 bytes will be returned per call.
-// From the manpage
-//
-//   - When reading from the urandom source, a maximum of 33554431 bytes
-//     is returned by a single call to getrandom() on systems where int
-//     has a size of 32 bits.
-const maxGetRandomRead = (1 << 25) - 1
diff --git a/src/crypto/rand/rand_solaris.go b/src/crypto/rand/rand_solaris.go
deleted file mode 100644 (file)
index bbad0fe..0000000
+++ /dev/null
@@ -1,10 +0,0 @@
-// Copyright 2021 The Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-package rand
-
-// maxGetRandomRead is the maximum number of bytes to ask for in one call to the
-// getrandom() syscall. Across all the Solaris platforms, 256 bytes is the
-// lowest number of bytes returned atomically per call.
-const maxGetRandomRead = 1 << 8
index 58c97649c47eb0f60f047565f0ec22328f2919f9..2dd4158888ffa849d154555d223a525dd33e0205 100644 (file)
@@ -15,7 +15,6 @@ import (
        "io"
        "os"
        "sync"
-       "sync/atomic"
        "syscall"
        "time"
 )
@@ -30,19 +29,69 @@ func init() {
 type reader struct {
        f    io.Reader
        mu   sync.Mutex
-       used int32 // atomic; whether this reader has been used
+       used bool // whether this reader has been used
 }
 
 // altGetRandom if non-nil specifies an OS-specific function to get
 // urandom-style randomness.
 var altGetRandom func([]byte) (ok bool)
 
+// batched returns a function that calls f to populate a []byte by chunking it
+// into subslices of, at most, readMax bytes, buffering min(readMax, 4096)
+// bytes at a time.
+func batched(f func([]byte) error, readMax int) func([]byte) bool {
+       bufferSize := 4096
+       if bufferSize > readMax {
+               bufferSize = readMax
+       }
+       fullBuffer := make([]byte, bufferSize)
+       var buf []byte
+       return func(out []byte) bool {
+               // First we copy any amount remaining in the buffer.
+               n := copy(out, buf)
+               out, buf = out[n:], buf[n:]
+
+               // Then, if we're requesting more than the buffer size,
+               // generate directly into the output, chunked by readMax.
+               for len(out) >= len(fullBuffer) {
+                       read := len(out) - (len(out) % len(fullBuffer))
+                       if read > readMax {
+                               read = readMax
+                       }
+                       if f(out[:read]) != nil {
+                               return false
+                       }
+                       out = out[read:]
+               }
+
+               // If there's a partial block left over, fill the buffer,
+               // and copy in the remainder.
+               if len(out) > 0 {
+                       if f(fullBuffer[:]) != nil {
+                               return false
+                       }
+                       buf = fullBuffer[:]
+                       n = copy(out, buf)
+                       out, buf = out[n:], buf[n:]
+               }
+
+               if len(out) > 0 {
+                       panic("crypto/rand batching failed to fill buffer")
+               }
+
+               return true
+       }
+}
+
 func warnBlocked() {
        println("crypto/rand: blocked for 60 seconds waiting to read random data from the kernel")
 }
 
 func (r *reader) Read(b []byte) (n int, err error) {
-       if atomic.CompareAndSwapInt32(&r.used, 0, 1) {
+       r.mu.Lock()
+       defer r.mu.Unlock()
+       if !r.used {
+               r.used = true
                // First use of randomness. Start timer to warn about
                // being blocked on entropy not being available.
                t := time.AfterFunc(time.Minute, warnBlocked)
@@ -51,8 +100,6 @@ func (r *reader) Read(b []byte) (n int, err error) {
        if altGetRandom != nil && altGetRandom(b) {
                return len(b), nil
        }
-       r.mu.Lock()
-       defer r.mu.Unlock()
        if r.f == nil {
                f, err := os.Open(urandomDevice)
                if err != nil {