Reflections on Curly Braces – Apple’s SSL Bug and What We Should Learn From It
blog.codecentric.de
blog.codecentric.de
To me, wrapping such a simple test in a macro just obscures what is going on by introducing unnecessary abstraction. Making the decision about whether to use a macro in this case seems to me rather like the decision to use operator overloading in C++. There are occasions when you might want to do it to hide complexity, but you'd better have a good reason.
Trust me: I glance at that code, and it is 100% clear to me in that moment what it is doing and why it is doing it--I have been programming primarily in C/C++ now for almost 20 years--but I simply do not believe you that an experienced C/C++ programmer glances at this code and knows 100% for certain that it is correct. If nothing else, clearly the person who wrote this code screwed it up ;P.
This pattern just lends itself to silly mistakes: you might check the error using the wrong constant (!= 0 when you need == -1; this could even work temporarily!) or think that a function can't return an error when it actually can (the setuid mistake that burned Android, see Rage Against the Cage); by using structured error handling you aren't just "hiding" complexity: you are removing it.
Now, I happily admit (and already did in my original comment) that the macro doesn't solve all of these problems as well as "use a language that provides better abstractions (even C++!)"; however, it does mean that you can't accidentally miss the set of parentheses (assigning the comparison result), double up the equal sign (comparing instead of assigning), add an extra semicolon (breaking the comparison, so as to always "goto fail;"), copy/paste the boilerplate incorrectly (which you might have even done in a misguided attempt to avoid the previous errors ;P) leaving you with a second copy of "goto fail;" (as the developer here probably did; it might also have been a merge failure: thankfully, that also becomes impossible, as the error check now is naturally part of the same line as the code being checked), or otherwise make any silly "it still compiles, it looks almost identical, but now it doesn't work" errors. That's valuable :/.
I agree, but then I didn't actually say that I would know it was correct (to any percentage) by glancing at it. What I tried to say was that the "boilerplate" does not detract from the code exposing its meaning or intention.
Again, I agree that the the wrong error constant could be used. But thats also true of a macro is used - especially if the constant is embedded in the macro which is elsewhere.
I didn't want to get into your point about whether this would be better written in another language, because we are where we are. libssl has bee around for a while and has accumulated plenty of dependencies. I don't think a re-write is going to happen any time soon.
TRUE * 1e6
It's horrible code that seems to indicate a lack of production coding standards.Errors, warnings and assertions need to be handled semantically sensibly, obviously and consistently.
Most functions, errors should be handled individually and returned as they happen and the only condition at the very bottom should be success.
My personal fav is anomaly(...) which checks for a nonfatal but suspicious condition. It always throws a warning on STDERR if it fails, even when compiled with -DNDEBUG.
Example: https://gist.github.com/steakknife/9271284
If anyone wants to see decent examples of real C/C++ done right:
nginx [0], doom3 [1], postgres [2], varnish [3] and openssh [4]
References:
[0] http://hg.nginx.org/nginx/file/0251f2f1dc93
[1] https://github.com/TTimo/doom3.gpl
[2] https://github.com/postgres/postgres
if (ctx->main_conf == NULL) {
return NGX_CONF_ERROR;
} /* No MIN_SIZEOF here - we absolutely *must not* truncate the
* username (XXX - so check for trunc!) */
strlcpy(li->username, pw->pw_name, sizeof(li->username));Basically the remark was that the comment went from discussing error handling, to the (much) broader scope of 'C and C++ done "right"', and as an example 5 complete, large open source projects were dumped for us to find the examples ourselves.
if (strlcpy(li->username, pw->pw_name, sizeof(li->username)) >=
sizeof(li->username)) {
error("%s: username too long (%lu > max %lu)", __func__,
(unsigned long)strlen(pw->pw_name),
(unsigned long)sizeof(li->username) - 1);
return NULL;
} rv = strlcpy(dst, src, sizeof(dst));
check(rv == strlen(dst), "Error, src string truncated");
Or perhaps more clear: strlcpy(dst, src, sizeof(dst));
check(strlen(src) == strlen(dst), "Error, src string truncated");However if I were writing that code I would've probably used an OR-chain, like this:
if ((err = SSLFreeBuffer(&hashCtx)) ||
(err = ReadyHash(&SSLHashSHA1, &hashCtx)) ||
(err = SSLHashSHA1.update(&hashCtx, &clientRandom)) ||
(err = SSLHashSHA1.update(&hashCtx, &serverRandom)) ||
(err = SSLHashSHA1.update(&hashCtx, &signedParams)) ||
(err = SSLHashSHA1.final(&hashCtx, &hashOut)))
goto fail;
There's also another oddness I noticed: if(err) {
sslErrorLog("SSLDecodeSignedServerKeyExchange: sslRawVerify "
"returned %d\n", (int)err);
goto fail;
}
fail:The 2nd version fails as an example because it hides the act of creation then surprises you with "here, look at this, anything wrong?" with the author knowing full well he's making it wrong.
My takeaway from this whole discussion is: assume failure, prove success. The offending function failed by presuming success; it did not positively confirm every criteria for success, with even the "fail:" section executed by success.
* For critical code, enable as many compiler warnings as you can while remaining sane. This would definitely include unreachable code warnings.
* Consider using static analysis
* Initialise your error variable in the "error" state not in the "everything is ok" state
* Stop copying and pasting the same code over and over. If you do a complex series of operations several times, encapsulate them in a function/method.
I find it hard to believe that this code had a proper peer review before being committed. A 2nd and 3rd set of eyes experienced at reading C should have spotted this. Peer review is especially important when dealing with core security components like this where simple mistakes can have huge consequences.
If proper indentation is so critical to a human parsing of the code that every style guide in the world demands some type of indentation (note the lack of style guides demanding a complete lack of indentation,) then why not use that very same convention to convey that very same information to the compiler?
The C-family of languages is on the unfortunate side of this trade-off: there's one method to convey statement blocks to the compiler (brackets) and another to convey it to the reader (the indentation convention.)
TL;DR: Multiple channels with redundant information run the risk of disagreeing with each other. I don't know if anyone's come up with a catchy phrase for this, or coined some sort of "law", but it's very real. As soon as you have redundancy of information, you automatically inherit the problem of synchronizing the information between channels.
I find it hard to believe that this code had a proper peer review before being committed. A 2nd and 3rd set of eyes experienced at reading C should have spotted this.
They could've spotted this, and maybe due to some other factors (e.g. some people just don't want to point out other's mistakes, like hierarchical cultures) let it pass; or the other reviewers were at the same competency as the one who wrote the code.
I have seen teams following thorough review and testing processes in far less critical software!
The conclusion I found wanting. "Should," is a word like, "obvious," that is best avoided. It is a sign of weak, wishful thinking and stinks of condescension. Could, would, or should have... it happened and was dealt with, it seems, in an appropriate manner. Armed with good suggestions on how to avoid it in the future perhaps we won't see the same mistake made again (hah).
goto fail;
if ((err = SSLHashMD5.update(&hashCtx, &clientRandom)) != 0)
goto fail;
Now, what would have happened if there had been braces?To get the two `goto fail;`s he would have had to duplicate something like this, which would have immediately caused a syntax error!
goto fail;
}
if ((err = SSLHashMD5.update(&hashCtx, &clientRandom)) != 0) {
goto fail;
}I just don't think it's fair to say that braces wouldn't have helped in this situation, because they would have. That doesn't mean that a pattern like this was a good solution in the first place of course.
On the subject of how the extra line appeared in the first place, my take on possible causes are, in order of likelihood:
1. Line added during developer testing to force a fail and not removed.
2. Copy/paste error during construction.
3. Artefact from a bad code merge.
4. Deliberately introduced by NSA mole.
Presumably, one someone goes through the version control history, the who and when will be known - though maybe not publicly. I'm not sure we'll ever know why, though.
(in case anyone is curious, the "goto fail" lines in that part of the code are identical: all spaces; and the file in general does have an unfortunate mishmash of spaces and tabs)
the only thing that changed in the relevant part was that "goto fail;" was added.
@@ -627,6 +628,7 @@
goto fail;
if ((err = SSLHashSHA1.update(&hashCtx, &signedParams)) != 0)
goto fail;
+ goto fail;
if ((err = SSLHashSHA1.final(&hashCtx, &hashOut)) != 0)
goto fail;
this makes a copy & paste error highly unlikely.IMHO, even better and simpler would be a coding convention to not skip curly braces. The end.
(as per last time this was raised: https://news.ycombinator.com/item?id=7283767 )
Otherwise, the OP is right on the money; there are multiple levels that would have caught this bug - a layout convention, a linter, a code review, pairing, a unit test, an integration test. Or just a refactor of the big old method for clarity.
That and the dead code after it.
The way they use it is widely understood and accepted in C error handling. It's also not the only way you can screw up, either, which is a good reason why you should add a static analyser to your build process.
Don't get be wrong, it's the best language for the space that it occupies. There is no better language to implement such things.
But it still isn't good. It's just that it's failed to be replaced by anything better.