diff --git a/Cargo.lock b/Cargo.lock
index 475bb1b8208ae636c12ed705b1af981ebc3159d6..19d9a7ee3c5e67f21ef1c843edcef30dfc543910 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -238,6 +238,7 @@ dependencies = [
  "num-traits",
  "pci-ids",
  "qemu-exit",
+ "rand_chacha",
  "scopeguard",
  "shell-words",
  "smoltcp",
@@ -464,6 +465,12 @@ version = "0.2.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6"
 
+[[package]]
+name = "ppv-lite86"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
+
 [[package]]
 name = "proc-macro2"
 version = "1.0.47"
@@ -497,6 +504,16 @@ dependencies = [
  "rand_core",
 ]
 
+[[package]]
+name = "rand_chacha"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
+dependencies = [
+ "ppv-lite86",
+ "rand_core",
+]
+
 [[package]]
 name = "rand_core"
 version = "0.6.4"
diff --git a/Cargo.toml b/Cargo.toml
index 3039a82b63f7c3e4cff7df6ce1e54ce1a87dc605..1b677020330e545c64dfff49ead5c5064de282b2 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -84,6 +84,7 @@ pci-ids = { version = "0.2", optional = true }
 scopeguard = { version = "1.1", default-features = false }
 shell-words = { version = "1.1", default-features = false }
 qemu-exit = "3.0"
+rand_chacha = { version = "0.3", default-features = false }
 futures-lite = { version = "1.11", default-features = false, optional = true }
 async-task = { version = "4.3", default-features = false, optional = true }
 
diff --git a/src/arch/aarch64/kernel/processor.rs b/src/arch/aarch64/kernel/processor.rs
index 84011f8f8a88e0b5a76a97be2762cb281f2b4f12..e28dc9e3565b0ec4941492b5ac293fe73e575dc9 100644
--- a/src/arch/aarch64/kernel/processor.rs
+++ b/src/arch/aarch64/kernel/processor.rs
@@ -23,11 +23,7 @@ impl FPUState {
 	}
 }
 
-pub fn generate_random_number32() -> Option<u32> {
-	None
-}
-
-pub fn generate_random_number64() -> Option<u64> {
+pub fn seed_entropy() -> Option<[u8; 32]> {
 	None
 }
 
diff --git a/src/arch/x86_64/kernel/processor.rs b/src/arch/x86_64/kernel/processor.rs
index bb483504e8cd47d49150160516592597833eba26..eb7b9da0c4c40e6696f09504186955c6a1482693 100644
--- a/src/arch/x86_64/kernel/processor.rs
+++ b/src/arch/x86_64/kernel/processor.rs
@@ -2,8 +2,7 @@
 
 use core::arch::asm;
 use core::arch::x86_64::{
-	__rdtscp, _fxrstor, _fxsave, _mm_lfence, _rdrand32_step, _rdrand64_step, _rdtsc, _xrstor,
-	_xsave,
+	__rdtscp, _fxrstor, _fxsave, _mm_lfence, _rdseed64_step, _rdtsc, _xrstor, _xsave,
 };
 use core::convert::Infallible;
 use core::hint::spin_loop;
@@ -49,7 +48,7 @@ struct Features {
 	linear_address_bits: u8,
 	supports_1gib_pages: bool,
 	supports_avx: bool,
-	supports_rdrand: bool,
+	supports_rdseed: bool,
 	supports_tsc_deadline: bool,
 	supports_x2apic: bool,
 	supports_xsave: bool,
@@ -79,7 +78,7 @@ static FEATURES: Lazy<Features> = Lazy::new(|| {
 		linear_address_bits: processor_capacity_info.linear_address_bits(),
 		supports_1gib_pages: extend_processor_identifiers.has_1gib_pages(),
 		supports_avx: feature_info.has_avx(),
-		supports_rdrand: feature_info.has_rdrand(),
+		supports_rdseed: extended_feature_info.has_rdseed(),
 		supports_tsc_deadline: feature_info.has_tsc_deadline(),
 		supports_x2apic: feature_info.has_x2apic(),
 		supports_xsave: feature_info.has_xsave(),
@@ -893,32 +892,27 @@ pub fn print_information() {
 	infofooter!();
 }
 
-pub fn generate_random_number32() -> Option<u32> {
-	unsafe {
-		if FEATURES.supports_rdrand {
-			let mut value: u32 = 0;
-
-			for _ in 0..RDRAND_RETRY_LIMIT {
-				if _rdrand32_step(&mut value) == 1 {
-					return Some(value);
-				}
+pub fn seed_entropy() -> Option<[u8; 32]> {
+	let mut buf = [0; 32];
+	if FEATURES.supports_rdseed {
+		for word in buf.chunks_mut(8) {
+			let mut value = 0;
+
+			// Some RDRAND implementations on AMD CPUs have had bugs where the carry
+			// flag was incorrectly set without there actually being a random value
+			// available. Even though no bugs are known for RDSEED, we should not
+			// consider the default values random for extra security.
+			while unsafe { _rdseed64_step(&mut value) != 1 } || value == 0 || value == !0 {
+				// Spin as per the recommendation in the
+				// IntelĀ® Digital Random Number Generator (DRNG) implementation guide
+				spin_loop();
 			}
-		}
-		None
-	}
-}
 
-pub fn generate_random_number64() -> Option<u64> {
-	unsafe {
-		if FEATURES.supports_rdrand {
-			let mut value: u64 = 0;
-
-			for _ in 0..RDRAND_RETRY_LIMIT {
-				if _rdrand64_step(&mut value) == 1 {
-					return Some(value);
-				}
-			}
+			word.copy_from_slice(&value.to_ne_bytes());
 		}
+
+		Some(buf)
+	} else {
 		None
 	}
 }
diff --git a/src/entropy.rs b/src/entropy.rs
new file mode 100644
index 0000000000000000000000000000000000000000..08d55da06e32fd2241148a6ab12f4548d79516fd
--- /dev/null
+++ b/src/entropy.rs
@@ -0,0 +1,53 @@
+//! Cryptographically secure random data generation.
+//!
+//! This currently uses a ChaCha-based generator (the same one Linux uses!) seeded
+//! with random data provided by the processor.
+
+use hermit_sync::InterruptTicketMutex;
+use rand_chacha::rand_core::{RngCore, SeedableRng};
+use rand_chacha::ChaCha20Rng;
+
+use crate::arch::kernel::processor::{get_timer_ticks, seed_entropy};
+use crate::errno::ENOSYS;
+
+// Reseed every second for increased security while maintaining the performance of
+// the PRNG.
+const RESEED_INTERVAL: u64 = 1000000;
+
+bitflags! {
+	pub struct Flags: u32 {}
+}
+
+struct Pool {
+	rng: ChaCha20Rng,
+	last_reseed: u64,
+}
+
+static POOL: InterruptTicketMutex<Option<Pool>> = InterruptTicketMutex::new(None);
+
+/// Fills `buf` with random data, respecting the options in `flags`.
+///
+/// Returns the number of bytes written or `-ENOSYS` if the system does not support
+/// random data generation.
+pub fn read(buf: &mut [u8], _flags: Flags) -> isize {
+	let pool = &mut *POOL.lock();
+	let now = get_timer_ticks();
+	let pool = match pool {
+		Some(pool) if now.saturating_sub(pool.last_reseed) <= RESEED_INTERVAL => pool,
+		pool => {
+			if let Some(seed) = seed_entropy() {
+				pool.insert(Pool {
+					rng: ChaCha20Rng::from_seed(seed),
+					last_reseed: now,
+				})
+			} else {
+				return -ENOSYS as isize;
+			}
+		}
+	};
+
+	pool.rng.fill_bytes(buf);
+	// Slice lengths are always <= isize::MAX so this return value cannot conflict
+	// with error numbers.
+	buf.len() as isize
+}
diff --git a/src/lib.rs b/src/lib.rs
index a5c7ae99800aa5474ef4eeabf5d62bfc22bacc01..39c47acd8e5bdd7b6af4447e07af42ba810a453c 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -75,6 +75,7 @@ mod arch;
 mod config;
 mod console;
 mod drivers;
+mod entropy;
 mod env;
 pub mod errno;
 pub(crate) mod fd;
diff --git a/src/macros.rs b/src/macros.rs
index 5c65c6829a4308cc4cdf724bf41095993a5c0bca..8697ace3cf8642b953918713dde5ad5e64a18830 100644
--- a/src/macros.rs
+++ b/src/macros.rs
@@ -42,18 +42,6 @@ macro_rules! dbg {
     };
 }
 
-macro_rules! try_sys {
-	($expr:expr $(,)?) => {
-		match $expr {
-			::core::result::Result::Ok(val) => val,
-			::core::result::Result::Err(err) => {
-				error!("{err}");
-				return -1;
-			}
-		}
-	};
-}
-
 /// Runs `f` on the kernel stack.
 ///
 /// All arguments and return values have to fit into registers:
diff --git a/src/syscalls/random.rs b/src/syscalls/entropy.rs
similarity index 50%
rename from src/syscalls/random.rs
rename to src/syscalls/entropy.rs
index 4ee3b7937ce1640896fd7c4cb6ebad49527bfe22..d9c679ab74136b259a6d0272eae46f0428009635 100644
--- a/src/syscalls/random.rs
+++ b/src/syscalls/entropy.rs
@@ -1,6 +1,11 @@
+use core::mem::size_of;
+use core::slice;
+
 use hermit_sync::TicketMutex;
 
 use crate::arch;
+use crate::entropy::{self, Flags};
+use crate::errno::EINVAL;
 
 static PARK_MILLER_LEHMER_SEED: TicketMutex<u32> = TicketMutex::new(0);
 const RAND_MAX: u64 = 2_147_483_647;
@@ -12,42 +17,74 @@ fn generate_park_miller_lehmer_random_number() -> u32 {
 	random
 }
 
-unsafe extern "C" fn __sys_rand32(value: *mut u32) -> i32 {
-	let rand = try_sys!(arch::processor::generate_random_number32().ok_or("sys_rand32 failed"));
-	unsafe {
-		value.write(rand);
-	}
-	0
-}
+unsafe extern "C" fn __sys_read_entropy(buf: *mut u8, len: usize, flags: u32) -> isize {
+	let Some(flags) = Flags::from_bits(flags) else { return -EINVAL as isize  };
 
-unsafe extern "C" fn __sys_rand64(value: *mut u64) -> i32 {
-	let rand = try_sys!(arch::processor::generate_random_number64().ok_or("sys_rand64 failed"));
-	unsafe {
-		value.write(rand);
-	}
-	0
+	let buf = unsafe {
+		// Cap the number of bytes to be read at a time to isize::MAX to uphold
+		// the safety guarantees of `from_raw_parts`.
+		let len = usize::min(len, isize::MAX as usize);
+		buf.write_bytes(0, len);
+		slice::from_raw_parts_mut(buf, len)
+	};
+
+	entropy::read(buf, flags)
 }
 
-extern "C" fn __sys_rand() -> u32 {
-	generate_park_miller_lehmer_random_number()
+/// Fill `len` bytes in `buf` with cryptographically secure random data.
+///
+/// Returns either the number of bytes written to buf (a positive value) or
+/// * `-EINVAL` if `flags` contains unknown flags.
+/// * `-ENOSYS` if the system does not support random data generation.
+#[no_mangle]
+pub unsafe extern "C" fn sys_read_entropy(buf: *mut u8, len: usize, flags: u32) -> isize {
+	kernel_function!(__sys_read_entropy(buf, len, flags))
 }
 
 /// Create a cryptographicly secure 32bit random number with the support of
 /// the underlying hardware. If the required hardware isn't available,
-/// the function returns `None`.
+/// the function returns `-1`.
 #[cfg(not(feature = "newlib"))]
 #[no_mangle]
 pub unsafe extern "C" fn sys_secure_rand32(value: *mut u32) -> i32 {
-	kernel_function!(__sys_rand32(value))
+	let mut buf = value.cast();
+	let mut len = size_of::<u32>();
+	while len != 0 {
+		let res = unsafe { sys_read_entropy(buf, len, 0) };
+		if res < 0 {
+			return -1;
+		}
+
+		buf = unsafe { buf.add(res as usize) };
+		len -= res as usize;
+	}
+
+	0
 }
 
 /// Create a cryptographicly secure 64bit random number with the support of
 /// the underlying hardware. If the required hardware isn't available,
-/// the function returns `None`.
+/// the function returns -1.
 #[cfg(not(feature = "newlib"))]
 #[no_mangle]
 pub unsafe extern "C" fn sys_secure_rand64(value: *mut u64) -> i32 {
-	kernel_function!(__sys_rand64(value))
+	let mut buf = value.cast();
+	let mut len = size_of::<u64>();
+	while len != 0 {
+		let res = unsafe { sys_read_entropy(buf, len, 0) };
+		if res < 0 {
+			return -1;
+		}
+
+		buf = unsafe { buf.add(res as usize) };
+		len -= res as usize;
+	}
+
+	0
+}
+
+extern "C" fn __sys_rand() -> u32 {
+	generate_park_miller_lehmer_random_number()
 }
 
 /// The function computes a sequence of pseudo-random integers
@@ -68,7 +105,7 @@ pub extern "C" fn sys_srand(seed: u32) {
 	kernel_function!(__sys_srand(seed))
 }
 
-pub(crate) fn random_init() {
+pub(crate) fn init_entropy() {
 	let seed: u32 = arch::processor::get_timestamp() as u32;
 
 	*PARK_MILLER_LEHMER_SEED.lock() = seed;
diff --git a/src/syscalls/mod.rs b/src/syscalls/mod.rs
index 36e18331ea8986909f7a3742b3bc6f296384b8ad..a1cd3320cdf51a4b122cc64a05c51cce388a0bb3 100644
--- a/src/syscalls/mod.rs
+++ b/src/syscalls/mod.rs
@@ -5,9 +5,9 @@ use hermit_sync::InterruptTicketMutex;
 use hermit_sync::Lazy;
 
 pub use self::condvar::*;
+pub use self::entropy::*;
 pub use self::futex::*;
 pub use self::processor::*;
-pub use self::random::*;
 pub use self::recmutex::*;
 pub use self::semaphore::*;
 pub use self::spinlock::*;
@@ -21,6 +21,7 @@ use crate::syscalls::interfaces::SyscallInterface;
 use crate::{__sys_free, __sys_malloc, __sys_realloc};
 
 mod condvar;
+mod entropy;
 pub(crate) mod fs;
 mod futex;
 mod interfaces;
@@ -29,7 +30,6 @@ mod lwip;
 #[cfg(all(feature = "tcp", not(feature = "newlib")))]
 mod net;
 mod processor;
-mod random;
 mod recmutex;
 mod semaphore;
 mod spinlock;
@@ -68,7 +68,7 @@ pub(crate) fn init() {
 	// Perform interface-specific initialization steps.
 	SYS.init();
 
-	random_init();
+	init_entropy();
 	#[cfg(feature = "newlib")]
 	sbrk_init();
 }