Fun ways to generate random numbers in Rust

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.

1. The System Time

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)

2. The Time Stamp Counter

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!

3. CPU Timing Jitter

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.

4. Address Space Layout Randomization

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!

5. RDRAND and RDSEED

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:

6. The Hashmap Hack

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.

7. std::random

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.

8. System Memory

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.

9. Quantum Randomness

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)

10. Asking Your OS

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.

Conclusion

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