A brief history of one line fixes
tedunangst.com
tedunangst.com
I get that this is trying to make fun of the response to Apple's "goto fail;", but this logic ("These similar errors predate 2013 → Apple's similar error was not an NSA backdoor") seems rather faulty. There are a number of flaws with this line of reasoning. To name a few:
* The NSA could have been involved with backdoors before 2013 (unlikely in Debian, but mentioning 2013 is a bit of a red herring)
* Apple could have been encouraged to insert a backdoor and did so in a way that gave them plausible deniability (either because the NSA specified that, or because they wanted to cover their behinds)
Whether or not this incident was the result of the NSA's prompting is something we'll probably never know[0], but this article doesn't do much to argue one way or the other.
[0] The only way we could know is if someone literally came out and admitted it (or someone like Snowden were to leak it). It's possible to prove the existence of something (ie, a backdoor attempt), but impossible to prove the absence of something.
in the apple case though the two revision numbers we have are 55179.13 and 55471. i don't know how apple numbers, so i'm not entirely sure what the .13 is, but theoretically we could have 292 patches between the two we see publicly.
it's impossible for us to know if there would have been a code change that could have caused a merge conflict at that place.
I amazed your at bottom of the comments. But then again I was silly enough to upvote the parent before I read the full text.
"A sound banker, alas, is not one who foresees danger and avoids it, but one who, when he is ruined, is ruined in a conventional and orthodox way with his fellows, so that no-one can really blame him" - JM Keynes
If p => q, then ¬q => ¬p. So if p is "The goto fail was setup by the NSA", then surely we can come up with a reasonable q which could be (dis)provable?
if "setup by the NSA" then "goto fail"
I can think of a q that disproves p then! not goto fail.
problem is goto fail.
since q is true, p is either false or true, with no distinction either way. So... impossible to prove.A possible q could be "someone from nsa communicated with someone from apple"
Another possible q could be "goto fail was introduced on purpose"
These specific qs might be difficult/impossible to disprove, but that doesn't mean that there is no q we can disprove.
Of course, whether the NSA was involved or not isn't the point of this article. The point is that high severity one-liner bugs have been made before.
I feel partly to blame. Until my comment on HackerNews revealing where the source code was, no one seemed willing to post more details about the bug (based on Twitter posts). But I think it will end well, as more people will pay attention to the code in future. Sadly, Apple has yet to share revised source code for the 10.9.2-shipping security library that I'm aware of.
ASSERT(apples = 1);
VERIFY(oranges = 2);
(Note the '=' means assignment, not comparison.)Both are assertion-testers resulting in a core dump if the test fails, but ASSERT is only defined for DEBUG builds.
Since the assignments are to nonzero (true) values, buggy values of 'apples' are silently corrected -- but only during test runs. Buggy values of 'oranges' are always silently corrected.
Hilarity ensued. Afterwards, 'gcc -Wparentheses' became mandatory.
ASSERTS(1 == apples);
If you forget one `=`, it doesn't compile.+EDIT: In those occasions when you actually compare to a integer, the expression is so simple that you immediately understand the whole line. In case of an actual typo the compiler will warn you, and the inversion is not needed. Nowadays, there is simply no excuse for a ignored or missed( bad compiler ) warning.
Assignment in an If statement must produce a warning!
If you're doing
if (var1 == var2)
then yoda expressions won't save you, however. if (42 == some_long_expression_here)
{
...
}
It is immediately clear that the rest of the line after the two equals signs must be an expression because the curly brace in the beginning of the next line. So folks don't have to bother that much about counting parens.This is mostly due to the volume and prominence of C. But a language with the same fundamental semantics of C but lacking the cleverbait syntax and weakened types would have prevented over half of these mistakes.
But, that they're not integrated into the language itself means those tools can only ever make suggestions in a separate context, and so cannot function cohesively to permanently rule out classes of bugs in a way that can simply be taken for granted.
In fact, the Debian OpenSSL bug was actually caused [1] by a message from one of these tools being uncritically acted upon.
[1] the job of failure analysis is to find all contributing factors, not just pin everything on one.
Their presence isn't a sign of flaws of the language. They are signs of flaws in programmers. They give the designers the ability to extend the power of the compiler's warnings to help catch common mistakes and tune the automated feedback to fit the needs of the project.
The compiler could have prevented 'X' by not permitting "function == int" comparisons. That's 1/5.
The only thing you might be referring to with "cleverbait syntax" is the ++ fix in tarsnap, but note that the bug was not including ++. Being generous, we could say that if ++ wasn't allowed, it might have been more obvious that the code was broken, getting us 2/5. And being extra generous, maybe if `!i` was disallowed, they would have gone straight for `i <= 0` instead of `i != 0`. So 3/5, but yeah, dubious.
The only other one that's remotely like a C misfeature is that the compiler apparently didn't warn about 'unused parameter' in the Android bug.
I'm not claiming the unused parameter bug, because there can be legitimate unused function parameters, and we can easily envision a very similar bug that still used all parameters.
But add in the current Apple bug that precipitated this post, and you have 4/6.
The general defense by C programmers is that everything works as long as you do the right thing. I have been there. However as these bugs illustrate, vigilance does not scale.
And note that 2/3 of the bugs you're claiming, apply to all weakly typed languages, of which there are many. Python would not have prevented them, for example. Showcasing them as problems with C seems somehow fishy. (I don't mean to imply dubious motivations on your part, I just get the feeling of "there's something not quite right with this argument".)
You're right about my analysis applying to all weakly typed languages, or at least languages with weakly-typed core functions. Python pretty much only brings memory safety to the table (which as I said in a sibling comment has an actual overhead so I didn't touch upon it here). But it does show that the Tarsnap and the Apple bug are caused by syntax, which is a pretty heavy indictment.
In this case, we're referring to none of these being errors as weak typing:
if 0: pass
not 42
'a' == 90 is also a special case litteral for the null pointer, and function names are valid in pointer contexts, so I'm not even sure that "f == 0" is badly typed.
#define NULL ((void *)0)
in which case, using NULL, you're comparing a function to a pointer.
The X one is probably the only very C bug on the list and compiler warnings and/or a good static code checker would have found it.
It's not so much about a language's ability to do unsafe memory operations, but the necessity of doing them to write useful code in the language.
The other examples are make the same point. There are ways to help catch specific bugs. But there is no magic "fix it" button. Consider static analysis for example: you actually have to run it and inspect the output. You have to interpret it right. You have to fix it right. And so on. Human error can ruin each step, as has happened.
>>> def f(a,b=[]):
... b.append(a)
... return b
...
>>> print f(1)
[1]
>>> print f(1)
[1, 1]
>>> print f(1)
[1, 1, 1]
>>> import random
>>> def key():
... return random.randint(1,10000)
...
>>> def f2(a, b=key):
... return b()
...
>>> def f3(a, b=key()):
.... return b
>>> f2(1)
3974
>>> f2(1)
8684
>>> f3(1)
2867
>>> f3(1)
2867Unfortunately a lot of people still use it
(and it's actually correct, IF you don't modify it)
It's a risk not worth taking (to me, at least)
Depending on a problem it can be worthwhile to use other techniques of creating stateful callables, but that doesn't mean you should never create a function with mutable default argument. It's there in the language - learn about it, understand how it works and why it works like this, then understand where to use it and use it where it makes sense. That's a pretty generic advice, valid for almost any language feature.
What about this?
def foo():
if not hasattr(foo, 'static_list'):
foo.static_list = []
At least it's a bit easier to see that something funky is going on.If your language has a lint tool and your code is remotely important, please run the lint tool as part of your build.
> But since Python syntax is arguably cleaner
> and more readable, it's easier to catch.
I've seen nasty bugs in Python code having to do with the end of blocks losing their indentation due to someone's merge/editor mistake.Having braces or other explicit start/end markers for blocks would have prevented those issues. So Python's syntax can in some cases encourage these sorts of mistakes.
>>> from __future__ import braces
And I'm actually being serious. I program in both quite regularly. The Python ecosystem is amazing, but the web defaults to Javascript.
External symbols are resolved at link time; as far as the compiler is concerned, setuid might be a symbol declared as __weak.
I don't particularly like the snide remarks towards the maintainers of these libraries.
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main(void)
{
if (getuid() != 0 && geteuid == 0)
{
printf("only root\n");
return 1;
}
printf("okay\n");
return 0;
}
With just a plain GCC (no options), the code compiled and exhibited the bug (it always printed "okay"). That's to be expected. What was not expected what when I compiled the code with "gcc -ansi -Wall -Wextra -pedantic". NOT ONE ERROR OR WARNING!How is that happening?
Well, C allows function pointers. Also, in C, a value of 0 used in a pointer context is treated as a NULL pointer. So that one line is seen as:
if (getuid() != 0 && geteuid == NULL)
Now, let's play compiler. geteuid() is a function and it's defined. So the compiler can generate: if (getuid() != 0 && false)
The result of this expression can never be true. But because the call to getuid() could cause side-effects (the prototype for getuid() does not include the GCC extension marking it as a "pure" function) the compiler has to emit a call to the function, but otherwise, it ignores the result since it doesn't matter what the result is. The resultant code is: int main(void)
{
getuid();
printf("okay\n");
return 0;
}
And thus, it's possible, even with the commonly used "-Wall -Wextra" options to GCC. To detect this, you will also need to add "-Wunreachable-code" (why that isn't included with '-Wall' is a good question). $ rpm -q gcc
gcc-4.8.2-7.fc20.x86_64
$ gcc -ansi -Wall -Wextra -pedantic test.c
test.c: In function ‘main’:
test.c:7:36: warning: the comparison will always evaluate as ‘false’ for the address of ‘geteuid’ will never be NULL [-Waddress]
if (getuid() != 0 && geteuid == 0)
^
If fact, just -Wall is enough to enable that warning. Maybe the coverage of -Waddress has changed since your version?gcc 4.2 -Wall:
warning: the address of ‘geteuid’ will never be NULL
gcc 4.8 -Wall: warning: the comparison will always evaluate as 'false' for the address of 'geteuid' will never be NULL [-Waddress]
if (getuid() != 0 && geteuid == 0)
Also, as came up in the goto fail discussion, -Wunreachable-code was removed from gcc and doesn't warn on anything.By the way, I'm interested to know which version you used.
Sure it could be an honest mistake, but that's also true for Apple's bug (and the rest).
(To be more clear, each comment mimics the comments made about the "goto fail" bug, and the crucial point is not pure mockery or a defence of Apple, but rather the final paragraph.)
NOTE: I have no opinion on their involvement in the goto fail;
I'm pretty sure he's being sarcastic.
You were being voted down earlier, but not any more. So there are others who agree with you.
"Imagine the NSA snuck a one line 'fix' into your software overnight. Do your tests quickly and accurately detect the problem and point to the code that is broken? If not, your unit tests are broken."
My number one take away from this is that we should all be using them more.
For another, sometimes warnings are there because the code generated by compiler macros isn't available to the compiler for the purpose of e.g. checking for unused variables/parameters or type range checking. For example, there are some Linux kernel macros that will ignore certain parameters on a lot of architectures; this will give unused-variable warnings on those architectures if the variable/param isn't used elsewhere. In other places, data types change between signed and unsigned based on architecture, meaning that an in-general-meaningful test for >= zero will give you a warning on platforms where the variable is unsigned.
Long story short, the preprocessor, while super-useful, is not good for static checking.
So your code might not compile anymore on newer compiler versions. This is not a problem as such but for example in combination with updates to your continuous integration environment, this might cause build fails.
Wonder what the solution would be.
For GCC, see http://gcc.gnu.org/onlinedocs/gcc/Diagnostic-Pragmas.html
From http://www.daemonology.net/blog/2011-01-18-tarsnap-critical-... regarding the Tarsnap bug.
Solution: write more unit tests.
I don't write a lot of C code, but when I did, and even more relevant - when I compile packages, the compiler tends to spit out chapters worth of warnings that I just ignore. How accepted is compiling C code in practice with Werror? Could it be the compiler did warn about goto fail, and the OP's error, but it was 1 warning out of a million? Really interested in an answer from those who work in C shops.
I started work on a new from the ground up simulator in c++. The 12+ coders got used to the warning = errors (Werror). I think it made us better coders. We were able to do that because the project was from the ground up new. Most projects in the Radar field use a lot of previously tested legacy code that does issue warnings.
Putting in the effort to get down to "emit no warnings" (and then enforcing that you stay there with -Werror) is worth doing exactly because you then do get new problems drawn forcibly to your attention, IMHO.
2. Use an IDE and/or compiler that actually warns you about common human errors like unused variables, unreachable statements, and dumb mistakes like a case-block with silent fallthrough to the next case-block.
3. Acknowledge responsibility when writing security related code. Know that there's no room for mistakes.
Mostly human, and avoidable but always present because people don't take the time to do it correctly. Quality, taking the time to make the things right, slowly, correctly, all that matters more than genius. Quality code at all level is the key to secure development. Every little things count.
I know a few openBSD developers, and their song about doing the right things, taking our times to do it correctly - simpler, with quality because it is the shortest path to correctness thus speed (what is the use of an incorrect program: none). This song is appealing. The fact they are strangely psychopathic paranoid nerds is not ;)
I think computer industry would be far better if we would slow down and focus on doing less software, but better built.
Less functionalities maybe, but the one that maybe more powerful. The stable foundation for sustainable progress.
And, if you like them -even though they are repulsing antipathic nerds- you can support them :)
But thanks to those who support what we do!
[Now I wait for a deluge or replies about how it's "idiomatic" C. All I have to say to that is, s/ma// ]
--- libc-a/memset.c
+++ libc-b/memset.c
@@ -1,6 +1,6 @@
void *memset(void *_p, unsigned v, unsigned count)
{
unsigned char *p = _p;
- while(count-- > 0) *p++ = 0;
+ while(count-- > 0) *p++ = v;
return _p;
}
[Edited later to use the code mode]Questions like this and others (eg., does the pointer deref operation applies to incremented or non-incremented value of p?) that you have to answer before you can make sense of the code. The cognitive overload on the reader's brain is at least part of the reason that a bug like that went past code-review in memset function on a major platform. MEMSET !! Was the needless cleverness and terseness of the code worth it?
P.S. The answer to your question is No. The variable count as used in the body of this function is local to this function (it's passed by value... so it's a copy of what the caller passed).
C++ added pass by reference (&), which can be a handy bit of syntactic sugar.
What you're saying is correct in the practical sense. But ggreer's answer is the real technically detailed explanation (everything is passed by value, including pointers to memory. But then you can manipulate that memory you have the pointer to. And that makes it effectively pass-by-reference). Also, C++ adds a language defined pass-by-reference operation... so there's that.
You have an interesting take on C.
And I don't see what's 'clever' about decrementing a count instead of incrementing a count.
It would be just as easy to assign 0 in a larger function, and I see no way of reducing the cognitive load. This line isn't doing anything complex or weird.
Can't tell if this question is referring to something I said? If so, I'd like to know what I said that implied such a thing.
The only way to avoid altering the count parameter is to make a new variable for counting.
The single line while loop there is just bristling with pitfalls. I'd reject such code in code review and ask it to be rewritten in more readable C. As an added bonus, this exercise would probably also have caught the original bug that started this discussion.
The pass-by-value question came from someone who hadn't written C code in years and I'm pretty sure a practicing C coder (in production code) would not have that question in the first place. Or if it does come from a fresh programmer just getting started, it'll only take one code review to get that straight (and probably earn that programme's work some added scrutiny for some time).
Finally, the same people who write that kind of code also write things like f(x++, (x+1)) without realizing that this is undefined in C.
Also, as an added bonus, postincrement/decrement sometimes creates extra unwanted copies in C++ so you need to use the pre- versions.
Compilers are good enough to handle tight loops like this even on very low optimization settings. Better to write that stuff out.
I have found that as I program more, I kind of agree with Crockford that ++, and -- are evil and should always be replaced by pp += 1; What that does is force you to avoid actions in conditional expressions and pull the mutators inside the block. It also forces you to be explicit about when you expect the increments to happen.
Those are fundamental things that should be second-nature to you if you claim to really know C, just as a mathematician would know that exponentiation has higher precedence than multiplication which has higher precedence than addition.
> Did p get incremented or did p get incremented?
There's an easy and concise mnemonic for the precedence of ++/-- and dereference - just remember that this is a strcpy, as shown in K&R:
while(*dst++ = *src++)
;
So here it is the pointer that is incremented, and not what it points to.> Finally, the same people who write that kind of code also write things like f(x++, (x+1)) without realizing that this is undefined in C.
Those are the people who think they know C, but actually don't; it's an issue of not knowing what you don't know, and in such situations you should either seek to find the answer, or be conservative.
That's a bogus argument. There's an easy way to write code that's readable and maintainable by mixed teams and then there's this macho way of showing off to no particular advantage.
- just remember that this is a strcpy, as shown in K&R: while(dst++ = src++) ;
Or -- just don't. Just write code that doesn't rely on every reader to mentally clear through a dense thicket of precedence and side-effects to understand your strcpy and get your job done in a pragmatic fashion with 4 lines of easily understood C code instead of 1 line that makes your heart glow with momentary pride.
Given the kinds of projects I've been part of are already swarming with (externally imposed) system complexity to begin with, an attitude of "I write dense code since I'm a professional" won't get anyone very far in such teams.
And yet we started off discussing a serious bug in libc discovered in exactly that kind of code. That's the libc in Android!! My argument is that the extra cognitive overhead of digesting needless complexity leads to the original authors and reviewers of the code to miss other obvious bugs.
I'm astonished at the vitriol
I'm sorry if my straightforward and respectful response to your dismissive and patronizing remarks sounds vitriolic to you.
This bug is simply a failure to test. Where was the trivial automated unit test for this library routine? Never written I imagine.
Reminds me of another buggy library I had to work with - the Linux c runtime. I was porting to a risc processor oh ten years ago. Something was wrong with the memcpy routine. So I wrote a unit test. A very thorough unit test. Found 12(!) bugs before I was through.
Test was: copy from aligned source + (0..128) to aligned destination + (0..128) length (0..128). Simple triple loop. Anybody could have written it. Nobody ever had.
This is not a C problem, or an Android problem. Its an industry problem. Intel shipped a Pentium processor once upon a time that couldn't divide by 10.
Would we criticize assembler because its full of confusing registers and stuff? Sure go ahead; but it sounds silly.
*s = 0;
s++;One could reasonable speculate if a code-reviewer had sent the original code back to be written in a readable fashion, the actual bug in the code would have been caught too.
I can see your point about *p++ being a speed bump, but overall this version seems less complex to me.
NEVER WRITE THIS CODE! EVER!
This is the code that gives us the classic buffer overflow vulnerability all because checking for a length isn't elegant.
Also, how does this code work if what one of those pointers points to are declared volatile? What if both pointers point to volatile things? What happens if one of those pointers crosses through 0 because it rolled over from max?
Thanks. I doubt you could have provided a better example of the precise problem.
The few characters it would take to check length didn't effect this API, it was just part of the decision to use null terminated strings instead of storing lengths.
>Also, how does this code work if what one of those pointers points to are declared volatile? What if both pointers point to volatile things?
I can't imagine any way in which volatile would cause problems. What are you pondering? strcpy doesn't take volatile pointers anyway.
> What happens if one of those pointers crosses through 0 because it rolled over from max?
That's just a very specific type of buffer overflow. strcpy is the wrong function to use in such a scenario.
> Thanks. I doubt you could have provided a better example of the precise problem.
strcpy is for very specific situations. Writing it in a simple way isn't the problem. It's not like it's even theoretically possible for strpy to avoid overflowing. It doesn't have enough information, only two pointers.
In short, this is a bad API, it is not a bad implementation.
Termination condition. Which data applies and does it have to be reread after the assignment?
Note that this "elaborated" version,
while(*src) {
char c = *src;
src++;
*dst = c;
dst++;
}
if the pointers were volatile, would actually read the source value twice. char c;
do {
c = *src;
src++;
*dst = c;
dst++;
} while (c != '\0');
But, even here, people reading the code can reason about the volatiles without having to go look up the standard.And don't even get me started about compiler implementation of volatile.
No coding style is immune to problems caused by changing the number of reads to a variable. Volatile is an exceptional thing that must be treated with care and heavy documentation. It has no relevance to the quality of a normal-code strcpy.
Or rather, buffer overflows arise because someone did not think of lengths, and somehow this mentality became ingrained in a rather large number of programmers; this code is perfectly fine if the length of the source string is always less than or equal to that of the destination. I think saying "never use X" is almost always a bad idea, since it discourages reasoning, and not thinking about lengths is what lead to this problem in the first place.
On the other hand, "never use gets()" is more appropriate, since whoever came up with that function clearly never thought of lengths...
I assume you wanted your code to look like this:
void *memset(void *_p, unsigned v, unsigned count) {
unsigned char *p = _p;
- while(count-- > 0) *p++ = 0;
+ while(count-- > 0) *p++ = v;
return _p;
}
In which case, I'm not sure what you're complaining about. Is it that memset here has a non-standard signature (v is supposed to be int, not unsigned)? Or that the "before" code ignored v? Or the un-braced loop body on the same line? Or is the some other problem I'm overlooking (which is possible, it's been about a decade since C was my main day-to-day language)?[Edited to try to coax better text formatting from HN]
Oh I know, the writer obviously wants very very badly to write this while loop as a one-liner. That's the start of all the other badness. As a reader of this code now, I have to keep every possible combination of post-increment side effects and precedence rules in mind before this code makes sense.
And yes, I know code like this is quoted in books as example of how cleverly you can write "powerful" one-liners. As far as I'm concerned, it's just a dumb show of alpha-coder pride and it's not a surprise to me that a serious bug was buried here.
Also, I assume that once discovered, the time to fix it is usually negligible. Usually. And as long as you don't count the time for full deployment in production...
i guess whoever wrote that line thought the same.
It luckily only took me a full day to find within the codebase..