I fail to see why one should either rely on a single random source nor roll their own.
I fail to see why one should either rely on a single random source nor roll their own.
The only legitimate reason to roll your own is when you're developing for an embedded system or a bootloader or something like that where there is no kernel API available.
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.
so instead you suggest trusting your own untested unlooked at implementation more?
https://maradns.blogspot.com/2010/07/radiogatun32-passes-all...
>untested
The automated tests includes tests that make sure the XOF is correctly implemented. [1]
>unlooked at
People have been looking at my code for security holes for well over 20 years, and I have been getting multiple AI assisted security reports over the last year, things like “there’s a buffer overflow in this code which is nay to impossible to exploit, using code which hasn’t even been able to compile since 2022”.
[1] https://github.com/samboy/MaraDNS/tree/master/deadwood-githu... and https://github.com/samboy/MaraDNS/tree/master/deadwood-githu...
You’re correct about black and white thinking. Then you invoke multiple straw men in this thread to defend that you’ll roll your own.
Disclaimer: I’ve been hired for multiple DoD projects to break hardware and software security systems, and I nearly always succeed, because so many people (and companies) roll their own.
One reason why I don’t change the RNGs used in my code is because I know how dangerous playing with RNG code is. For example, one implemention I wrote of the XOF—not one I used in production code, mind you—generated incorrect vectors, but only in clang and only at some levels of optimization. Needless to say, I now have a test to make sure my XOF code generates correct vectors with both GCC and clang at multiple different optimization levels.
People have brought up CVE-2008-0166 in this thread, but the Coldcard incident from this year (where people literally lost millions of dollars) also comes to mind, so I’m aware how dangerous playing with RNG code is.
That’s why the code is basically the same code I had 18 years ago, and why I (as well as multiple people running AI-assisted security audits) have extensively tested that code.
The proof is in the pudding: No security issues have ever been found with the XOF PRNG, and it’s been nearly two decades.
(I also think “straw men” is being used incorrectly here; most likely the parent poster thinks I was implying that Linux’s /dev/urandom is insecure but the actual argument is that my code runs on a lot more than just Linux, and some of those systems could have an insecure /dev/urandom)
[1] As per https://blog.cr.yp.to/20140205-entropy.html as long as we’re not using a malicious source of entropy, but said malicious source will need to perform 2^n operations of the XOF to generate n bits of controlled output, and only in the case if said malicious entropy source can somehow know the output of the other entropy sources, especially since the XOF is seeded once then run indefinitely in my code.
Isn’t that also true of the cryptographic sponge function that is used to implement /dev/{,u}random?
Secure is not a binary state; not understanding basic information theory and how entropy evolves is fatal.
This right here shows 100% why no one should trust you or your code. You have a seriously fundamental misunderstanding of entropy or what the Bernstein blog post (and it is a blog post, even if it’s Bernstein) states.
His post states, correctly, that a hash is as secure as its weakest entropy source. Adding more bad ones does not strengthen it. He doesn’t say you can ignore the entropy per source, and simply hope one “is secure,” whatever the heck that even means.
Entropy of a source is a number, often in units of bits/sec (or nats or Harley’s or some rate for differential entropy). It’s most definitely doesn’t even make sense to say “one is secure”. That’s a nonsensical phrase.
If you do not correctly know the entropy bit rate of all your inputs, and very importantly cross correlations, you cannot know if you have enough entropy accumulated for an operation, which is then used up; you cannot make downstream claims about security. That you’re so incredibly lax and naive and state the opposite of reality shows the lack of crypto skill. This type of misunderstanding is why there’s still groups hiring people like me to break systems: tons are implemented very poorly, leave holes from poor entropy, timing attacks, power attacks, glitch attacks, etc. depending on the system.
The very least anyone designing such things should know about information theory is a solid understanding of the book by Cover, then stack on top significant knowledge about the physical systems used under the software and have detailed models for them.
This is why people should be skeptical about this stack.
I believe it is you with the misunderstanding. A hash is as secure as its _strongest_ entropy source provided that none of the inputs can snoop on the others. The key point being made in that blog post is that if a malicious source can snoop the other inputs and has knowledge of the implementation then it could potentially (partially) control the output. That's quite a high bar, and even then the attacker is limited to a brute force search for the desired partial output.
> you cannot know if you have enough entropy accumulated for an operation
This is superstitious nonsense. Entropy is merely an estimate of the effective size of the input space, ie how hard an attacker would have to work to exhaustively search it.
Exactly! Let me give you a real-world case where I estimated entropy:
I ran a bunch of clock_gettime() calls and then looked at the delta time between calls. Just eyeballing the deltas, I decided that just running clock_gettime() by itself wasn’t giving me enough entropy (the time differences would jitter between two delta values in a most predictable manner). So I updated the code to initialize then destroy an instance of the CSPRNG I use in the code between clock_gettime() calls. Looking at the deltas after doing that, I saw the deltas in an unpredictable fashion, alternated between over a dozen different values. That in mind, I decided a single call to clock_gettime() gives one 1 bit or more of entropy.
I did the calculations using Cygwin, Ubuntu 26, and an Alpine 24 Docker container, using an x86_64 chip. In all three cases, looking at the deltas showed at least one bit of entropy per call. So I have the code do 112 calls and add the nanosecond timestamps to my entropy pool.
Now, it’s possible that on a Raspberry Pi the calls will be much more predictable, so I can only say things look nice and random on an x86_64 system. Since my code is open source, I can’t control what systems people will compile my code on (I’m pretty sure someone in China has probably already made a RISC-V compile of my code, but they aren’t telling me about it).
That leads us to: >>>A hash is as secure as its _strongest_ entropy source provided that none of the inputs can snoop on the others<<<
My code also uses /dev/urandom to seed some of the entropy pool. So, if there’s a system out there where clock_gettime() is really coarse and not a good source of entropy, we’re still OK if /dev/urandom is good [1]
In terms of entropy, I would say that the best attacks out there are maybe 80 bits. We know 40 bits is hideously insecure these days (e.g. the ColdCard incident), that large companies have solved problems with about 64 bits of entropy, so I would guesstimate that 100 bits of entropy is beyond even big money governments right now. So, I don’t need to know the exact amount of entropy my entropy pool has; if it has 100 bits of entropy or more, it’s safe for the time being (I would go for 256 bits of entropy for things where we don’t want people to be able to decrypt things later on, but I only use my CSPRNG for numbers which only have to be secure for a couple of minutes at most). [2]
[1] This was not always a guarantee on Raspberry Pi. There were issues before the 5.6 Linux kernel where the entropy pool was drying up and people were using things like haveged to try and keep the entropy pool replenished.
[2] Because of limitations in the DNS protocol, an attacker can brute force things with about 28 bits of effort. Which is a big problem but there are spoof mitigations I use.
Read my post above. It’s trivial to ask Google if these are the same, and they’re not.
A hash is as secure as the entropy used to make it. That entropy is the sum of the entropy sources, if they do not share information. If they do, and you can accurately model the partials, then you can compute how much entropy is in the result. Using the entropy from a pool reduces it, and over time the input sources are analyzed to know if there’s enough in the pool to perform a requested operation.
Has no one in this tread actually learned basic information theory at the level of Cover?
> This is superstitious nonsense….
You are confusing entropy with complexity. Simply google “ what is the difference between entropy and complexity in cryptography” and it clearly shows you’re confused.
Entropy is the unpredictability in a source of information. How hard it is to exhaust a space by attack is complexity. These are fundamentally different concepts in both information theory and cryptography.
Matt Mackall: "It's worth noting that the maintainer of record (me) for the Linux RNG quit the project about two years ago precisely because Linus decided to include a patch from Intel to allow their unauditable RdRand to bypass the entropy pool over my strenuous objections. "
https://cryptome.wikileaks.org/2013/07/intel-bed-nsa.htm?utm...
Instead, people used haveged to workaround the issue. Not a conspiracy by some dude, but rather just more budget hardware limitations.
Don't worry about it, there are lots of real dubious things people do already. =3
I don't like piling on people that engage in good faith. Best regards =3
Perhaps it is time to get outside for a walk to lower stress levels. =3
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.
“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.
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?
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):
#include<stdio.h>
#include<stdint.h>
#include<stdlib.h>
#define b(z) for(c=0;c<z;c++)
uint32_t c,e[42],f[42],g=19,h
=13,n[45],i,j,k;void m(){j=0;
b(12)f[c+c%3*h]^=e[c+1];b(g){
i=c*7%g;k=e[i++];k^=e[i%g]|~e
[(i+1)%g];j=j+c;n[c]=n[c+g]=k
>>j%32|k<<-j%32;}for(i=39;i--
;f[i+1]=f[i])e[i]=n[i]^n[i+1]
^n[i+4];b(3)e[c+h]^=f[c*h]=f[
c*h+h];*e^=1;}int main(int c,
char**v){char*q=malloc(2);if(
q==0)return 0;q[0]&=31;q[0]|=
1;q[1]=0;for(;;m()){b(3){for(
j=0;j<4;){f[c*h]^=k=(*q?255&
*q:1)<<8*j++;e[c+16]^=k;if(!
*q++){b(18)m();b(8){j=c;b(1)
printf("%02x",(e[1+j%2]>>8*c)
&255);c=j;if(c%2)m();}puts(
"");return 0;}}}}}
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:
0a5d51f3745c7266
f84b051f67115f1a
f87105c4ecfefe67
92074ac8e1e7a42e
1441ac245f288e18
87023372e57ae001
047a3ddd14209546
340b2ff47c61172e
bfb9289ed096f977
dfd56a7a8d7d723e
2151460954a80242
6822335c6e0160dc
3783ce3cae3d0774
4e0156df46c00bac
69795d939d211e7a
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
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:
https://github.com/llvm/llvm-project/issues/174844?utm_sourc...
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.
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.
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]
#define b(z) for(c=0;c<z;c++)
uint32_t c,e[42],f[42],g=19,h
=13,n[45],i,j,k;void m(){j=0;
b(12)f[c+c%3*h]^=e[c+1];b(g){
i=c*7%g;k=e[i++];k^=e[i%g]|~e
[(i+1)%g];j=j+c;n[c]=n[c+g]=k
>>j%32|k<<-j%32;}for(i=39;i--
;f[i+1]=f[i])e[i]=n[i]^n[i+1]
^n[i+4];b(3)e[c+h]^=f[c*h]=f[
c*h+h];*e^=1;}
[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: 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).
This is an unverified claim. My own testing does not show TCC, GCC, nor clang “optimizing out” the code using uninitialized memory as an entropy pool.
The full test is here: https://github.com/samboy/MaraDNS/tree/master/deadwood-githu...
In summary: On Ubuntu, and in clang at higher levels of optimization, the uninitialized memory is made 0s, but the cryptographic pseudo random number generator still runs.
Tests have been done against TCC, GCC, clang, as well as GCC and clang in Cygwin. As an aside, uninitialized memory does seem to give a little bit of entropy with GCC in cygwin, which indicates it probably did back in 2007 when I originally wrote that code (it doesn’t these days with clang with optimization, nor in Ubuntu, which is why I use clock_gettime() as a second possible source of entropy instead of uninitialized memory)
I take claims of security holes in my software seriously, and this isn’t the first time someone made a claim of a real-world security problem, I tested the claim, and was unable to reproduce the alleged security hole in my code.
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.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.
The code you're responsible for is the code that runs on the CPU. Compilers in the day could not optimize this away.
Is /dev/random or /dev/urandom part of the POSIX specification?
I actually at one time had a Windows binary which would use Windows proprietary calls to make a “urandom” file (secret.txt was its name) so people could have good entropy on systems using the exact same interface as fopen("/dev/urandom","rb") (i.e fopen("secret.txt","rb")) without needing an actual /dev/urandom.