nos_
ENRU
All documentation

How it works

Randomness

docs/random.mdView on GitHub

Everything in the kernel that needs random bytes asks one pool, the random crate (src/rust/random): the TLS client for its handshake keys, TCP for its initial sequence numbers, DHCP and DNS for their ids, the SSH server for its keys, the shell's random command, anything later that needs a token or a nonce. It is one ChaCha20 generator, seeded from every entropy source the machine turns out to have. This page is what those sources are, what the generator does with them, and what is and is not being claimed.

Where it lives

The pool is Rust, in the image: a static with nothing to construct, behind a lock that allocates nothing (IrqSpinLock), because the boot seeds it from Main2, long before rust_init. What reaches it, and how:

  • the layers and the modules -- net, tls, fs, the sshd module -- through kcore::random (fill_random, random_u64), which is the C ABI's kernel_get_random, defined in the crate. In the image that is a call from Rust into Rust across the C ABI, and it is on purpose: it is the seam the fuzzers stand in for (fuzz/common/machine), so that what is random in a fuzzer is the input's -- the TCP target hands out sequence numbers that are about to wrap.
  • a hardware generator's driver registers as a trait object, random::register_source(name, &'static dyn random::Source) -- drivers/virtio_rng.
  • the boot, C++: Hal::ProbeHwRandom() and then rust_random_setup() in Main2 / MainArm64, rust_random_reseed() once the devices are up.
  • the CPU's instruction is the one piece left in C++, behind hal/random.h, because it is an instruction and the HAL is where those are: the pool asks for it through calls that take values and give values back (kernel_hw_random_kind, kernel_hw_random, kernel_hw_random_seed in rust_ffi.cpp, kcore::random::hw_* in Rust), which fail rather than fault on a CPU without one.

What this replaced

There used to be no generator, only the source registry: EntropySource implementations registered with EntropySourceTable, and a caller took GetDefault() — whichever had registered first — and read it directly. Only one source was ever implemented, virtio-rng, and that is a device a hypervisor provides. So on real hardware the table was empty, GetDefault() returned null, and the first wget https:// on the Hetzner box died at the first thing a TLS handshake does:

tls: connection: FailedToGetRandomBytes
TlsConn: handshake with github.com failed

Reading a source directly does not work even where there is one. Supply is nothing like uniform: virtio-rng is a device round trip, RDSEED runs dry when several cores draw at once, timing jitter yields a bit at a time. A handshake wants hundreds of bytes at once and cannot be told to come back later. That is what a generator is for — sources contribute, the generator answers.

The sources

source where what it is
rdseed / rdrand x86-64, arch/x86_64/hal_random.cpp the CPU's own DRBG. RDSEED taps the conditioned output of the physical noise source and is what another generator wants to be keyed from; RDRAND is the AES-CTR DRBG downstream of it — faster, never dry, one more deterministic step from the noise
rndr arm64, arch/arm64/hal_random.cpp FEAT_RNG, the same idea (RNDRRS reseeds before answering, RNDR does not). Optional from Armv8.5 and not implemented by Apple's M-series, so no arm64 machine nos runs on today has it
rng0… src/rust/drivers/virtio_rng virtio-rng, i.e. the host's entropy. Present under QEMU, absent on bare metal — exactly the wrong way round from the CPU instruction, which is why both exist
jitter src/rust/random/src/jitter.rs timing jitter, the fallback (below)

hal/random.h is the seam for the CPU instruction, so common code never asks which architecture it is on. Hal::ProbeHwRandom() runs once on the BSP and is what decides whether the instruction exists: reading RNDR on a core without FEAT_RNG is an undefined-instruction trap, and CPUID has to be checked before RDRAND for the same reason. It also honours hwrng=off.

The probe answering "yes" is not enough. cpu::self_test() (src/rust/random/src/cpu.rs) draws eight values and refuses the instruction unless at least half of them arrive and at least two differ — which is what a broken RDRAND looks like (some AMD parts answer 0xFFFF'FFFF'FFFF'FFFF forever after a resume) and what a hypervisor that stubs the instruction out looks like too. In both cases the capability bit still says the instruction is there.

Timing jitter, and what it is worth

On a machine with no random instruction and no virtio-rng, the only thing left that differs between two boots is how long things take. The collector times a dependent read-modify-write walk over a 4 KiB buffer — the trip count and the starting offset depend on the state the last measurement left, so the walk cannot be prefetched or unrolled into something of fixed cost — and keeps the low bit of the duration. The buffer is on the caller's stack, its own for the call, so two reseeds at once share nothing; its address is put out of the compiler's sight first, so that the cycle-counter calls on either side of a walk are calls it must take to read the walk, and it cannot move the walk out from between them. Pairs of those bits go through von Neumann extraction: a pair that agrees is discarded, a pair that differs contributes one bit, which removes whatever fixed bias the low bit has at the cost of most of the samples.

What varies between two measurements is cache and store-buffer state, the branch predictor, the CPU's own frequency, an SMT sibling, and any interrupt that lands in the middle of one. That is real but it is not a hardware entropy source, the bits are not independent, and no attempt is made here to put a number on it. It is mixed in as material of unknown worth, which the absorb below makes safe.

It can also fail outright, and says so rather than handing back biased bytes: if 256 pairs go by without a single measurement differing from the one before it, the counter is too coarse to see this much work, or is not running.

The generator

Both halves are Linux's construction, in miniature. ChaCha20's block function (src/rust/random/src/chacha20.rs, RFC 8439, checked at boot against the RFC's own test vector by random::selftest) is the only primitive.

        sources                   pool                  callers
  rdseed ------\                                 /--- kernel_get_random -> rustls, TCP, ...
  rng0 --------->  add_entropy  ->  key[32]  ->  fill ---- random [len]
  jitter ------/     absorb          |        fast key
  boot marks --/                     \--- erasure --/

Output — fast key erasure. A request generates one ChaCha20 block over the 32-byte key, keeps the first half of the block as the next key and hands the second half out. The state that produced a value is gone by the time the caller has it, so no output leads back to an earlier one. Where the CPU has a random instruction, one draw from it is also XORed into the key on every request: a few hundred cycles, no lock and no device, and it means the stream keeps taking in fresh entropy with no timer to drive it.

Seeding — absorb. Seed material is XORed into the key 32 bytes at a time and the block function run over the result. XOR cannot take entropy out of the key, and ChaCha20's feed-forward addition is what stops the new key leading back to the old one. Together those two properties are what let material of unknown quality — jitter, boot-time constants — be mixed in without an entropy estimate to argue about: a source that turns out to be worthless cannot make the pool worse.

When it happens

when what
Main2 / MainArm64, before anything can ask for bytes Hal::ProbeHwRandom(), then rust_random_setup() — register the sources that need no device, seed from them. No heap, no device
BpStartup / BpStartupArm, after the virtio probe rust_random_reseed() — fold in every registered source, virtio-rng now included
rust_test random::selftest — the block function against RFC 8439, then the pool's output
entropy reseed the reseed, by hand

Setup comes first because nothing may ask the pool for bytes before it. On arm64 nothing better than jitter exists at that point — no FEAT_RNG on any core nos runs on, and the virtio-mmio slots have not been probed yet — so the pool is seeded from jitter there and the reseed after device bring-up is what puts hardware entropy into it. Setup finding nothing to seed from, which takes a machine with no random instruction and a cycle counter too coarse for the jitter collector (as under TCG), is not fatal: boot says so plainly, and HTTPS is what does not work until a reseed finds a source.

There is no periodic reseed. A CSPRNG needs one good seed, and the rest of what Linux reseeds against — recovery from a state compromise across a privilege boundary — is not a threat this kernel has a boundary for. The per-request draw from the CPU instruction covers freshness where such an instruction exists.

Using it

random [len]        len random bytes as hex, 1..1024, default 16
entropy             pool state: seeded, whether hardware entropy reached it,
                    reseed count, bytes generated, and the registered sources
entropy reseed      draw from every source again, then print the same

hwrng=off on the kernel command line makes the CPU instruction invisible to the probe. That is how the fallback path gets exercised on a machine that does have RDRAND — entropy then reports hardware entropy no unless a virtio-rng is attached.

Reading entropy is the first thing to do when a handshake fails on a new machine. hardware entropy no with pool: chacha20 seeded means everything works but is resting on timing jitter alone; pool: chacha20 UNSEEDED means nothing could seed it and HTTPS will refuse to start rather than key itself from a zero pool (kernel_get_random returns 0, which rustls reports as FailedToGetRandomBytes).