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
Parent is right. Use of uninitialized memory is UB, and incidentally, the type of UB that the C standard is not working to define, but is relying on sanitizers to find in source programs, since it is considered always a bug.
This entire thread has a lot of "no security issues have ever been found in my code, and I test a lot. Therefore no bugs will ever exist in my code and we're all safe." To see you doing this in an explicitly security-conscious setting is distressing.
If anything, I see assertions like this and juxtaposed with blatant, willful misunderstanding of how C and C compilers work and it does the opposite of inspiring confidence.
Look at CVE-2009-1897; this is the classic example of how C compilers are happy to try to optimize code in the face of UB and lead to worse problems.
> If the above code has any but one of the above 16 outputs
I don't think you understand how insane optimizations in the face of UB can be. Just go look at this issue:
>>>no security issues have ever been found in my code, and I test a lot. Therefore no bugs will ever exist in my code and we're all safe<<<
That’s not what I have said. 35 issues (mostly minor, but a couple of remote denial of service attacks) have been found with my code in the last 25 years; of those, none have come from the PRNG code I used. Here in the age of AI, I get multiple security reports a year, so the code is being looked at.
With crypto, you can never know for sure the code doesn’t have weaknesses, but one can have confidence in code and algorithms which have been around for years without any weaknesses discovered in them.
My question is: If code being around for years doesn’t build confidence in it being secure, what would it take to build confidence in the code.
What you’re seeing here is two schools of thought: One is the issue with using uninitialized memory, which yes does result in undefined behavior as per the C99 spec—but, back two decades ago when I made that decision, GCC was the only compiler of significance (clang was just released but was not widely used until years later) and its behavior was to put randomish data in undefined allocated memory.
The other is the notion that only Linux Kernel developers can develop a secure PRNG, and obviously I find that attitude very condescending and arrogant.
> My question is: If code being around for years doesn’t build confidence in it being secure, what would it take to build confidence in the code.
One of my experiences with programmers is that we (and I do not exclude myself from this category) are extraordinarily bad at sufficiently imagining the failure paths that our code might take and making code work handle failure cases correctly. It's these erroneous failure paths that are the real issue with code, and age doesn't really indicate how much testing of those failure paths actually exist.
To build confidence in code, what we need is proactive testing of potential failure paths that don't rely on humans to think of them in the first place--that means investment in various exhaustive testing techniques. (And I'd also like to see formal verification be more of a thing, but the tech just isn't there.) A stepping stone in that regard is also heavy use of static and dynamic analyzers to catch things that at known to be Obviously Bad™. The gold standard here really is whitebox concolic execution that's specifically trying to get something akin to 100% path coverage by trying to synthesize inputs to test the unhit paths.
Saying that it's okay to seed an entropy pool with uninitialized memory in 2005 is maybe defensible. There is a shift in compiler design around that time from thinking of it as compiling to a set of instructions and then optimizing them (so that the basic 'structure' of the code is something that's inherent to the program) towards looking at program semantics as abstract things where the only thing you need to preserve are the observable semantics [1]. One of the side effects of that shift is that undefined behavior stops being something that is fairly reliable so you assume you get the 'equivalent' assembly effects for that machine and starts being something that really screws over code.
But it's not 2005; it's 2026, and this change in compilers has been heavily advertised, discussed, complained about for well over a decade. And if you're using the kind of tools that give me confidence in code, those tools would have been bitching about that behavior for decades. If this is a surprise to you in this time and age, then it suggests to me that you've not really been proactive in trying to test your code in the manner I suggest, or worse, you have been proactive and decided to ignore everything telling you your practices are wrong because you know better than the tools and your code isn't obviously wrong.
(I say obviously wrong because your code example does demonstrate, when I tried it in the latest version of clang on godbolt, that it is eliminating the seeding of the entropy pool, in a way that is actually pretty clear if you read the assembly.)
[1] One of the most concrete examples to really observe the difference is the concept of control flow. Compilers nowadays are really happy to turn control flow (if statements) into dataflow (conditional moves or funky bit manipulations) and vice versa, because the only thing that needs to be preserved is the final value. Of course, cryptographers keep complaining that we broke their code by turning their obfuscated dataflow-based if statement into an actual if statement and so it's no longer constant-time, no matter how many times we keep telling them that we do not make any pretense of guaranteeing constant-time execution of code.
>>>we (and I do not exclude myself from this category) are extraordinarily bad at sufficiently imagining the failure paths that our code might take and making code work handle failure cases correctly<<<
The way I somewhat work around this with the newer coLunacyDNS code (from 2020) is by using `-DGCOV` and `gcov` to check the code coverage when running the automated SQA tests for the code. I can’t cover every single failure that could be caused by sanity tests in the C code, but I can cover pretty much all (99.53%) other code.
>>>But it's not 2005; it's 2026, and this change in compilers has been heavily advertised, discussed, complained about for well over a decade.<<<
My code compiles to the C99 standard (-std=c99 and only two syscalls not defined in POSIX) [1]. This in mind, compiler makers have a responsibility to make sure that their compilers, no matter what changes they introduce to them, conform to the C99 spec when compiling with the -std=c99 flag. [2]
This means that when I interact with people working on compilers, I bring out the C99 spec and then use that to determine whether it’s a bug in my code or a bug with the compiler. In this particular case, the C99 spec said it results in undefined behavior when “The value of the object allocated by the malloc function is used”, so that’s a bug with my code.
The thing about standards is this: A given piece of C code, if standards compliant, should, when compiled, act a given way with any compiler conformant with that standard. C developers writing C99 code shouldn’t have to look at any development or document which exists after 1999 to determine whether their code will act a given way. C compiler writers shouldn’t be telling C99 developers “well, you should know about this 2021 change to the C compiler”. They should instead say, “well, if you look at this page of the C99 spec, that behavior is undefined so we have no obligation to implement it the same way GCC does”.
Standards correct C99 code written in 2005 should behave the same way when compiled in 2026 as it did in 2005.
This discussion is like the fights guys get into when playing wargames where they argue whether a given move in the game is legal or not. When this happens, the correct thing to do is to look at the reference manual and see what that says.
>>>it's okay to seed an entropy pool with uninitialized memory in 2005 is maybe defensible<<<
Back when I made that decision, clock_gettime() was not universally implemented (it wasn’t implemented on MacOS), so my options for having some kind of entropy for the XOF should /dev/urandom have issues were very limited. I’ve since updated the code to use clock_gettime(); the Windows port will instead use the non-portable GetSystemTimeAsFileTime() (ghosts of embrace/extend/extinguish). [3]
>>>cryptographers keep complaining that we broke their code by turning their obfuscated dataflow-based if statement into an actual if statement<<<
The cryptography I use, as is typical for post-AES cryptography, makes sure that the cryptographic core doesn’t use any control flow statements, as seen in this compact representation of that code: [4]
[1] The code also assumes that /dev/urandom returns a random stream of bytes, a behavior which POSIX doesn’t specify (newer POSIX finally gives us randomness with getentropy() but that spec is too new for me to assume it’s widely implemented)
[2] Until about two years ago, -std=c99 wasn’t needed; C99 code happily compiled as recently as 2022.
[3] Let me make this crystal clear: I use both /dev/urandom and looking at jitter with clock_gettime() in the entropy pool my XOF PRNG uses. Should one of those not have enough entropy, the PRNG is still as secure as the other source of entropy.
[4] I very rigorously made sure that k>>j%32|k<<-j%32 trick works to do a bit rotate while being C99 standards compliant because clang broke an earlier version of this bit rotate at some optimization values; note that j and k are uint32_t variables. Looking at the relevant parts of the standards show this trick only works when the modulo is a power of 2.
The production code either uses x>>r|x<<(32-r)%32 or this:
r = ((i * (i + 1)) / 2) % DWR_WORDSIZE;
// Other code not shown
if(r > 0 && r < DWR_WORDSIZE) {
A[i] = (x >> r) | (x << (DWR_WORDSIZE - r));
} else {
A[i] = x;
}
The “if” isn’t a security issue because r has a predictable value which we assume the attacker already knows.
> The thing about standards is this: A given piece of C code, if standards compliant, should, when compiled, act a given way with any compiler conformant with that standard. C developers writing C99 code shouldn’t have to look at any development or document which exists after 1999 to determine whether their code will act a given way. C compiler writers shouldn’t be telling C99 developers “well, you should know about this 2021 change to the C compiler”. They should instead say, “well, if you look at this page of the C99 spec, that behavior is undefined so we have no obligation to implement it the same way GCC does”.
The thing about standards is this: we have the same ability to write large, bug-free specifications as we do to write large, bug-free applications--effectively none. Bugs in the specification can take years or even decades to be discovered, and then the interpretation adjudicated and fixed in a newer version of the standard, with the fossil C99 specification never being updated or given any indication that the original text was buggy. On top of that, compilers don't implement C99, they implement C99-with-compiler-extensions, and those compiler extensions' documentation range from poor to atrocious.
> Standards correct C99 code written in 2005 should behave the same way when compiled in 2026 as it did in 2005.
Standards-correct code means not hitting UB. The number of programs that exhibit UB is approximately 100%, especially in 2005 (which is about when GCC started optimizing based on C's effective type rules). The best way to figure out whether or not your code is standards-correct generally isn't to read the standard [1]. Instead, go run a suite of undefined behavior sanitizers on your code to see if your code is known to violate some of the rules. We unfortunately don't have checkers for all the known UBs (for example, effective type rules).
[1] The standard is hard to read, especially because you have to know where to track down more authoritative sources to be able to resolve interpretation issues. I'll note that you've both incorrectly identified the source of undefined behavior and incorrectly identified where to find the undefined behavior--you're citing Annex J, which is an informative section, meaning it doesn't actually mean anything as far as interpretation goes (and I'm aware of at least one entry in there which is outright incorrect).
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
titzer · · focus · HN ↗
This entire thread has a lot of "no security issues have ever been found in my code, and I test a lot. Therefore no bugs will ever exist in my code and we're all safe." To see you doing this in an explicitly security-conscious setting is distressing.
If anything, I see assertions like this and juxtaposed with blatant, willful misunderstanding of how C and C compilers work and it does the opposite of inspiring confidence.
Look at CVE-2009-1897; this is the classic example of how C compilers are happy to try to optimize code in the face of UB and lead to worse problems.
> If the above code has any but one of the above 16 outputs
I don't think you understand how insane optimizations in the face of UB can be. Just go look at this issue:
<a href="https://github.com/llvm/llvm-project/issues/174844?utm_source=chatgpt.com" rel="nofollow">https://github.com/llvm/llvm-project/issues/174844?utm_sourc...
strenholme · · focus · HN ↗
That’s not what I have said. 35 issues (mostly minor, but a couple of remote denial of service attacks) have been found with my code in the last 25 years; of those, none have come from the PRNG code I used. Here in the age of AI, I get multiple security reports a year, so the code is being looked at.
With crypto, you can never know for sure the code doesn’t have weaknesses, but one can have confidence in code and algorithms which have been around for years without any weaknesses discovered in them.
My question is: If code being around for years doesn’t build confidence in it being secure, what would it take to build confidence in the code.
What you’re seeing here is two schools of thought: One is the issue with using uninitialized memory, which yes does result in undefined behavior as per the C99 spec—but, back two decades ago when I made that decision, GCC was the only compiler of significance (clang was just released but was not widely used until years later) and its behavior was to put randomish data in undefined allocated memory.
The other is the notion that only Linux Kernel developers can develop a secure PRNG, and obviously I find that attitude very condescending and arrogant.
jcranmer · · focus · HN ↗
One of my experiences with programmers is that we (and I do not exclude myself from this category) are extraordinarily bad at sufficiently imagining the failure paths that our code might take and making code work handle failure cases correctly. It's these erroneous failure paths that are the real issue with code, and age doesn't really indicate how much testing of those failure paths actually exist.
To build confidence in code, what we need is proactive testing of potential failure paths that don't rely on humans to think of them in the first place--that means investment in various exhaustive testing techniques. (And I'd also like to see formal verification be more of a thing, but the tech just isn't there.) A stepping stone in that regard is also heavy use of static and dynamic analyzers to catch things that at known to be Obviously Bad™. The gold standard here really is whitebox concolic execution that's specifically trying to get something akin to 100% path coverage by trying to synthesize inputs to test the unhit paths.
Saying that it's okay to seed an entropy pool with uninitialized memory in 2005 is maybe defensible. There is a shift in compiler design around that time from thinking of it as compiling to a set of instructions and then optimizing them (so that the basic 'structure' of the code is something that's inherent to the program) towards looking at program semantics as abstract things where the only thing you need to preserve are the observable semantics [1]. One of the side effects of that shift is that undefined behavior stops being something that is fairly reliable so you assume you get the 'equivalent' assembly effects for that machine and starts being something that really screws over code.
But it's not 2005; it's 2026, and this change in compilers has been heavily advertised, discussed, complained about for well over a decade. And if you're using the kind of tools that give me confidence in code, those tools would have been bitching about that behavior for decades. If this is a surprise to you in this time and age, then it suggests to me that you've not really been proactive in trying to test your code in the manner I suggest, or worse, you have been proactive and decided to ignore everything telling you your practices are wrong because you know better than the tools and your code isn't obviously wrong.
(I say obviously wrong because your code example does demonstrate, when I tried it in the latest version of clang on godbolt, that it is eliminating the seeding of the entropy pool, in a way that is actually pretty clear if you read the assembly.)
[1] One of the most concrete examples to really observe the difference is the concept of control flow. Compilers nowadays are really happy to turn control flow (if statements) into dataflow (conditional moves or funky bit manipulations) and vice versa, because the only thing that needs to be preserved is the final value. Of course, cryptographers keep complaining that we broke their code by turning their obfuscated dataflow-based if statement into an actual if statement and so it's no longer constant-time, no matter how many times we keep telling them that we do not make any pretense of guaranteeing constant-time execution of code.
strenholme · · focus · HN ↗
The way I somewhat work around this with the newer coLunacyDNS code (from 2020) is by using `-DGCOV` and `gcov` to check the code coverage when running the automated SQA tests for the code. I can’t cover every single failure that could be caused by sanity tests in the C code, but I can cover pretty much all (99.53%) other code.
>>>But it's not 2005; it's 2026, and this change in compilers has been heavily advertised, discussed, complained about for well over a decade.<<<
My code compiles to the C99 standard (-std=c99 and only two syscalls not defined in POSIX) [1]. This in mind, compiler makers have a responsibility to make sure that their compilers, no matter what changes they introduce to them, conform to the C99 spec when compiling with the -std=c99 flag. [2]
This means that when I interact with people working on compilers, I bring out the C99 spec and then use that to determine whether it’s a bug in my code or a bug with the compiler. In this particular case, the C99 spec said it results in undefined behavior when “The value of the object allocated by the malloc function is used”, so that’s a bug with my code.
The thing about standards is this: A given piece of C code, if standards compliant, should, when compiled, act a given way with any compiler conformant with that standard. C developers writing C99 code shouldn’t have to look at any development or document which exists after 1999 to determine whether their code will act a given way. C compiler writers shouldn’t be telling C99 developers “well, you should know about this 2021 change to the C compiler”. They should instead say, “well, if you look at this page of the C99 spec, that behavior is undefined so we have no obligation to implement it the same way GCC does”.
Standards correct C99 code written in 2005 should behave the same way when compiled in 2026 as it did in 2005.
This discussion is like the fights guys get into when playing wargames where they argue whether a given move in the game is legal or not. When this happens, the correct thing to do is to look at the reference manual and see what that says.
>>>it's okay to seed an entropy pool with uninitialized memory in 2005 is maybe defensible<<<
Back when I made that decision, clock_gettime() was not universally implemented (it wasn’t implemented on MacOS), so my options for having some kind of entropy for the XOF should /dev/urandom have issues were very limited. I’ve since updated the code to use clock_gettime(); the Windows port will instead use the non-portable GetSystemTimeAsFileTime() (ghosts of embrace/extend/extinguish). [3]
>>>cryptographers keep complaining that we broke their code by turning their obfuscated dataflow-based if statement into an actual if statement<<<
The cryptography I use, as is typical for post-AES cryptography, makes sure that the cryptographic core doesn’t use any control flow statements, as seen in this compact representation of that code: [4]
[1] The code also assumes that /dev/urandom returns a random stream of bytes, a behavior which POSIX doesn’t specify (newer POSIX finally gives us randomness with getentropy() but that spec is too new for me to assume it’s widely implemented)[2] Until about two years ago, -std=c99 wasn’t needed; C99 code happily compiled as recently as 2022.
[3] Let me make this crystal clear: I use both /dev/urandom and looking at jitter with clock_gettime() in the entropy pool my XOF PRNG uses. Should one of those not have enough entropy, the PRNG is still as secure as the other source of entropy.
[4] I very rigorously made sure that k>>j%32|k<<-j%32 trick works to do a bit rotate while being C99 standards compliant because clang broke an earlier version of this bit rotate at some optimization values; note that j and k are uint32_t variables. Looking at the relevant parts of the standards show this trick only works when the modulo is a power of 2. The production code either uses x>>r|x<<(32-r)%32 or this:
The “if” isn’t a security issue because r has a predictable value which we assume the attacker already knows.jcranmer · · focus · HN ↗
The thing about standards is this: we have the same ability to write large, bug-free specifications as we do to write large, bug-free applications--effectively none. Bugs in the specification can take years or even decades to be discovered, and then the interpretation adjudicated and fixed in a newer version of the standard, with the fossil C99 specification never being updated or given any indication that the original text was buggy. On top of that, compilers don't implement C99, they implement C99-with-compiler-extensions, and those compiler extensions' documentation range from poor to atrocious.
> Standards correct C99 code written in 2005 should behave the same way when compiled in 2026 as it did in 2005.
Standards-correct code means not hitting UB. The number of programs that exhibit UB is approximately 100%, especially in 2005 (which is about when GCC started optimizing based on C's effective type rules). The best way to figure out whether or not your code is standards-correct generally isn't to read the standard [1]. Instead, go run a suite of undefined behavior sanitizers on your code to see if your code is known to violate some of the rules. We unfortunately don't have checkers for all the known UBs (for example, effective type rules).
[1] The standard is hard to read, especially because you have to know where to track down more authoritative sources to be able to resolve interpretation issues. I'll note that you've both incorrectly identified the source of undefined behavior and incorrectly identified where to find the undefined behavior--you're citing Annex J, which is an informative section, meaning it doesn't actually mean anything as far as interpretation goes (and I'm aware of at least one entry in there which is outright incorrect).