Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

crypto/rand: make Read not escape the byte slice #66801

Open
wants to merge 17 commits into
base: master
Choose a base branch
from
20 changes: 20 additions & 0 deletions src/crypto/rand/getrand.go
@@ -0,0 +1,20 @@
// Copyright 2024 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.

//go:build (js && wasm) || wasip1 || windows

package rand

import "crypto/rand/internal/getrand"

var randReader = rngReader{}

type rngReader struct{}

func (r rngReader) Read(b []byte) (int, error) {
if err := getrand.GetRandom(b); err != nil {
return 0, err
}
return len(b), nil
}
26 changes: 26 additions & 0 deletions src/crypto/rand/internal/getrand/getrand.go
@@ -0,0 +1,26 @@
// Copyright 2024 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.

//go:build linux || dragonfly || freebsd || illumos || solaris || darwin || openbsd || netbsd || (js && wasm) || wasip1 || windows

package getrand

import "math"

// GetRandom populates out with cryptographically secure random data.
func GetRandom(out []byte) error {
if maxGetRandomRead == math.MaxInt {
return getRandom(out)
}

// Batch random read operations up to maxGetRandomRead.
for len(out) > 0 {
readBytes := min(len(out), maxGetRandomRead)
if err := getRandom(out[:readBytes]); err != nil {
return err
}
out = out[readBytes:]
}
return nil
}
Expand Up @@ -2,11 +2,18 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package rand
//go:build darwin

import "internal/syscall/unix"
package getrand

func init() {
import (
"internal/syscall/unix"
"math"
)

const maxGetRandomRead = math.MaxInt

func getRandom(out []byte) error {
// arc4random_buf is the recommended application CSPRNG, accepts buffers of
// any size, and never returns an error.
//
Expand All @@ -15,5 +22,6 @@ func init() {
//
// Note that despite its legacy name, it uses a secure CSPRNG (not RC4) in
// all supported macOS versions.
altGetRandom = func(b []byte) error { unix.ARC4Random(b); return nil }
unix.ARC4Random(out)
return nil
}
18 changes: 18 additions & 0 deletions src/crypto/rand/internal/getrand/getrand_getentropy.go
@@ -0,0 +1,18 @@
// Copyright 2024 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.

//go:build openbsd || netbsd

package getrand

import (
"internal/syscall/unix"
)

// getentropy(2) returns a maximum of 256 bytes per call.
const maxGetRandomRead = 256

func getRandom(out []byte) error {
return unix.GetEntropy(out)
}
@@ -1,39 +1,25 @@
// Copyright 2018 The Go Authors. All rights reserved.
// Copyright 2024 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.

//go:build js && wasm

package rand
package getrand

import "syscall/js"
import (
"syscall/js"
)

// The maximum buffer size for crypto.getRandomValues is 65536 bytes.
// https://developer.mozilla.org/en-US/docs/Web/API/Crypto/getRandomValues#exceptions
const maxGetRandomRead = 64 << 10

var batchedGetRandom func([]byte) error

func init() {
Reader = &reader{}
batchedGetRandom = batched(getRandom, maxGetRandomRead)
}

var jsCrypto = js.Global().Get("crypto")
var uint8Array = js.Global().Get("Uint8Array")

// reader implements a pseudorandom generator
// getRandom populates the input slice with pseudorandom data
// using JavaScript crypto.getRandomValues method.
// See https://developer.mozilla.org/en-US/docs/Web/API/Crypto/getRandomValues.
type reader struct{}

func (r *reader) Read(b []byte) (int, error) {
if err := batchedGetRandom(b); err != nil {
return 0, err
}
return len(b), nil
}

func getRandom(b []byte) error {
a := uint8Array.New(len(b))
jsCrypto.Call("getRandomValues", a)
Expand Down
23 changes: 23 additions & 0 deletions src/crypto/rand/internal/getrand/getrand_unix.go
@@ -0,0 +1,23 @@
// Copyright 2024 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.

//go:build linux || dragonfly || freebsd || illumos || solaris

package getrand

import (
"internal/syscall/unix"
"syscall"
)

func getRandom(out []byte) error {
n, err := unix.GetRandom(out, 0)
if err != nil {
return err
}
if n != len(out) {
return syscall.EIO
}
return nil
}
@@ -1,27 +1,22 @@
// Copyright 2023 The Go Authors. All rights reserved.
// Copyright 2024 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.

//go:build wasip1

package rand
package getrand

import "syscall"
import (
"math"
"syscall"
)

func init() {
Reader = &reader{}
}

type reader struct{}
const maxGetRandomRead = math.MaxInt

func (r *reader) Read(b []byte) (int, error) {
func getRandom(b []byte) error {
// This uses the wasi_snapshot_preview1 random_get syscall defined in
// https://github.com/WebAssembly/WASI/blob/23a52736049f4327dd335434851d5dc40ab7cad1/legacy/preview1/docs.md#-random_getbuf-pointeru8-buf_len-size---result-errno.
// The definition does not explicitly guarantee that the entire buffer will
// be filled, but this appears to be the case in all runtimes tested.
err := syscall.RandomGet(b)
if err != nil {
return 0, err
}
return len(b), nil
return syscall.RandomGet(b)
}
18 changes: 18 additions & 0 deletions src/crypto/rand/internal/getrand/getrand_windows.go
@@ -0,0 +1,18 @@
// Copyright 2024 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.

//go:build windows

package getrand

import (
"internal/syscall/windows"
"math"
)

const maxGetRandomRead = math.MaxInt

func getRandom(b []byte) error {
return windows.ProcessPrng(b)
}
14 changes: 14 additions & 0 deletions src/crypto/rand/internal/getrand/maxread_linux.go
@@ -0,0 +1,14 @@
// Copyright 2024 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.

//go:build linux

package getrand

// 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.
const maxGetRandomRead = (1 << 25) - 1
9 changes: 9 additions & 0 deletions src/crypto/rand/internal/getrand/maxread_unix.go
@@ -0,0 +1,9 @@
// Copyright 2024 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.

//go:build dragonfly || freebsd || illumos || solaris

package getrand

const maxGetRandomRead = 1 << 8
13 changes: 13 additions & 0 deletions src/crypto/rand/internal/getrand/notgetrand.go
@@ -0,0 +1,13 @@
// Copyright 2024 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.

//go:build !(linux || dragonfly || freebsd || illumos || solaris || darwin || openbsd || netbsd || (js && wasm) || wasip1 || windows)

package getrand

import "errors"

func GetRandom(out []byte) error {
return errors.ErrUnsupported
}
66 changes: 46 additions & 20 deletions src/crypto/rand/rand.go
Expand Up @@ -6,7 +6,9 @@
// random number generator.
package rand

import "io"
import (
"io"
)

// Reader is a global, shared instance of a cryptographically
// secure random number generator.
Expand All @@ -19,28 +21,52 @@ import "io"
// - On Windows, Reader uses the ProcessPrng API.
// - On js/wasm, Reader uses the Web Crypto API.
// - On wasip1/wasm, Reader uses random_get from wasi_snapshot_preview1.
var Reader io.Reader
var Reader io.Reader = randReader

// Read is a helper function that calls Reader.Read using io.ReadFull.
// Read is a helper function that reads data from the [Reader] and populates
// the entire out byte slice with cryptographically secure random data.
// It has the same behaviour as calling io.ReadFull with the [Reader].
// On return, n == len(b) if and only if err == nil.
func Read(b []byte) (n int, err error) {
return io.ReadFull(Reader, b)
func Read(out []byte) (n int, err error) {
if Reader != randReader {
// We document that this function reads from the global Reader, as of now
// the compiler is not able to devirtualize the Reader.Read call, thus making
// the input slice to this function escape to the heap. To prevent that in cases
// when the Reader has changed, we use a temporary buffer in readFromReader.
// That buffer will be allocated on the heap, but at least it will not make the
// out slice to be escaped, thus making the most common path (Reader not changed)
// escape-free.
return readFromReader(out)
}

// To avoid escaping the out slice, inline the io.ReadFull function.
// The following code has the same behaviour as: io.ReadFull(Reader, out).
for n < len(out) && err == nil {
var nn int
nn, err = randReader.Read(out[n:])
n += nn
}
if n >= len(out) {
err = nil
} else if n > 0 && err == io.EOF {
err = io.ErrUnexpectedEOF
}
return
}

// 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) error, readMax int) func([]byte) error {
return func(out []byte) error {
for len(out) > 0 {
read := len(out)
if read > readMax {
read = readMax
}
if err := f(out[:read]); err != nil {
return err
}
out = out[read:]
}
return nil
func readFromReader(out []byte) (n int, err error) {
tmp := make([]byte, max(len(out), 512))

for n < len(out) && err == nil {
var nn int
nn, err = Reader.Read(tmp)
copy(out[n:], tmp[:nn])
n += nn
}
if n >= len(out) {
err = nil
} else if n > 0 && err == io.EOF {
err = io.ErrUnexpectedEOF
}
return
}