logoalt Hacker News

jcranmeryesterday at 4:42 AM1 replyview on HN

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


Replies

strenholmeyesterday at 11:00 AM

>>>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<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.
show 1 reply