Fun with UB in C: returning uninitialized floats

June 30th, 2015

The average C/C++ programmer's intuition says that uninitialized variables are fine as long as you don't depend on their values.

A more experienced programmer probably suspects that uninitialized variables are fine as long as you don't access them. That is, computing c=a+b where b is uninitialized is not harmless even if you never use c. That's because the compiler could, say, optimize away the entire block of code surrounding c=a+b under the assumption that c=a+b, where b is proven to be always uninitialized, is always undefined behavior (UB). And if it's UB, the only way for the program to be correct is for this code to be unreachable anyway. And if it's unreachable, why waste instructions translating any of it?

However, the following code looks like it could potentially be OK, doesn't it?

float get(obj* v, bool* ok) {
  float c;
  if(v->valid) {
    *ok = true;
    c = v->a + v->b;
  else {
   *ok = false; //not ok, so don't expect anything from c
  return c;

Here you return an uninitialized c, which the caller shouldn't touch because *ok is false. As long as the caller doesn't, all is well, right?

Well, it turns out that even if the caller does nothing at all with the return value – ever, regardless of *ok – the program might bomb. That's because c could be initialized to a singaling NaN, and then say on x86, when the fstp instruction is used to basically just get rid of the return value, you get an exception. In release mode but not in debug mode, some of the time but not all the time. This gives you this warm, fuzzy WTF feeling when you stare at the disassembled code. "Why is there even a float here in the first place?!"

How much uninitialized data is shuffled around by real-world C programs? A lot, I wager – likely closer to 95% than to 5% of programs do this. Otherwise Valgrind would not go to all the trouble to not barf on uninitialized data until the last possible moment (that moment being when a branch is taken based on uninitialized data, or when it's passed to a system call; to not barf then would require some sort of a multiverse simulation approach for which there are not enough computing resources.)

Needless to say, most programs enjoying ("enjoying"?) Valgrind's (or rather memcheck's) conservative approach to error reporting were written neither in assembly which few use, nor in, I dunno, Java, which won't let you do this. They were written in C and C++, and most likely they invoke UB.

(Can you touch uninitialized data in C without triggering UB? I seriously don't know, I'm not a language lawyer. Being able to do this is actually occasionally useful for optimization. Integral types for instance don't have anything like signaling NaNs so at the assembly language level you should be fine. But at the C level the compiler might get needlessly clever if it manages to prove that the data is uninitialized. My own intuition is it can never prove squat about data passed by pointer because of aliasing and so I kinda assume that if I get a buffer pointing to some data and some of it is uninitialized I can do everything to it that I could in assembly. But I'm not sure.)

What a way to make a living.

1. whitequarkJun 30, 2015

Integral types actually have an equivalent of signaling NaN on IA64, which IIRC was taken into consideration by the C workgroup.

2. Yossi KreininJun 30, 2015

Only in registers, right? It's not like they tag every 8b pixel in an image as "a number" as opposed to "not a number", do they? So there's a substantial grey area here in practice (int local[3] might land in registers whereas int local[256] probably won't, etc.), and say malloced ununitialized bytes are always OK. I wonder if the C standard left the option to touch these without getting hosed or did they "make it simple" and said theoretically you're hosed with any type, anywhere. (Intuitively I'd assume they did the latter...)

3. FUZxxlJul 1, 2015

Compilers don't need to use fstp to get rid of an FPU register. They can instead use the ffree ; fincstp combination or even the undocumented ffreep instruction for that.

4. saarniJul 1, 2015

As whitequark pointed out, integral values may have "signaling NaN" equivalents: trap representations.

"Any type (except unsigned char) may have trap representations, but no type is required to have them."

Hence reading uninitialized automatic storage integral variable may contain a trap.

5. LGJul 1, 2015

I'm not following, what would this trap representation look in like in practice? You mean that the compiler would generate an "int3" (or something like that) instead of the actual uninitialized int read?

6. MarkJul 1, 2015

The real language lawyers hang out on Stack Overflow:

The short answer is that for once, C is saner than you would expect. If you ever take the address of a variable, and the type in question has no scary values like signaling NaNs, then the behavior is well defined: you merely have to contend with assembly-like behavior. (I did not know this five minutes ago! Your blog post made me learn something!) If you want a long answer, go read the top answer on Stack Overflow, or even the top. But the upshot of all this is that the following awesome trick is actually legal C:

For those who have not seen it and are wondering whether it's worth following the link: it's a very simple data structure that uses uninitialized memory to achieve O(1) time for an operation that a naĂŻve implementation would use linear time for. (It's possible to get O(1) time without resorting to uninitialized memory by using a hash table, but that takes work to implement. This is easier to code and runs faster.)

7. Matthew FernandezJul 1, 2015

I suspect the answer to your final doubt depends on how clever your compiler is at inter-procedural analysis and how much visibility it has within the current translation unit. Of course this always triggers UB in the technical sense, so while you might get away with it you probably shouldn't do it.

8. hudsonJul 1, 2015

Minor typo: "c = v->a + b->b;" should probably be "c = v->a + v->b;"

9. Yossi KreininJul 2, 2015

Thanks – fixed

Post a comment