On uninitialized variables

Quite busy week, sorry for being silent.
I wanted to talk about an annoyance I discovered with all my C/C++ compilers.
Here is quite interesting presentation from Halvar Flake:
Attacks on uninitialized local variables
After reading it I wanted to verify my compilers and created a small C file. I wanted to check if the compilers would warn me of a potential uninitialized variable. The source code was pretty simple:

void const_ptr_acceptor(const int *);
int control_func(void)
{
int x;
return x + 1; // compiler emits a warning
}
int check_func(void)
{
int x;
const_ptr_acceptor(&x); // we do not modify x here!
return x + 1; // compiler does not emit a warning
}

We have two functions, they both use an uninitialized variable. The only difference is the call to const_ptr_acceptor() which promises not to modify ‘x’. I compiled this source code with all warnings turned on. I was expecting two warnings from the compiler: the first warning about ‘control_func’ and the second warning about ‘check_func’. However, there was only one warning:

E:\hex\const_ptr>cl /Wall /c const_ptr.cpp
Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 14.00.50215.44 for 80×86
Copyright (C) Microsoft Corporation. All rights reserved.
const_ptr.cpp
e:\hex\const_ptr\const_ptr.cpp(6) : warning C4700: uninitialized local variable ‘x’ used

I tried with all available compilers, but they were unanimous in their behavior: as soon as we pass a pointer to a variable, the compiler thinks that it is initialized. We explicitly specify with the const specifier that the function does not modify the variable, but the compilers seems to ignore it.
I compiled the code with Microsoft Visual Studio, Borland BCB6, GNU C, Intel compilers.
Still have no explanation why all compilers behave this way.

This entry was posted in Programming, Security. Bookmark the permalink.

16 Responses to On uninitialized variables

  1. Purplet says:

    Possible reasons :
    - The variable could be initialized with a const_cast (a compiler *cannot* assume a const is really .. uh constant). If I were the compiler I would warn anyway, but actually I’m not a compiler :S
    - It might behave that way to avoid useless warning in case the & operator is overloaded (I don’t even know if it’s possible). But truly it should know that in the above example & is not overloaded for int :S

  2. Ilfak Guilfanov says:

    I agree that a compiler can not assume that something pointed to a “const ptr” is constant (because the same object can be pointed to by another pointer and modified on the fly).
    Nevertheless the compiler can and does enforce that a “const” pointer does not modify the object…

  3. Mr. PickNit says:

    void const_ptr_acceptor(const int *p)
    {
    *(int*)p = 42;
    }

  4. Ilfak Guilfanov says:

    Yes, you can modify even constant objects. The goal is to get a warning from the compiler: a hint that very probably the programmer has made a mistake…

  5. Ryan Russell says:

    Well, I’ll state the obvious:
    Once the compiler sees an access that is capable of initializing the variable, it stops tracking it for that purpose. Even though you have handed it a trivial case where it could statically know that it was uninitialized, the compile authors have decided not to bother with the trivial case, since you basically need runtime analysis to solve the general case.

  6. rob says:

    with the const int* construction you specify that function doesn’t modify pointer to value, not the value itself.
    if you do something like
    int q=0;
    void const_ptr_acceptor(const int *);
    int control_func(void)
    {
    int x;
    return x + 1; // compiler emits a warning
    }
    int check_func(void)
    {
    int x;
    const_ptr_acceptor(&x); // we do not modify x here!
    return x + 1; // compiler does not emit a warning
    }
    void const_ptr_acceptor(const int *p)
    {
    }
    void const_ptr_acceptor2(const int *p)
    {
    *(int*)p = 42;
    }
    void const_ptr_acceptor3(const int *p)
    {
    p = &q;
    }
    int x;
    int *d;
    const_ptr_acceptor2(&x);
    const_ptr_acceptor3(&x);
    const_ptr_acceptor3(d);
    compiler release two warnings one for uninitialized x and second for d – as you expect
    regards

  7. Mr. PickNit says:

    rob, with “const int*” you don’t specify that the function doesn’t modify the value pointed to. You just specify that you accept not only an int* but also a const int*.
    By using a cast, you can turn a const pointer into a mutable pointer and modify the object pointed to by the const pointer as long as the object pointed to can be modified. (Of course, that’s bad style.)
    const int c = 0;
    int v = 0;
    const_ptr_acceptor(&c); // OK
    const_ptr_acceptor2(&c); // undefined behavior
    const_ptr_acceptor2(&v); // OK
    const_ptr_acceptor3(&c); // OK

  8. Ilfak Guilfanov says:

    Ryan, in our case the reference is not to modify the variable since it is a const pointer.
    As about using a type cast to modify a variable, this should not be taken into account for warnings. In fact you can do almost anything using casts. For example, if you use
    *(__int64*)ptr = 0;
    you are overwriting much more than the pointed variable itself.
    The sole purpose of warnings are to help the programmer to discover problems with the code. In our case a warning about uninitialized variable would be very useful.
    I start to understand why compilers do not display the warning. To display it, they must perform data flow analysis and find all used but not defined variables. Alas, before performing this analysis they must throw away all ‘const’ modifiers. The ‘const’ modifier should not be used for code generation. As Purplet noted in the very first comment, a ‘const’ object should not be considered as constant for the code generation purposes.
    So, the short answer is: because displaying such a warning would require a separate data flow analysis. BTW, do you know any source code checkers performing better than compilers in this aspect?

  9. mikeb says:

    Ilfak -
    I think that in this case detailed data flow analysis would not be required. At the point that
    const_ptr_acceptor(&x)
    is called, the compiler knows that it is passing in an address of a read-only int (since const_ptr_acceptor() has promised by its declaration not to write the location pointed to by the int*). Since the int whose address is being passed in has not been initialized yet, the compiler really has enough info at that point to issue a warning I think.
    For example, in C# if you passed in an uninitialized reference to a method that has its parameter marked as “readonly”, the compile would fail (an error, not warning).
    C/C++ obviously can’t issue an error in this case, but I think a warning is not unreasonable.

  10. Mr. PickNit says:

    Ilfak,
    *(__int64*)ptr = 0;
    invokes undefined behavior in our example (unless int is __int64), whereas
    *(int*)ptr = 0;
    does not invoke undefined behavior (if the pointer passed to the function points to a modifyable object which it is in your example).
    mikeb,
    my point is that (as per ISO 9899 and ISO 14882) const_ptr_acceptor() does not promise not to write to the object pointed to by the pointer passed. Without seeing the actual code of const_ptr_acceptor() the compiler cannot tell whether the object will be modified or not.

  11. Ilfak Guilfanov says:

    Mr. PickNit,
    I think there is a confusion with the interpretation of the ‘const’ keyword.
    ISO 9899 at paragraph 6.7.5.1 gives an example:

    EXAMPLE The following pair of declarations demonstrates the
    difference between a ‘‘variable pointer to a constant value’’ and a ‘‘constant pointer to a variable value’’.

    const int *ptr_to_constant;
    int *const constant_ptr;

    The contents of any object pointed to by ptr_to_constant shall not be modified through that pointer, but ptr_to_constant itself may be changed to point to another
    object.

    So in our case the compiler knows that the pointer passed to the function cannot modify ‘x’ and therefore it can issue a warning.
    Another question is whether const objects can be considered as constant by the compiler. The answer is no – for example, such constants can be modified by hardware.
    For the compiler it means that it cannot assume that ‘x’ has not changed its value after the call. The compiler must reload ‘x’ from the memory.
    As about the undefined behavior for the cast to __int64*, ISO 9899 says (6.3.2.3, point 7) that this conversion is acceptable if the resulting pointer is correctly aligned.

  12. Mr. PickNit says:

    Ilfak, my code does not modify the object through a const pointer. It uses a typecast to get a *different* pointer through which the object can be modified.
    Here’s the relevant definition of const from 6.7.3:

    If an attempt is made to modify an object defined with a const-qualified type through use
    of an lvalue with non-const-qualified type, the behavior is undefined.

    The cast from int* to __int64* is valid, but dereferencing the resulting pointer is not.

  13. Ilfak Guilfanov says:

    My quote from the standard was to say that ‘const’ pointers are not supposed to modify the object. This is the meaning of the ‘const’ qualifier (why would it be in the declaration otherwise?)
    Your quote of 6.7.3 is irrelevant because we have no const objects in the source code.
    Here is a slight modification of the initial source code:

    int check_func2(void)
    {
    int x;
    const int *p;
    p = &x;
    return x + 1;
    // compiler does not emit a warning about x
    }
    

    Here the compiler has all information about x. However, the warning is not generated. (there is another warning about unused p, but this is irrelevant).

  14. mikeb says:

    Mr. PickNit:
    I realize that your example will attempt to modify the memory pointed to by the const pointer.
    However, the fact that you cast away the constness just means that your code has undefined behavior – it can do different things when compiled by different compilers or on different platforms (for example, what if the compiler places const variables into ROM, which it is permitted to do).
    The entire purpose of having const parameters is to allow functions to state that they have no intention of modifying the value. If the function goes ahead and modifies the value in spite of that promise, then all bets are off as far as standard C++ is concerned. See ISO/IEC 14882 5.2.11 (7) and 7.1.5.1 (especially paragraphs 3 and 4):
    ——————-
    3 A pointer or reference to a cv-qualified type need not actually point or refer to a cv-qualified object, but it is treated as if it does; a const-qualified access path cannot be used to modify an object even if the object referenced is a non-const object and can be modified through some other access path. [Note: cv-qualifiers are supported by the type system so that they cannot be subverted without casting (5.2.11). ]
    4 Except that any class member declared mutable (7.1.1) can be modified, any attempt to modify a const object during its lifetime (3.8) results in undefined behavior.
    ——————-
    Therefore I think it is reasonable (though not required) that a compiler generate a warning diagnostic when passing an uninitialized variable to a function that declares the corresponding paramter to be const.

  15. Mr. PickNit says:

    mikeb, there is no const object in the code I am talking about:

    void const_ptr_acceptor(const int *p)
    {
    *(int*)p = 42;
    }
    int check_func(void)
    {
    int x;
    const_ptr_acceptor(&x);
    return x + 1;
    }

    (Of course, it’s not allowed to pass the address of a const object
    to this version of const_ptr_acceptor, as I pointed out earlier.)
    ISO 14882 5.2.11 paragraph 7 is non-normative and is just a reminder
    of 7.1.5.1 paragraph 4.
    ISO 14882 7.1.5.1 paragraph 3 defines the static consequences of using
    const (requiring a diagnostic from the compiler if the rule is
    violated); it does not apply to the code above as no pointer to a
    const-qualified type is used to modify the object. This equivalent
    code should make this point clearer:

    void const_ptr_acceptor(const int *p)
    {
    int *q = (int*)p; /* not forbidden by any wording in the standards, in fact that’s what C++’s const_cast is for */
    *q = 42; /* not forbidden by any wording in the standards, unless p actually points to a const object, which depends on the caller */
    }

    ISO 14882 7.1.5.1 paragraph 4 defines the consequences at runtime of
    using const (no diagnostics from the compiler are required or even
    possible in the general case); it does not apply here as there’s no
    const object in the code above.

  16. mikeb says:

    Mr. PickNit:
    You are correct. On closer look at the standard I see that your example is, in fact, well-defined.
    However, the declaration of const_ptr_acceptor() is making a claim (although a claim that can possibly be legally broken) that it will not modify the int passed in via the pointer.
    I still think that it’s reasonable (though certainly not required) and well within the capabiities of the current state of the art for a compiler to issue a warning if the address of an uninitialied int is passed to a call to const_ptr_acceptor().