How do you generate random numbers in Rust? The usual answer is: use a crate like rand
or getrandom
. That's a useful answer, but not a very educational one. Those crates aren't magic — they must get their random numbers from somewhere. I like to implement things from scratch, so (out of stubbornness as well as curiosity) I've been collecting ways of generating random numbers using nothing but the standard library. Many of these methods should work in other programming languages as well.
Whether some of these can be called "random" is debatable, so maybe the better term to use is "unpredictable". You decide.
If you're interesting in randomness, it might be worth checking out how some popular operating systems do it. The Linux kernel's random number module is open-source, and in 2019 Microsoft released a white paper which gives a lot of details on how random numbers are generated on Windows. I'm not sure what the situation is like on macOS.
use std::time::{UNIX_EPOCH, SystemTime};
fn system_time() -> u128 {
SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_nanos()
}
By far the simplest and stupidest way: get the number of nanoseconds since the Unix epoch (the midnight of January 1, 1970). Running this a couple of times on my machine gives:
1739754187206481600 1739754191672008900 1739754196540256800 1739754200615486800
Since my computer's clock only has a precision of 100 ns, the last two digits are always zero. Similarly, the first few digits stay the same because each number is only a few seconds apart. But the ones in the middle look pretty good!
To quantify how "random" these numbers actually are, we can calculate the entropy of this function. In information theory, entropy represents how much information something has, or equivalently, how "unexpected" it is. It's calculated by simply taking the log2 of the number of possible values.
Let's say we use this function to generate some "randomness" when I start a program. If I, as a human, know when I do this to an precision of 0.1 seconds (that's 100,000,000 ns), then the number of possible numbers is 100,000,000 divided by 100 (the precision of the timer) which is 1,000,000.
The entropy is therefore log2(1,000,000), which is about 20. That means that this function is about as "random" (unpredictable) as a perfectly random 20-bit number.
It turns out that this method is Windows-approved:
Winload gets the current time during every boot, and uses it as an entropy input.
Rationale: there is no reason to believe this provides any good entropy, but it greatly reduces the chance of two machines producing the same RNG state if they boot from the same disk image.
—Windows white paper (page 12)
This is like the cooler version of Method 1. The way this works is that every modern x86 CPU (that's pretty much every Intel and AMD processor) has a special instruction called RDTSC
. This instruction lets us access the number of CPU cycles that have passed since the processor was powered on. To show how it works, here's an example using inline assembly:
use std::arch::asm;
fn tsc() -> u64 {
let tsc_high: u64;
let tsc_low: u64;
unsafe {
asm!(
"rdtsc",
out("eax") tsc_low,
out("edx") tsc_high,
options(nostack, nomem)
);
}
(tsc_high << 32) + tsc_low
}
This code is a little tricky to understand. When you call rdtsc
, the time stamp counter is returned as a 64-bit integer, but in kind of a weird way: the low 32 bits are in the eax
register, and the high 32 bits are in the edx
register. This code basically reads the values out of both registers and then glues them together using some bitshifts. The options(nostack, nomem)
part tells the compiler that our inline assembly doesn't affect process memory at all, allowing it to optimize more effectively.
If you think that inline assembly is too much of a hassle (understandably), Rust provides the _rdtsc()
intrinstic which returns the same value.
I tried running this and I got the value 2369595578570654
. According to Task Manager, my computer's uptime is 18 days and 5 hours. Doing the math, this comes out to about 1.5 billion cycles per second (1.5 GHz) on average. Neat!
Here's another method that involves time, but in a different way. The idea is to make the CPU do something useless, and measure how long that took.
use std::time::Instant;
use std::thread;
fn jitter() -> u64 {
let start = Instant::now();
thread::spawn(|| {}).join().unwrap(); // spawn a thread that does nothing
start.elapsed().as_nanos() as u64
}
This function measures how long it takes to spawn a thread that immediately returns. Calling this a couple times gives:
130700 126200 139900 116800 121500
That seems kind of random, but is it? It's hard to tell, which is the main issue with this method. Maybe your friend's computer always takes exactly the same amount of time to spawn a thread. You could also combine this function with Method 2 by measuring the time stamp counter before and after. Would that make it more random? No idea.
This function creates a temporary value on the stack and gets its address:
fn stack_address() -> usize {
let temp = 0;
&raw const temp as usize
}
This might be my favourite method just because of how simple and stupid it is. Because of something called address space layout randomization, which exists in every major operating system, the address of your program's stack is randomized every time you run it (the goal is to mitigate certain security vulnerabilities).
This is honest-to-god randomness. Trying it a few times:
832769685160 599178343976 394739578952 364080266200 14258076008
Make sure not to call it twice in the same program execution!
Recent Intel and AMD CPUs have a pair of special instructions to generate random numbers using thermal noise: RDRAND
and RDSEED
. Apparently the difference between the two is that RDSEED
is "really random" and RDRAND
is "kind of random" (see this Stack Exchange answer for more details) — so I'll use RDSEED
for my implementation. Once again, we can use inline assembly:
use std::arch::asm;
fn rdseed() -> Option<u64> {
let output: u64;
let succeeded: u8;
unsafe {
asm!(
"rdseed {}",
"setc {}",
out(reg) output,
out(reg_byte) succeeded,
options(nostack, nomem)
);
}
(succeeded == 1).then_some(output)
}
Note that RDRAND
and RDSEED
can both fail! The carry bit gets set to 1 if the function succeeds or 0 if it fails. The code uses the setc
instruction to read the carry bit into succeeded
. If succeeded
equals 1, return Some(<the number>)
, otherwise return None
. From my testing I never actually saw this function fail.
Rust offers a couple of intrinsics which wrap around this instruction, like _rdseed64_step().
Although these instructions seem convenient, they might not be the best choice for a few reasons:
RDSEED
takes about 900 ns for me).Despite the fact that randomness isn't officially in the standard library, there is one part of the standard library that uses randomness: hashmaps (and hashsets). The way hashmaps work is that you take a key and compute its hash, and based off of this result you choose a bucket to put your data in. There exists a potential security issue in that if an attacker is allowed to pick the keys, and they know your hash algorithm, they could choose "evil" keys that all fall into the same bucket. This could potentially result in your application slowing down to a crawl. This is called HashDoS.
Rust mitigates this using an algorithm called SipHash. SipHash uses a randomized secret value to hash the keys, meaning that an attacker can't predict what keys will fall into what bucket. It's actually pretty simple to get a random number from the std::hash
module:
use std::hash::{Hasher, BuildHasher, RandomState};
fn random_state() -> u64 {
RandomState::new().build_hasher().finish()
}
The reason why I call this a "hack" is you're not supposed to use this interface for generating general-purpose random numbers. There's no guarantee of it being uniformly random on every platform.
Wait, the standard library does have a way to get random numbers? Sort of. There is a std::random::random
function, but it exists only in the nightly compiler and it might be removed soon. So for now, you can do:
#![feature(random)]
use std::random::random;
fn main() {
let num = random::<u64>();
}
Pretty convenient.
This is a method that I haven't ever seen used before. All we do here is ask the OS how much free memory we have. On Windows, we can do this by creating a MEMORYSTATUSEX
struct and passing it into the GlobalMemoryStatusEx
function.
use std::mem;
#[allow(non_snake_case)]
#[repr(C)]
struct MEMORYSTATUSEX {
dwLength: u32,
dwMemoryLoad: u32,
ullTotalPhys: u64,
ullAvailPhys: u64,
ullTotalPageFile: u64,
ullAvailPageFile: u64,
ullTotalVirtual: u64,
ullAvailVirtual: u64,
ullAvailExtendedVirtual: u64,
}
extern "system" {
fn GlobalMemoryStatusEx(lpBuffer: *mut MEMORYSTATUSEX) -> i32;
}
fn available_memory() -> u64 {
let mut mem_status = mem::MaybeUninit::<MEMORYSTATUSEX>::uninit();
let ptr = mem_status.as_mut_ptr();
unsafe {
ptr.byte_add(mem::offset_of!(MEMORYSTATUSEX, dwLength))
.cast::<u32>()
.write(mem::size_of::<MEMORYSTATUSEX>() as u32);
assert_eq!(GlobalMemoryStatusEx(ptr), 1);
mem_status.assume_init().ullAvailPhys
}
}
On Linux this would have been a lot simpler, because I could just read from the /proc/meminfo
file.
Even when my computer is apparently idle, calling this gives variable results:
8908259328 8905326592 8899018752 8897945600 8903757824
Looks like the OS is always up to something.
What if you didn't trust your computer to generate truly random numbers? What if you wanted the absolute most random numbers possible? As it turns out, the Australian National University currently offers a free API to get numbers that are "generated in real-time in our lab by measuring the quantum fluctuations of the vacuum". To our understanding of physics, quantum fluctuations are true randomness in the strongest possible sense. Here's how to use the API to get 10 random numbers from 0 to 255:
use std::io::{self, Read, Write};
use std::net::TcpStream;
use std::time::Duration;
fn main() -> io::Result<()> {
let mut connection = TcpStream::connect("qrng.anu.edu.au:80")?;
connection.write_all(b"GET /API/jsonI.php?length=10&type=uint8 HTTP/1.1\r\nHost: anu.edu.au\r\nConnection: close\r\n\r\n")?;
connection.set_read_timeout(Some(Duration::from_secs(1)))?;
let mut data = String::new();
connection.read_to_string(&mut data)?;
println!("{}", data);
Ok(())
}
Running this code gives some nice random numbers:
HTTP/1.1 200 OK Date: Tue, 18 Feb 2025 05:40:04 GMT Server: Apache Access-Control-Allow-Origin: * Connection: close Transfer-Encoding: chunked Content-Type: application/json 56 {"type":"uint8","length":10,"data":[73,208,67,1,85,89,75,194,102,124],"success":true} 0
But when I ran it again, I got:
HTTP/1.1 500 Internal Server Error Date: Tue, 18 Feb 2025 05:40:33 GMT Server: Apache Content-Length: 143 Connection: close Content-Type: text/html; charset=iso-8859-1 The QRNG API is limited to 1 requests per minute. For more requests, please visit https://quantumnumbers.anu.edu.au or contact [email protected].
So maybe not the most efficient method.
Note that this request is being made using an insecure HTTP connection, which means that someone between my computer and the Australian National University could have peeked at these numbers. If we needed security, we would need to establish an HTTPS connection. (That would involve generating cryptographic keys using... random numbers... yeah)
I saved the best for last, because this is what you're actually supposed to do. This is what almost every random number library does under the hood. On Windows, there are a few APIs for random numbers, but apparently ProcessPrng
is the best. Here's how to use it:
use std::mem;
extern "system" {
fn ProcessPrng(pbData: *mut u8, cbData: usize) -> i32;
}
fn windows_random() -> u64 {
let mut num = mem::MaybeUninit::<u64>::uninit();
unsafe {
ProcessPrng(&raw mut num as _, mem::size_of_val(&num));
num.assume_init()
}
}
ProcessPrng
is very easy to use: just give it a pointer and a length. It's also guaranteed to succeed. But even so, Linux makes this a lot easier by providing the /dev/random
and /dev/urandom
files that I can just read from. Although, we can get more low-level by directly using the getrandom
syscall.
use std::mem;
use std::arch::asm;
fn linux_random() -> u64 {
let mut num = mem::MaybeUninit::<u64>::uninit();
unsafe {
asm!(
"syscall",
in("rax") 318,
in("rdi") &raw mut num,
in("rsi") mem::size_of_val(&num),
in("rdx") 0,
out("rcx") _,
out("r11") _,
);
num.assume_init()
}
}
When you call getrandom
, you have to pass in four parameters via registers: the syscall number (318 in this case), a pointer, a length, and flags (which can be kept as 0). You also need to tell the compiler that the syscall clobbers (sets to garbage values) the rcx
and r11
registers.
The getrandom
syscall can return an error, but according to the man page this should never happen when requesting 256 bytes or less, so I'm not checking the return value.
Generating random numbers is a fascinating subject, and there are tons of ways to go about it. Even if you never end up using these low-level methods, it's still good to know where your data is coming from.
Published: 2025-02-18