At Boot, crypto/rand Waits and /dev/urandom Doesn't
Go's crypto/rand on Linux can block, and it is /dev/urandom that never will. That is the wrong way round from the folklore, which goes: "crypto/rand never blocks, so it is safe to call anywhere". The second half is mostly true. The first half is backwards, and the title I was originally going to give this post had the same mistake baked in.
It is a slightly odd thing to discover, because most people assume the Go package is a thin wrapper over /dev/urandom. It has not been for a long time. Here is what it actually does.
strace shows a blocking getrandom, not a file read
Here is a small program. It asks the kernel, without blocking, whether its random number generator (the CRNG) has been initialised, then calls rand.Read and times it. The probe uses golang.org/x/sys/unix because the frozen syscall package does not expose getrandom on every architecture.
package main
import (
"crypto/rand"
"errors"
"fmt"
"os"
"time"
"golang.org/x/sys/unix"
)
// crngReady asks the kernel for one byte without blocking. EAGAIN means the
// CRNG is not initialised yet.
func crngReady() (bool, error) {
var b [1]byte
_, err := unix.Getrandom(b[:], unix.GRND_NONBLOCK)
switch {
case err == nil:
return true, nil
case errors.Is(err, unix.EAGAIN):
return false, nil
default:
return false, err
}
}
func main() {
ready, err := crngReady()
if err != nil {
fmt.Fprintln(os.Stderr, "getrandom:", err)
os.Exit(1)
}
fmt.Println("crng ready:", ready)
start := time.Now()
key := make([]byte, 32)
if _, err := rand.Read(key); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
fmt.Printf("rand.Read took %s\n", time.Since(start))
}
On a booted machine it prints crng ready: true and a read time in single-digit microseconds. The interesting part is what strace says (I ran this on a 6.8 kernel; output trimmed):
$ strace -f -e trace=getrandom,openat ./t
getrandom("\xc3", 1, GRND_NONBLOCK) = 1
getrandom("\x0b\xaa\xfa\x82...", 32, 0) = 32
Two calls, and they differ in one flag:
- The first is my probe, with
GRND_NONBLOCK. - The second is
crypto/rand, and its flags are0. NoGRND_NONBLOCK, and noopenatof/dev/urandomanywhere.
With flags of zero, getrandom(2) sleeps until the kernel's generator has been seeded. If the probe had returned EAGAIN, the second call would have sat there.
Blocking is the design, and Go says so
This is not an accident or a quirk of my kernel. The Go 1.22 source (crypto/rand/rand_getrandom.go) has the comment right above the call: if the kernel supports getrandom, it "will block until the kernel has sufficient randomness (as we don't use GRND_NONBLOCK)".
It is the safe choice. A program that generates a key before the kernel is seeded would otherwise get output that an attacker with a decent model of the boot process could predict.
There is also a safety net for the case where it goes on for too long. On first use the reader starts a one-minute timer, and if it fires you get this on stderr:
crypto/rand: blocked for 60 seconds waiting to read random data from the kernel
It is a plain println, so it turns up in the journal of whatever service is stuck. If you ever see it, you are looking at an early-boot entropy problem rather than a Go bug.
/dev/urandom is the one that does not wait
Historically, if you read /dev/urandom before the pool was initialised, you simply got bytes, with whatever quality the kernel had managed so far. That is the failure behind the 2012 "Mining Your Ps and Qs" research. Embedded devices generated SSH and TLS keys at first boot, and a striking number of them collided. The keys were fine cryptographically; the seeding was not there yet.
Actually, this bit is worth a moment. The two interfaces used to differ in exactly this way, and getrandom(2) was added (Linux 3.17) largely to give programs a way to say "give me randomness, but only once it is real" without opening a file. Go picked that up, which is why the package prefers it.
Where Go still falls back to /dev/urandom
The fallback is real, though. In the 1.22 source, if getrandom returns any error, Read drops through to opening /dev/urandom and reading from that.
The comment says the case they care about is kernels too old to have the syscall (ENOSYS), but the code does not check for that specifically. So an environment that makes getrandom fail some other way, a restrictive seccomp filter for example, quietly gets the non-blocking path instead. I have not tested that combination, so treat it as something to check with strace if you care.
What Go 1.24 changed, and what it did not
Go 1.24 tidied the contract. Per the release notes:
rand.Readis now "guaranteed not to fail": it always returns a nil error, and if it does hit an error the program crashes rather than handing you a short or zeroed buffer.- The one exception is Linux before 3.17, where the default reader still opens
/dev/urandomand may fail. - On Linux 6.11 and later, the reader uses
getrandomthrough the vDSO, which is several times faster for small reads.
None of that changes the blocking semantics. "Never returns an error" and "never waits" are different promises, and only the first one is being made.
Where it bites in practice
Rarely, on a modern kernel. Since Linux 5.4 the kernel will try to gather entropy itself from CPU timing jitter if someone is waiting on it, so the wait is normally short. On a laptop or a decent server you will never see it.
The places it does show up are the ones you would guess:
- small VMs and cloud images with no hardware RNG exposed;
- embedded boards with no battery-backed clock and no on-chip RNG;
- very early systemd units that generate keys, certificates or tokens before the seed file has been loaded;
- containers on hosts booting from a fresh image, where nothing has accumulated yet.
The tell is a service that hangs at boot with no CPU use, whose first sign of life is a call into crypto/rand. Run strace -f -p against it and look for getrandom(..., 0) not returning. Then check the kernel log for a line about the CRNG initialising, to see how far into boot it landed.
Fix the system, not the read
Do not "fix" it by reading /dev/urandom yourself to dodge the wait. That trades a delay for a possibly predictable key, which is the worse outcome. The better options are on the system side:
- give VMs a virtio-rng device so the guest has a proper entropy source from the start;
- on hardware you trust, the kernel can credit the CPU's RNG instruction at boot (the
random.trust_cpuoption, or the equivalent build config), which is a judgement about your threat model rather than a free win; - make sure a saved seed is loaded early (systemd ships
systemd-random-seed.servicefor this), and order key-generating units after it; - if your program can reasonably do something else while waiting, use the
GRND_NONBLOCKprobe above and retry with a log line, instead of hanging silently.
The last one is the only thing you can change from inside Go, and it is a diagnostic more than a cure. For the actual secret, you still want the blocking read; the whole point is that it refuses to answer until the answer is worth having.