Fixing a 37-year-old bug by merging a 22-year-old fix
cvsweb.openbsd.org
cvsweb.openbsd.org
The general structure should be
FILE *fp = stdin; /* sane default */
then override with a new pointer from fopen if necessary.In fact, looking at this more closely, note how fp is scoped to the body of the for loop. That's where it should be defined:
for (firsttime = 1; ; firsttime = 0) {
FILE *fp = stdin;
/* logic flips it to fp = fopen(...) as necessary */
}
[Note: the C compiler Joy was working with probably didn't support block scope declarations except at the top of the function. This comment applies to merging the fix using the environment of today.]Note how the fclose at the bottom of the loop ends up closing stdin in the case that fp == stdin! It ensures that this only happens once, but you have to analyze it to see that this is true. I would write this as:
if (fp != stdin) /* defensive! */
fclose(fp);
Making it an infinite loop terminated by calling exit is unnecessary. There is already a continue in the loop, and a break in an inner loop; another break instead of the exit, and a "return status;" at the bottom wouldn't hurt. Better yet: /* Enter loop whenever there are args left to
process, or even if there are no args if it is
the first time through (to process stdin instead). */
for (firsttime = 1; firsttime || *argv; firsttime = 0) {
FILE *fp = stdin;
if (*argv) {
/* have args; do the fopen to replace stdin */
/* set status = 1 and continue if cannot fopen */
}
/* do head logic */
if (fp != stdin)
fclose(fp);
}
return status;
C itself was only a couple of years old when that code was written originally, and the systems were small. Whereas I've been using it since 1989. Not criticizing the original!Also the original is super efficient which is pretty cool, and (at least when correct) is arguably more readable (probably not so much re. extendable) since it seems like there's no way to look at the code and think you know what it's doing when you actually do not.
For starter, you don't have prescience. You can't prevent future version from growing new alternate code paths. As the code evolve, checking that every initialization-free variable will still get initialized is a burden and easily overlooked. Then there are more potent evolution that will make it just impossible: the code will get generalized, or moved into a library, or get a a context object to be able to run multiple instances. Or will get a state machine to handle complex combination of options.
You're relying on the compiler telling you the variable will get initialized. But the compiler may change. Or the human compiling it might not look at warnings.
OTOH, double-iniitalization is mostly a myth. If the compiler can tell the variable may not get any initialization and warn you are exactly the same as the case the optimizer will be able to see the double init and optimize it out. I've looked at generated code and see compilers properly eliminate redundant stores.
You're better off betting on optimization and get bitten by occasional double-stores than get bitten by uninitialized variables and give system penetrators yet another free lunch.
It doesn't hold water, because potentially initializing variables to wrong values is just as much as consequence of using that static tool, as it is a consequence of an "always initialize" coding habit.
The static tool produces a report of uninitialized variables or potentially uninitialized ones. The developers react to this by fixing all those instances, so that the tool is "shut up". Once that happens, they have a code base for which the tool doesn't report anything, just like the other developers who always initialize.
There is no reason to believe that either team has a lower or higher rate of incorrect-initial-value bugs. Or is there?
I would argue that the developer who is writing the code, at that moment, quite possibly has a better idea of what the correct initial value should be, than someone who is running a tool across a large code base and following up (by him or herself).
Really, the coding convention should be "always initialize, with the correct value which doesn't need to be overwritten later."
Even in imperative languages, we should program as "functionally" as is reasonably possible.
Imperative code that mutates local variables should only do so if it implements iteration. If the code has no loops and is setting local variables conditionally, that shows that it's trying to influence the behavior of some common code where the control flow paths merge --- instead of putting it into a function!
In the specific program we are looking at, for instance, the function is trying to use the head processing block at the end, but over different input streams. This can be done by putting it into a function, so then there is no assignment to the fp variable:
if (!*argv) {
head_processing(stdin);
} else {
FILE *fp = fopen(*argv, "r");
/* check for fopen failure ... */
head_processing(fp);
fclose(fp);
}
We can eliminate assignments out of loops, too, but that requires tail recursion: /* pseudo code */
head(int argc, char **argv, int first_time)
{
if (argc == 0 && first_time) {
return head_processing(stdin);
} else if (argc == 0) {
return 0;
} else {
FILE *fp = fopen(*argv, "r");
/* head_processing checks for null */
int status = head_processing(fp);
fclose(fp);
/* tail call, if status was good */
return (status != 0)
? status : head(argc - 1, argv + 1, 0);
}
}
C programmers will tend not to be comfortable with this style, and it requires tail call support in the compiler to not generate O(N) stack. So for pragmatic reasons, you can't make a coding convention out of this. let f = case something of
Foo -> 10
Bar -> 22
cannot reasonably take the same form in C, despite there not being a "real" update going on.This means in C you wind up writing things like,
int f;
switch(something) {
case Foo:
f = 10;
break;
case Bar:
f = 22;
break;
default:
/* ... handle error ... */
}
And the flow is basically the same, but with an additional degree of freedom to make a mistake, and there some static checking can have your back.Incidentally, on GCC and Clang I recommend building with -Wswitch-enum, which will help point you at the above when you add something to the enum that contains Foo and Bar.
If it makes you feel better, move this code to a function that returns a single value and initialize the variable once in one place (and then mark e value const!). I actually also agree this is a better option, but semantically the if/else is clearer than the if/default, so I would not fall back to if/default over if/else.
As for relying on the compiler: turn that warning on manually and turn that warning into an error manually. (Do not turn all warnings on, as upgrading your compiler can break your build, but turn on warnings that you actively want to verify your code against and have cleared your code against (including this one.)
I will also argue that you should treat your static analysis similar to a dynamic unit test on your code: it doesn't matter if some people are using a compiler that doesn't have this feature (though that seems unlikely as I believe all major compilers support this feature) as the maintainer will, in the same sense that running unit tests sometimes requires more dependencies than running the underlying code.
Again, though, if you are very used to the if/default control flow, I can appreciate wanting to use it: I just would rather see people take advantage of static language analysis that is present to deal with silly errors, and then arrange their code to better handle high-level logic errors.
FILE *f;
int i = 1;
...
if(argc < 2) {
f = stdin;
goto do_head;
}
...
while(i<argc) {
f = fopen(argv[i], "r");
/* check for failure to open */
...
do_head:
/* head'ing code goes here */
...
fclose(f);
i++;
}
That being said, OpenBSD code is still far more concise and readable than GNU; compare coreutils' head.c here:http://code.metager.de/source/xref/gnu/coreutils/src/head.c
GNU head has a bit more functionality, but I don't think the increase in complexity - nearly 10x more lines - is proportional to that.
http://www.opensource.apple.com/source/text_cmds/text_cmds-8...
(Although I think the use of malloc() and strcpy() to handle the -[0-9]+ case seems rather unnecessary...)
edit: Wow, not sure what's offensive here...
Threads involving OpenBSD can involve fierce downvoting so it's probably a good idea to be clear about what you're saying.
The site is now more political than ever and this just seems to fuel the "silence what you don't agree with" type voting, which I don't think is a good thing.
The pythonic tendency towards being explicit about what we mean to say serves us well even in prose. :-) I agree with the OP, it's a pretty neat engineering feat. I think the downvotes would likely have been avoided had that admiration been more clearly stated.
Theo de Raadt co-created AnonCVS back in 1995/1996. He and Chuck Cranor from AT&T Labs Research published a paper in 1999 about it.
OpenBSD was the first open source project to make access to their source repository public to non-developers, before AnonCVS you needed commit access to review history, annotations and even checkout. OpenBSD had a read-only CVS tree even before their website was created in 1996.
There are no known prior cited examples of a project providing this level of transparency to the development process, most open source projects at the time only provided "snapshots" of development in the form of compressed source directory tarballs, excluding RCS files. SUP and FreeBSD's ctm(1) only let you merge "current" remote changes to a local non-versioned directory.
http://www.openbsd.org/papers/anoncvs-paper.pdf
http://www.openbsd.org/papers/anoncvs-slides.pdf
And here's an additional historical tidbit relating to FreeBSD and AnonCVS: https://twitter.com/canadianbryan/status/517714611089719296
http://cvsweb.openbsd.org/cgi-bin/cvsweb/src/usr.bin/head/he...
You're one click away from the diff.
Maybe some important shell scripts hang in a loop because they expect head to report an unsuccessful termination status when a file doesn't exist. Maybe some situation exists where a privileged system script can be fooled into looping by an unprivileged user.
Here is the mailing list discussion:
https://www.mail-archive.com/misc@openbsd.org/msg132628.html
There is no report of any actual DDoS. Craig Skinner was really just testing tools on nonexistent files!
So the commit comment just follows from some general hypothesis that incorrect exit statuses from tools can be exploited in some way by attackers.
He and his team lead had no idea how such a bug missed 11 years of peer-QA through all edits on that module / activity.
It was as simple as converting to string and back and hitting an edge case on internationalization with commas vs periods.
Another I've seen is the potential landmine that can go of at ANY time and then one day, 5 years in the future, it hits you at 2AM and you can't figure out WTF is happening.
Or, if the website reads some string from the back end, like say a config value "DefaultCreditLimit", the user night be able to modify the interpretation of it.
I've come to the conclusion that this automatic behavior that tries to guess at the format required is simply wrong. If a program wants user-specific formatting, it should opt-in.
That said, my favorite localization issues stem from CH/Switzerland. Numbers are formatted as 1'000.00 there (someone correct me), which is especially odd:
1) That's a weird decimal separator. DE uses a , and as far as I know FR uses a , as well. I _assume_ IT uses a , - so why is this a . in CH?
2) The ' really kills a lot of naive parsers and causes quite a bit of frustration in other fields (think OCR - some overly fat 1'000 might turn into a 11000 with bad luck/crappy image quality or (marginally better) lead to 1000 where means 'I think here's a character, but no clue what that might be')
Yes, different conventions exist (in this case the thread drifted to bugs based on locales, so obviously we're talking about different conventions here). I .. don't understand what you're saying.
(and my original post is missing an asterisk in two places, as in 1?000 as OCR result for example - messed up the formatting in the middle of the night)
2) Actually, I find it much more confusing that people are constantly using . and , for two completely different functions and not get confused about it. Mistaking a . for a , and vice-versa is quite easy.
And that not even considers the likely confusion for people coming from countries with different rules.
OTOH an ' is highly distinct visually from both a , and . so it's always clear that it can't be the decimal separator.
Fortunately, after the predicable mass panic, they were eventually able to relocate the satellite.
This sounds extremely sloppy, especially for satellite control software. Anything not fitting exactly the expected format should lead to an error so the operator can immediately correct it, not be silently ignored.
http://cvsweb.openbsd.org/cgi-bin/cvsweb/src/usr.bin/head/he...
does this line predate the while loop?
or boolean values in C?
> while ((ch = getopt(argc, argv, "n:")) != -1) {
bool was introduced in C99
Also, as the other guy said, that code has while loops.
firsttime=True
while (True) {
..
..
..
firsttime=False
}People educated in the Wirth way would say "that's not a for loop; there is no way to tell how often it will iterate when it is first entered" and write it as a while loop.
Old-school C programmers seem to think one iteration construct is enough for everybody and like to write any iteration as a for loop, ideally with a few commas and no actual statements, as in the classic strcpy:
char *strcpy(char *dst, CONST char *src)
{
char *save = dst;
for (; (*dst = *src) != '\0'; ++src, ++dst);
return save;
}
(from http://www.ethernut.de/api/strcpy_8c_source.html)Programmers with soe Wirth-style training do at least something like
char *
strcpy(char *s1, const char *s2)
{
char *s = s1;
while ((*s++ = *s2++) != 0)
;
return (s1);
}
(from http://www.opensource.apple.com/source/Libc/Libc-167/gen.sub...)[If you google, you can find way more variations on this. for example, http://fossies.org/dox/glibc-2.20/string_2strcpy_8c_source.h... does a do {...} while(....) that I am not even sure is legal C; it uses offsets into the source to write into the destination]
From your example I guess you are definitely in the Wirth (Pascalish) camp, beyond even that second state. Classic C programmers would either use 0 and 1 for booleans or #define macros FALSE and TRUE (modern C has true booleans, but that extra #include to get it increases compilation time, so why do it?
I am in te Pascal-ish camp. I like to say C doesn't have a for loop because its 'for' is just syntactic sugar for a while loop.
It's closer to an iterator over argv.
First, it sets the firsttime=1 flag, then it tries to open the first argument (file). After the first arg, it sets firsttime=0. It only breaks out of the loop once argv is exhausted.
It's the other way around. Very early C didn't have a for loop. It seems it was introduced in Unix V5: http://minnie.tuhs.org/cgi-bin/utree.pl?file=V5/usr/c/c00.c
And a for loop is basically just a while one, but they are sometimes used in different ways than the usual `for (size_t i = 0; i < len; ++i)`. One of my favorites is http://git.musl-libc.org/cgit/musl/tree/src/internal/floatsc....
>or boolean values in C?
1 and 0 are boolean values in C. But C didn't have a boolean type until C99 (called _Bool or bool after including <stdbool.h>).
c|32 will do a bitwise downcase of an ascii character so, "INFINITY", "infinity", "INfinITy" will all be accepted. In any of those cases i == 8 at the end of the loop and is still in scope, so the following if statement will ultimately return + or - Infinity.
> What does that loop even do?
for (i=0; i<8 && (c|32)=="infinity"[i]; i++)
if (i<7) c = shgetc(f);
It's a pattern matching loop. The 'c' variable is a character which is read from FILE * f. In this case, it's attempting to match the string "infinity" which has a length of 8. With ASCII, uppercase letters start from hex value 0x41 for the letter 'A'. Lowercase letters start from hex value 0x61 for the letter 'a'. Thus the difference between lowercase and uppercase is 0x20 which is 32 decimal or 100000 in binary. Thus, if you or the value of 'c' with 32 (0x20 or 00010000), you're setting the 2^5 bit of the character which converts uppercase to lowercase and leaves lowercase unchanged since all lowercase characters already have that bit set.I'm sure you can figure out the rest. As long as the character matches the corresponding index position in the character array "infinity", the for loop keeps matching characters. Eventually, a successful match results in a return value of sign * INFINITY from the function.