Random Number Generation for Engineers
Random number generation is one of the very few places in engineering where a single misplaced function call — one that compiles cleanly, passes every test, and runs correctly for years — can silently and totally destroy the security of an entire system. A web server that calls the wrong randomness function will still serve pages. A key generator that seeds itself badly will still emit keys of the correct length and format. Nothing crashes, nothing logs an error, and nobody notices until an attacker who understood the failure has already enumerated everyone’s private keys. This is the central, uncomfortable fact about randomness: correctness is invisible from the outside, and the failure modes are catastrophic rather than graceful. The discipline that prevents disaster comes down to keeping three categories rigorously separate — true hardware randomness (TRNG), deterministic pseudo-randomness from an algorithm (PRNG), and the cryptographically secure subset of the latter (CSPRNG) — and knowing, without thinking, which one each task demands. Get the category right and the rest is mostly calling the operating system. Get it wrong and no amount of careful code review elsewhere will save you. This post walks the three categories, explains how the Linux kernel actually produces randomness in 2026, settles the long-dead /dev/random versus /dev/urandom debate honestly, surveys hardware entropy sources and the trust questions around them, dissects the seeding disasters that have leaked real private keys into the wild, and ends with a flat, prescriptive list of exactly what to call and what to never use.
Three Categories, And Why The Distinction Is Everything
Every source of “random” numbers an engineer will encounter falls into one of three buckets, and confusing them is the root of nearly every randomness bug that matters.
A TRNG (True Random Number Generator), sometimes called a hardware RNG, harvests entropy from a physical process that is fundamentally unpredictable: thermal (Johnson-Nyquist) noise across a resistor, jitter between free-running ring oscillators, shot noise in a reverse-biased diode (avalanche noise), metastability of flip-flops, or radioactive decay. The output is non-deterministic in the strong sense — even an attacker with a perfect copy of the hardware design and full knowledge of every prior output cannot predict the next bit, because the underlying physics genuinely is not determined. The catch is that raw physical entropy is slow, biased (more zeros than ones, or correlated adjacent bits), and dependent on the hardware behaving correctly, which is hard to verify continuously.
A PRNG (Pseudo-Random Number Generator) is a deterministic algorithm. You give it a seed (a starting state), and it produces a long sequence of numbers that look statistically random — they pass distribution tests, have huge periods, and have no obvious patterns — but the entire sequence is a fixed mathematical function of the seed. Same seed, same sequence, always. This determinism is a feature for simulation and games: reproducible runs, fast generation, no I/O. Classic examples are the Mersenne Twister, the Linear Congruential Generator (LCG), xorshift, and the PCG family. These are excellent at being statistically random and terrible at being unpredictable, because predictability was never a design goal.
A CSPRNG (Cryptographically Secure PRNG) is a PRNG that adds two security properties on top of statistical quality. First, next-bit unpredictability: given any number of output bits, an attacker with bounded computational resources cannot predict the next bit better than guessing. Second, state-compromise resistance (often split into backward and forward secrecy): even if the attacker recovers the internal state at some moment, they cannot reconstruct prior outputs (backtracking resistance), and after the generator is reseeded with fresh entropy they cannot predict future outputs. A CSPRNG is still deterministic given its state — the difference is that recovering that state from the output is computationally infeasible, which is exactly the property non-crypto PRNGs lack.
| Category | Deterministic? | Predictable from output? | Typical example | Use for security? |
|---|---|---|---|---|
| TRNG (hardware) | No | No (physics) | RDSEED, ring oscillator, TPM | Yes (as entropy source) |
| Non-crypto PRNG | Yes | Yes — state recoverable | Mersenne Twister, LCG, xorshift, PCG | NEVER |
| CSPRNG | Yes (given state) | No (computationally infeasible) | ChaCha20-DRBG, AES-CTR-DRBG, getrandom() |
Yes |
The single most important takeaway from this table: a non-crypto PRNG and a CSPRNG can produce output that is indistinguishable to the naked eye and indistinguishable to most statistical test suites, yet one is safe for cryptographic keys and the other will get you owned. Statistical randomness and cryptographic unpredictability are different properties, and tests for the first do not establish the second.
Why Non-Crypto PRNGs Are Fine For Games And Fatal For Keys
The Mersenne Twister (MT19937) is the default PRNG in Python’s random module, in NumPy’s legacy generator, in Ruby, in PHP, in the C++ standard library, and in countless game engines. It has a period of 2^19937 − 1, passes a huge battery of statistical tests, and is fast. For a Monte Carlo integration, a procedurally generated dungeon, particle effects, load-balancing jitter, or A/B-test bucketing, it is an excellent choice. None of those applications care whether an adversary can predict the next number, and most positively benefit from reproducibility: seed the generator and you can replay the exact same simulation or game level.
The problem is that the Mersenne Twister’s internal state is 624 32-bit words, and it leaks. Observe 624 consecutive outputs and you can recover the full internal state by inverting the tempering transform, after which you can predict every future output and, with a little more work, reconstruct past ones. You do not even need 624 consecutive raw outputs in many real systems; researchers have repeatedly broken applications that exposed MT output indirectly. An LCG is far worse — its state is a single integer and can be recovered from a handful of outputs (sometimes one), and the low bits are notoriously non-random. xorshift and PCG are better-engineered modern non-crypto PRNGs with smaller state and good statistical properties, and PCG in particular was explicitly designed to make state recovery harder than xorshift, but neither makes any cryptographic security claim. The PCG author is clear about this: PCG is a statistical PRNG, not a CSPRNG, and must not be used where an adversary is in the loop.
The lesson is a clean dividing line. If a human attacker benefits from predicting your numbers — session tokens, password-reset tokens, API keys, nonces, IVs, salts that must be unique, cryptographic keys, shuffles in online gambling, anti-CSRF tokens, anything that gates access — you need a CSPRNG. If the only consumer is a simulation, a game, or a statistical sampler with no adversary, a fast non-crypto PRNG is the correct, performant tool. The mistake that bites people is reaching for the familiar default (random.random(), Math.random(), rand()) out of habit and using it for a token. That habit has produced predictable password-reset links, guessable session IDs, and forgeable tokens in production systems many times over.
The Linux Entropy Story, Told Honestly
There is more accumulated confusion around /dev/random versus /dev/urandom than around almost any other Unix interface, most of it inherited from documentation and folk wisdom that has been wrong since roughly 2014. Here is the current, correct picture.
Both /dev/random and /dev/urandom are fed by the same kernel CSPRNG. On modern Linux (kernel 4.8 onward, hardened further through the 5.x and 6.x series largely by Jason Donenfeld’s rewrite), that CSPRNG is built on ChaCha20. The kernel maintains an entropy collector that gathers unpredictable timing data from interrupts, device drivers, and other hardware events; once enough entropy has been accumulated to seed the CSPRNG, the generator is considered initialized and produces an effectively unlimited stream of cryptographically secure output.
The historical distinction was this: /dev/random maintained an “entropy estimate” and would block — stop returning bytes — when that estimate ran low, on the theory that you might “use up” randomness. /dev/urandom never blocked, returning CSPRNG output continuously. This led to a widespread myth that /dev/random is “more secure” and should be preferred for keys. That myth is false and has been actively harmful. A correctly seeded CSPRNG does not deplete entropy in any meaningful sense; you cannot “run out” of the output of ChaCha20 any more than you can run out of the output of a block cipher. The real risk is the opposite one: /dev/random blocking caused real outages and, worse, encouraged developers to work around the blocking with insecure fallbacks. Meanwhile the genuine danger — reading randomness before the CSPRNG is seeded at all, early in boot — applied to both interfaces and was the actual problem worth solving.
The modern Linux kernel resolved this. The blocking behavior of /dev/random was effectively removed in kernel 5.6 (2020); it now blocks only until the CSPRNG is initially seeded and never again. The two device files are, for all practical purposes after boot, identical. And the correct interface for new code is neither device file but the getrandom(2) system call, which does exactly the right thing: it returns cryptographically secure bytes, and by default it blocks only until the CSPRNG has been seeded for the first time, then never blocks again. This precisely solves the early-boot problem (you will never get unseeded output) without the perpetual-blocking pathology of old /dev/random.
|
|
The GRND_NONBLOCK flag makes it fail with EAGAIN rather than wait for initial seeding, and GRND_RANDOM selects the (now nearly meaningless) blocking pool — you almost never want either. Default flags are the right answer.
How The Kernel CSPRNG Actually Works
It is worth seeing the data flow, because it explains why the prescriptions hold. The kernel gathers raw entropy from many noisy sources, distills it, uses it to key a fast cryptographic stream generator, and periodically refreshes that key. The output you read is never raw entropy — it is the output of a cryptographic construction, which is why it is uniform, unlimited, and unpredictable.
Physical / timing entropy sources
┌──────────────┬──────────────┬───────────────┬──────────────┐
│ interrupt │ device-driver│ HW RNG │ RDSEED / │
│ timing jitter│ event timing │ (TPM, SoC RNG) │ RDRAND │
└──────┬───────┴──────┬───────┴───────┬────────┴──────┬───────┘
│ │ │ │
▼ ▼ ▼ ▼
┌──────────────────────────────────────────────────┐
│ Entropy collector / input pool (BLAKE2s mix) │
│ accumulates and conditions raw, biased input │
└───────────────────────┬──────────────────────────┘
│ extract + reseed
▼
┌────────────────────────────────┐
│ ChaCha20 CSPRNG (keyed state) │ <── reseeded
│ fast, uniform, unpredictable │ periodically
└───────────────┬────────────────┘
│
┌───────────────────┼────────────────────┐
▼ ▼ ▼
getrandom(2) /dev/urandom /dev/random
(blocks until (same CSPRNG output, identical after seeding)
first seeded)
Two design choices matter for engineers. First, conditioning: raw entropy is biased and correlated, so it is hashed/mixed (BLAKE2s in current kernels) before it ever keys the generator, which is why you never read raw diode noise directly. Second, reseeding: the ChaCha20 key is periodically replaced using freshly collected entropy, which provides forward secrecy — if an attacker somehow snapshots the generator state, that knowledge expires at the next reseed. The kernel also handles the dangerous VM-fork case: it detects clones via a VM-generation-ID and forces an immediate reseed so two cloned virtual machines do not march in lockstep producing identical “random” values.
Hardware RNGs And The Trust Problem
Modern CPUs ship dedicated instructions for randomness. Intel’s RDRAND returns the output of an on-die CSPRNG (AES-CTR-DRBG) that is itself seeded from a hardware entropy source; RDSEED returns closer-to-raw conditioned entropy suitable for seeding your own generator. AMD provides equivalent instructions. ARM defines RNDR/RNDRSS on recent cores and SoC-level TRNGs reachable through TrustZone-protected firmware; many ARM platforms also expose entropy via a secure-world service. TPMs (Trusted Platform Modules) expose a hardware RNG over their command interface. All of these are useful — and none of them should be trusted as your sole source of randomness.
The reason is that a hardware RNG is opaque. You cannot inspect the silicon, you cannot continuously verify the physics is healthy, and you cannot rule out a deliberate or accidental flaw. A backdoored or simply broken RDRAND would emit values that look perfectly random to every statistical test while being known to whoever planted the flaw — and the Dual_EC_DRBG affair demonstrated that a deliberately backdoored standardized generator is not a hypothetical. The disciplined response, adopted by both the Linux and FreeBSD kernel teams, is to mix, never trust alone. The kernel folds RDRAND/RDSEED output into the entropy pool alongside independent sources and conditions everything together. If the CPU instruction is honest, it adds excellent entropy; if it is compromised, mixing it with independent timing-based entropy means it cannot, on its own, determine the output. FreeBSD made this stance explicit after community pressure, refusing to let RDRAND directly drive the generator. The takeaway for application engineers is simpler: do not call RDRAND yourself to generate keys. Let the OS use it as one input among many, and read from the OS CSPRNG.
Seeding: Where Catastrophe Actually Happens
In practice, almost no real-world randomness failure comes from a weakness in ChaCha20 or AES. They come from seeding — the generator was deterministic, or under-seeded, or seeded from something an attacker could reconstruct. The history here is a parade of disasters that every engineer should know by name.
The Debian OpenSSL disaster (2006–2008). A Debian maintainer, trying to silence a Valgrind warning about reading uninitialized memory, commented out a line that mixed uninitialized data (and, fatally, also the line that fed real entropy) into OpenSSL’s PRNG. For nearly two years, every cryptographic key generated on Debian and its derivatives was seeded almost entirely from the process ID — at most 32,768 possibilities. The entire keyspace for SSH host keys, SSH user keys, OpenVPN keys, and TLS certificates collapsed to a list short enough to precompute exhaustively. Attackers could simply check a stolen public key against a table. Vast numbers of keys had to be revoked and regenerated. One commented-out line, no crash, no test failure, total compromise.
Mining your Ps and Qs (2012). Heninger, Durumeric, Wustrow, and Halderman scanned the entire IPv4 Internet’s TLS and SSH hosts and found that a startling fraction of RSA and DSA keys were weak. Many devices shared identical keys; more damningly, thousands of RSA moduli shared a single prime factor with another modulus, which lets you recover both private keys instantly via a GCD computation across the whole collected set. The root cause was boot-time entropy starvation: headless embedded devices (routers, firewalls, VPN appliances) generated keys on first boot before the kernel had collected meaningful entropy, so different devices produced predictable, sometimes colliding, primes. The fix is structural — do not generate long-term keys before the CSPRNG is seeded — and getrandom(2)’s blocking-until-seeded behavior exists precisely to enforce it.
Cloned VM and container state. A virtual machine snapshot captures the CSPRNG’s internal state. Restore that snapshot twice, or clone a “golden image,” and both instances resume from identical generator state, producing identical “random” sequences — duplicate session keys, duplicate nonces, repeated TLS randoms. The modern kernel mitigation is the VM-generation-ID-triggered reseed mentioned above, but it depends on hypervisor and guest support, so the operational rule stands: treat freshly cloned or restored machines as needing reseeding before they generate anything security-sensitive.
The common thread is that the algorithm was never the weak point. The seed was. Unique, sufficient, unpredictable seeding is the whole game, and “let the OS, which has spent the entire boot collecting entropy from many sources, do the seeding” is the only reliable way to get it.
What To Actually Call (And Never Call)
This is the prescriptive core. For anything security-relevant — keys, tokens, nonces, IVs, salts, password-reset codes, anything an adversary benefits from predicting — call the operating system’s CSPRNG through your language’s secure interface. Do not roll your own, do not seed a Mersenne Twister from the clock, and do not call the default random/rand/Math.random family.
| Platform / language | Use this (CSPRNG) | Never use for security |
|---|---|---|
| C / Linux | getrandom(2) |
rand(), random(), mrand48() |
| C / fallback | read /dev/urandom |
srand(time(NULL)) + rand() |
| Python | secrets module, os.urandom |
random.random(), random.randint() |
| Java | SecureRandom (default; avoid forcing SHA1PRNG) |
java.util.Random, Math.random() |
| Go | crypto/rand |
math/rand |
| Rust | getrandom crate / rand::rngs::OsRng |
non-OS StdRng/SmallRng for keys |
| Node.js | crypto.randomBytes, crypto.getRandomValues |
Math.random() |
| Browser JS | crypto.getRandomValues() |
Math.random() |
| Windows | BCryptGenRandom (or RtlGenRandom) |
rand() |
In Python, the contrast is stark and worth burning into memory:
|
|
The secrets module is a thin, correct wrapper over os.urandom, which on Linux uses getrandom. It exists specifically because people kept using random for tokens. There is no performance reason to avoid it for the quantities of randomness security needs (a key is 32 bytes, not 32 gigabytes). The same logic applies everywhere: the OS CSPRNG is fast enough, is seeded correctly across boot, reseeds itself, handles the VM-fork case, and mixes hardware sources you would not assemble yourself. Reaching past it to a userspace PRNG for security purposes is strictly worse on every axis that matters.
A reasonable architecture for high-volume token generation, if you genuinely measure a bottleneck, is to seed a CSPRNG construction (e.g., ChaCha20 in your crypto library) from the OS once and stream from it — but use a cryptographically secure construction, reseed periodically, and handle forking. For the overwhelming majority of code, calling the OS directly is correct, simpler, and safer.
If you want to understand why predictable randomness specifically destroys the schemes built on top of it, the per-message nonces in elliptic curve cryptography are the canonical example: a single repeated or predictable ECDSA nonce leaks the private key outright, which is the same failure class as a weak CSPRNG.
Statistical Testing: What It Can And Cannot Prove
Engineers reasonably want to verify a random source, and there are well-known test suites: Dieharder, the NIST SP 800-22 statistical test suite, and the most stringent of the three, L’Ecuyer and Simard’s TestU01 (with its SmallCrush, Crush, and BigCrush batteries). These run an output stream through dozens of statistical tests — frequency, runs, spectral, rank, serial correlation, and many more — checking whether the distribution deviates measurably from what true randomness would produce.
These suites are genuinely useful for one thing: catching gross statistical defects. A broken hardware RNG with a stuck bit, a biased diode, an LCG with bad low bits, or an off-by-one in a conditioning step will usually show up as a failed test. If your source fails these batteries, it is definitively broken and you have learned something valuable.
But passing them proves far less than people assume, and this is the crucial point. Statistical tests cannot establish cryptographic security. They measure whether output looks random; they say nothing about whether it is predictable. The clean counterexample: a high-quality non-crypto PRNG like the Mersenne Twister or a well-tuned PCG passes essentially all of BigCrush, yet its state is recoverable and its future output predictable. Even more starkly, the keystream of a strong cipher and the keystream of that same cipher with a known key are statistically identical — both pass every test — but one is secret and one is fully predictable to the holder of the key. No amount of black-box statistical testing distinguishes them. Cryptographic security is a statement about what a computationally bounded adversary can do, and you cannot test for the absence of a clever prediction algorithm by measuring distributions. It is established through cryptanalysis of the construction, not through test batteries on the output. Use Dieharder, NIST STS, and TestU01 to catch broken hardware and obvious implementation bugs; do not use a passing score as evidence that something is safe for cryptographic use.
Verdict
Keep the three categories straight and almost everything else follows. Use a fast non-crypto PRNG (Mersenne Twister, PCG, xorshift) only when there is no adversary and you may even want reproducibility: simulations, games, sampling, jitter. For anything an attacker benefits from predicting, use a CSPRNG, and in practice that means call the operating system: getrandom(2) or /dev/urandom on Linux, BCryptGenRandom on Windows, SecureRandom in Java, crypto/rand in Go, OsRng in Rust, crypto.getRandomValues in the browser, and the secrets module in Python. Never use random.random(), Math.random(), rand(), or any default PRNG for keys, tokens, nonces, IVs, or salts. The /dev/random versus /dev/urandom debate is over: after the CSPRNG is seeded they are identical, the perpetual-blocking behavior is gone, and getrandom() with default flags handles the only real risk — reading before initial seeding. Treat hardware RNGs like RDRAND as inputs to mix, never as a sole source, and let the kernel do the mixing. Remember that every famous randomness disaster — Debian OpenSSL, Mining your Ps and Qs, cloned VMs — was a seeding failure, not an algorithm failure, so the rule is to let the well-seeded OS generator do the work. And remember that statistical test suites catch broken hardware but cannot prove cryptographic security; passing BigCrush means nothing about predictability. The correct call is almost always the boring one: ask the operating system, and never reach past it.
Sources
- getrandom(2) — Linux manual page: https://man7.org/linux/man-pages/man2/getrandom.2.html
- random(4) / urandom — Linux manual page: https://man7.org/linux/man-pages/man4/random.4.html
- Wikipedia, /dev/random: https://en.wikipedia.org/wiki//dev/random
- Jason Donenfeld, “Random number generation” (kernel RNG documentation): https://www.kernel.org/doc/html/latest/admin-guide/sysctl/kernel.html
- Heninger, Durumeric, Wustrow, Halderman, “Mining Your Ps and Qs: Detection of Widespread Weak Keys in Network Devices” (USENIX Security 2012): https://factorable.net/weakkeys12.extended.pdf
- The Debian OpenSSL predictable PRNG vulnerability (CVE-2008-0166): https://www.debian.org/security/2008/dsa-1571
- NIST SP 800-90A Rev. 1, Recommendation for Random Number Generation Using Deterministic Random Bit Generators: https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-90Ar1.pdf
- NIST SP 800-22 Rev. 1a, A Statistical Test Suite for Random and Pseudorandom Number Generators: https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-22r1a.pdf
- L’Ecuyer and Simard, TestU01: https://simul.iro.umontreal.ca/testu01/tu01.html
- Python
secretsmodule documentation: https://docs.python.org/3/library/secrets.html - Daniel J. Bernstein, “ChaCha, a variant of Salsa20”: https://cr.yp.to/chacha/chacha-20080128.pdf
Comments