This is why I use, in security critical contents of my software (where the numbers have to be computationally infeasible to produce), a type of random number generator called an XOF (extendable-output function).
It takes entropy from multiple different sources, makes it all input to the XOF, then the XOF uses cryptography to output a stream that has as much entropy as the combined entropy of all of its sources of randomness. So if an XOF, for example, takes 100 runs of rdrand16, along with the system time in microseconds and the number of milliseconds between receiving 100 packets over the network, the XOF will output a completely random stream without artifacts like never returning 0x0000, even if rdrand16 never outputs 0x0000.
Yes, /dev/(u)random is supposed to do that, but what if there’s a bug in a kernel (e.g. some embedded system which may not even be running Linux) which causes /dev/(u)ramdom to be less than secure? There’s also issues where, for example, it may no longer be possible to read /dev/(u)random after putting the process in a chroot() sandbox (chroot() isn’t defined in POSIX so its behavior is not guaranteed to be consistent across multiple operating systems).
getrandom() is often times suggested, but alas isn’t a standardized function, i.e. it’s not part of the POSIX specification. Considering how the C23 changes to the C specification caused a lot of perfectly good C code to no longer compile, I’m very anal about sticking to specs; I use '-std=C99' for my code these days (even though it can compile as C23 code) and stick to POSIX functions (except chroot() and setgroups(), but both of those predate POSIX, and even here I have a compile-time option to compile my code without those non-POSIX syscalls).
The code using a secure XOF (the algorithm was developed by the same team which later on made SHA-3, and includes people who helped make AES) has been around for nearly two decades (the code where I roll my own RNG to make secure random numbers has been around for over 25 years, but used AES before XOFs existed) and not one security problem has found with the RNG code has ever been found. [1] “Don’t roll your own RNG” is a suggestion, but it is possible to do so securely if one knows what they are doing (i.e. they have read Applied Cryptography and keep current with cryptographic developments).
For anything vibe coded (my code is 100% human written, for the record), rolling one’s own RNG is a really bad idea.
[1] There was a theoretical issue with cache timing attacks over two decades ago, so I put mitigations in place, and then chose to use an XOF for newer code.
[2] There was an issue where a separate implementation I made of this XOF would generate incorrect test vectors in clang, but only at some optimization levels. I now test the XOF in both GCC and clang at multiple optimization levels to make sure it acts correctly.
> getrandom() is often times suggested, but alas isn’t a standardized function
The POSIX standard function is getentropy(), which internally calls getrandom() on Linux.
> what if there’s a bug in the kernel which causes /dev/(u)ramdom to be less than secure?
It's often the other way around: the Linux kernel contains thousands of workarounds for buggy hardware, while the buggy hardware itself doesn't always get patched. Linux developers take this stuff very seriously. As a result it's often safer to rely on kernel APIs than to access the hardware directly.
The kernel code involving random number generation receives an exceptionally high amount of scrutiny because of its security implications, so I'd trust it to do the right thing over a naked call to RDRAND which nobody knows how exactly it's implemented in proprietary hardware or a handrolled solution to mix the RDRAND output with other entropy sources.
Remember the Debian openssl disaster from 2008? That happened exactly because someone had handrolled their entropy mixing solution, then someone else broke it.
From <a href="https://pubs.opengroup.org/onlinepubs/9799919799/functions/getentropy.html" rel="nofollow">https://pubs.opengroup.org/onlinepubs/9799919799/functions/g...
“The intended use of this function is to create a seed for other pseudo-random number generators”
So, if I were to use genentropy() in a POSIX-compliant way, I would need to do what I already do: Use my own pseudo-random number generator.
The Debian openssl disaster (CVE 2008-0166, I remember it well) was caused because someone incorrectly patched secure code: Since the code used uninitialized memory as one of many entropy sources, which causes Valgrind to complain, they patched the code to not use uninitialized memory for entropy, but then accidentally disabled all other sources of entropy (except the 16-bit PID). It was caused because the person making the patch didn’t fully understand why it was a good idea to, in that context, use code which Valgrind complained about. [1]
As an aside, here’s how I deal with those Valgrind errors:
#ifdef VALGRIND_NOERRORS
/* Valgrind reports our intentional use of values of uncleared
* allocated memory as one source of entropy as an error, so we
* allow it to be disabled for Valgrind testing */
memset(noise,0,512);
#endif /* VALGRIND_NOERRORS */
I do believe the Linux Kernel does have secure RNG code, but I also write code which has run on a lot of different systems and environments, including embedded ones, and some of them might not have a secure /dev/urandom.
[1] Debian has a lot of inflexible policies like this which can cause problems. Another issue Debian has is they have a policy a given piece of code must always compile to the same binary on a given architecture. That isn’t true with the unpatched version of my code, because the hash compression routine uses a 32-bit random number generated at compile time to avoid hash collision attacks (it also uses another 32-bit random number at runtime, and I make sure the hash compression values are never visible). So the Debian version of my code was forced to be patched to be less secure.
Uninitialized memory should never be used as source of entropy. Most release software these days compiles using hardening flags, which will (at some levels) replace uninitialized memory with sentinel values, making the entropy of uninitialized memory frequently around 0.
But it gets worse. If the optimizer sees that you're loading uninitialized memory, it can reason that since the result of uninitialized memory is garbage, doing any computation on that result is also garbage, and happily delete said computation as a result. The cascading effect of this is to delete all of the entropy-mixing code, leaving your entropy pool with only the very low entropy source--giving uninitialized memory effectively negative entropy.
The net effect is that, at least for me, seeing someone trying to seed an entropy pool with uninitialized memory is a giant neon flashing sign saying "do not trust this code." It provides at best very little entropy and at worst actively destroys entropy and has other calamitous effects like valgrind or sanitizer errors, so you need to have other entropy sources anyways, so why bother?
If the optimizer sees that you're loading uninitialized memory, it can reason that since the result of uninitialized memory is garbage, doing any computation on that result is also garbage, and happily delete said computation as a result
This is an interesting assertion, and one that is easy enough to prove true.
Let’s take the following C code, which uses the same XOF algorithm (but not implementation) as my application (Deadwood):
This code, as I’m sure the parent poster can clearly see, uses four bits of uninitialized allocated memory as its source of entropy. As per the parent’s assertion, there should therefore exist a compiler whose optimizer will cause this XOF to not correctly run.
The above code can have one of the following possible 16 outputs:
If the above code has any but one of the above 16 outputs, this is a real world case where a C compiler, seeing uninitialized memory being used, optimizes out the code which uses said uninitialized memory as an input, and therefore will not output one of the above 16 possible words.
I’ve tested the above code in GCC -O3 and clang -O3; both generate one of the above 16 possible outputs (each one generating a different output).
If there really is a compiler out there which does “happily delete said computation”, which would give a different output than one of the 16 outputs above, please name that compiler, the version of said compiler used, and all compile-time flags used with said compiler.
While I’ve never heard of a real world case where a compiler would refuse to run code using uninitialized memory as yet another source of entropy for a secure PRNG, I do know of a real world case where a very nasty security hole was caused because someone incorrectly removed code using uninitialized memory as part of an entropy pool: CVE-2008-0166
Remember that a compiler is allowed to do anything when it sees undefined behavior, which includes doing the thing you want it to do.
Here's a little example of code disappearing due to a read of uninitialized memory:
void test(int x) {
int uninit;
puts("hello");
if (uninit)
puts("non-zero");
else
puts("zero");
}
clang 23.1.0 -O3 targeting ARMv8 deletes both branches of the if. Not only that, it deletes the code to return from the function. The very last instruction of the function is `bl puts`, meaning that after puts returns, it will start executing whatever function happened to come after this one in memory. That's probably a good thing in context, because that's likely to crash or infinite loop and make it clear that something went badly wrong, but the failure could easily be something more subtle that just disables some random seeding while otherwise executing normally.
I concede the C99 specification (which I now have a copy of), on page 490 (502 of the PDF) states “The behavior is undefined in the following circumstances:” this is followed by a long list, and on page 501 (page 513 of the PDF) it says, one case where behavior is undefined is when “The value of the object allocated by the malloc function is used”
It’s not clear whether that is the memory location malloc() returns or the memory pointed to by malloc(), but based on the next item in the list of cases where behavior is undefined, we have “The value of any bytes in a new object allocated by the realloc function beyond the size of the old object are used [results in undefined behavior]”.
The good news is that, as Taek and sltkr have pointed out elsewhere in the thread, clock_gettime() gets us a tiny bit of entropy, not perfect, but better than nothing. clock_gettime() is also POSIX compliant, although I remember about 15 years ago macOS didn’t support clock_gettime() (I checked, and it does these days).
getentropy() will become better than /dev/urandom for kernel level random numbers, but the problem is that getentropy() was only standardized and added to POSIX in 2024—too recent for me to feel 100% sure it’s widely implemented. And, yes, /dev/urandom (like chroot(), like sergroups()) isn’t defined in POSIX but it’s widely used.
strenholme · · focus · HN ↗
It takes entropy from multiple different sources, makes it all input to the XOF, then the XOF uses cryptography to output a stream that has as much entropy as the combined entropy of all of its sources of randomness. So if an XOF, for example, takes 100 runs of rdrand16, along with the system time in microseconds and the number of milliseconds between receiving 100 packets over the network, the XOF will output a completely random stream without artifacts like never returning 0x0000, even if rdrand16 never outputs 0x0000.
stingraycharles · · focus · HN ↗
I fail to see why one should either rely on a single random source nor roll their own.
strenholme · · focus · HN ↗
getrandom() is often times suggested, but alas isn’t a standardized function, i.e. it’s not part of the POSIX specification. Considering how the C23 changes to the C specification caused a lot of perfectly good C code to no longer compile, I’m very anal about sticking to specs; I use '-std=C99' for my code these days (even though it can compile as C23 code) and stick to POSIX functions (except chroot() and setgroups(), but both of those predate POSIX, and even here I have a compile-time option to compile my code without those non-POSIX syscalls).
The code using a secure XOF (the algorithm was developed by the same team which later on made SHA-3, and includes people who helped make AES) has been around for nearly two decades (the code where I roll my own RNG to make secure random numbers has been around for over 25 years, but used AES before XOFs existed) and not one security problem has found with the RNG code has ever been found. [1] “Don’t roll your own RNG” is a suggestion, but it is possible to do so securely if one knows what they are doing (i.e. they have read Applied Cryptography and keep current with cryptographic developments).
For anything vibe coded (my code is 100% human written, for the record), rolling one’s own RNG is a really bad idea.
[1] There was a theoretical issue with cache timing attacks over two decades ago, so I put mitigations in place, and then chose to use an XOF for newer code.
[2] There was an issue where a separate implementation I made of this XOF would generate incorrect test vectors in clang, but only at some optimization levels. I now test the XOF in both GCC and clang at multiple optimization levels to make sure it acts correctly.
sltkr · · focus · HN ↗
The POSIX standard function is getentropy(), which internally calls getrandom() on Linux.
> what if there’s a bug in the kernel which causes /dev/(u)ramdom to be less than secure?
It's often the other way around: the Linux kernel contains thousands of workarounds for buggy hardware, while the buggy hardware itself doesn't always get patched. Linux developers take this stuff very seriously. As a result it's often safer to rely on kernel APIs than to access the hardware directly.
The kernel code involving random number generation receives an exceptionally high amount of scrutiny because of its security implications, so I'd trust it to do the right thing over a naked call to RDRAND which nobody knows how exactly it's implemented in proprietary hardware or a handrolled solution to mix the RDRAND output with other entropy sources.
Remember the Debian openssl disaster from 2008? That happened exactly because someone had handrolled their entropy mixing solution, then someone else broke it.
strenholme · · focus · HN ↗
“The intended use of this function is to create a seed for other pseudo-random number generators”
So, if I were to use genentropy() in a POSIX-compliant way, I would need to do what I already do: Use my own pseudo-random number generator.
The Debian openssl disaster (CVE 2008-0166, I remember it well) was caused because someone incorrectly patched secure code: Since the code used uninitialized memory as one of many entropy sources, which causes Valgrind to complain, they patched the code to not use uninitialized memory for entropy, but then accidentally disabled all other sources of entropy (except the 16-bit PID). It was caused because the person making the patch didn’t fully understand why it was a good idea to, in that context, use code which Valgrind complained about. [1]
As an aside, here’s how I deal with those Valgrind errors:
I do believe the Linux Kernel does have secure RNG code, but I also write code which has run on a lot of different systems and environments, including embedded ones, and some of them might not have a secure /dev/urandom.[1] Debian has a lot of inflexible policies like this which can cause problems. Another issue Debian has is they have a policy a given piece of code must always compile to the same binary on a given architecture. That isn’t true with the unpatched version of my code, because the hash compression routine uses a 32-bit random number generated at compile time to avoid hash collision attacks (it also uses another 32-bit random number at runtime, and I make sure the hash compression values are never visible). So the Debian version of my code was forced to be patched to be less secure.
jcranmer · · focus · HN ↗
But it gets worse. If the optimizer sees that you're loading uninitialized memory, it can reason that since the result of uninitialized memory is garbage, doing any computation on that result is also garbage, and happily delete said computation as a result. The cascading effect of this is to delete all of the entropy-mixing code, leaving your entropy pool with only the very low entropy source--giving uninitialized memory effectively negative entropy.
The net effect is that, at least for me, seeing someone trying to seed an entropy pool with uninitialized memory is a giant neon flashing sign saying "do not trust this code." It provides at best very little entropy and at worst actively destroys entropy and has other calamitous effects like valgrind or sanitizer errors, so you need to have other entropy sources anyways, so why bother?
strenholme · · focus · HN ↗
This is an interesting assertion, and one that is easy enough to prove true.
Let’s take the following C code, which uses the same XOF algorithm (but not implementation) as my application (Deadwood):
This code, as I’m sure the parent poster can clearly see, uses four bits of uninitialized allocated memory as its source of entropy. As per the parent’s assertion, there should therefore exist a compiler whose optimizer will cause this XOF to not correctly run.The above code can have one of the following possible 16 outputs:
If the above code has any but one of the above 16 outputs, this is a real world case where a C compiler, seeing uninitialized memory being used, optimizes out the code which uses said uninitialized memory as an input, and therefore will not output one of the above 16 possible words.I’ve tested the above code in GCC -O3 and clang -O3; both generate one of the above 16 possible outputs (each one generating a different output).
If there really is a compiler out there which does “happily delete said computation”, which would give a different output than one of the 16 outputs above, please name that compiler, the version of said compiler used, and all compile-time flags used with said compiler.
While I’ve never heard of a real world case where a compiler would refuse to run code using uninitialized memory as yet another source of entropy for a secure PRNG, I do know of a real world case where a very nasty security hole was caused because someone incorrectly removed code using uninitialized memory as part of an entropy pool: CVE-2008-0166
wat10000 · · focus · HN ↗
Here's a little example of code disappearing due to a read of uninitialized memory:
clang 23.1.0 -O3 targeting ARMv8 deletes both branches of the if. Not only that, it deletes the code to return from the function. The very last instruction of the function is `bl puts`, meaning that after puts returns, it will start executing whatever function happened to come after this one in memory. That's probably a good thing in context, because that's likely to crash or infinite loop and make it clear that something went badly wrong, but the failure could easily be something more subtle that just disables some random seeding while otherwise executing normally.strenholme · · focus · HN ↗
It’s not clear whether that is the memory location malloc() returns or the memory pointed to by malloc(), but based on the next item in the list of cases where behavior is undefined, we have “The value of any bytes in a new object allocated by the realloc function beyond the size of the old object are used [results in undefined behavior]”.
The good news is that, as Taek and sltkr have pointed out elsewhere in the thread, clock_gettime() gets us a tiny bit of entropy, not perfect, but better than nothing. clock_gettime() is also POSIX compliant, although I remember about 15 years ago macOS didn’t support clock_gettime() (I checked, and it does these days).
getentropy() will become better than /dev/urandom for kernel level random numbers, but the problem is that getentropy() was only standardized and added to POSIX in 2024—too recent for me to feel 100% sure it’s widely implemented. And, yes, /dev/urandom (like chroot(), like sergroups()) isn’t defined in POSIX but it’s widely used.