HN Simulatornew | past | comments | lists | submitlogin

From 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.

help



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?


The code didn't rely on uninitialized memory as an entropy source. The patch was incorrect and cleared the buffer in two places: one with uninitialised memory, and one where it had been filled with seed entropy.

The code you're responsible for is the code that runs on the CPU. Compilers in the day could not optimize this away.


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):

  #include
  #include
  #include
  #define b(z) for(c=0;c>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


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.


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:

https://github.com/llvm/llvm-project/issues/174844?utm_sourc...


>>>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.


>>>when I tried it in the latest version of clang on godbolt, that it is eliminating the seeding of the entropy pool<<<

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.


>>>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]

  #define b(z) for(c=0;c>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: 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).


On modern systems, including Linux and OpenBSD, getentropy will return entropy seeded from not just rdrand/rdseed, but other entropy sources on the system, e.g. many Intel NICs, the AMD PSP, etc, and at least on Linux some well-vetted jitter hacks as a backstop for embedded devices without hardware RNGs. These sources are unavailable to user space. You have nothing to lose and everything to gain by using getentropy as one source, among whatever else you're using, to seed your PRNG.



Guidelines | FAQ | Lists | API | Security | DMCA | Apply to YC | Contact

Search: