Skip to content

CWE-824: Access of Uninitialized Pointer - C++

Overview

C++ raw pointer members have the same uninitialized-by-default behavior as C - a class with a raw pointer field that isn't set on every constructor path holds garbage until it's assigned. Modern C++ removes most of this risk structurally: default member initializers give every pointer a defined value, and smart pointers default to a null, checkable state instead of an undefined one.

Primary Defense: Give every pointer and scalar member a default member initializer (= nullptr, or a smart pointer that defaults to null), so no constructor path - including ones added later - can leave it unset, and so T x;, T x{};, new T and new T() all produce the same fully-initialized object. Prefer a smart pointer to a raw one for any member that owns something, because its default state is both defined and safely destructible.

Common Vulnerable Patterns

Pointer Member Left Unset by One Constructor Path

// VULNERABLE - default constructor never assigns file/db
class ResourceManager {
    File* file;
    Database* db;

public:
    ResourceManager() {
        // file and db are uninitialized here
    }

    void init() {
        file = new File();
        db = new Database();
    }

    ~ResourceManager() {
        delete file;  // may delete a garbage pointer if init() was never called
        delete db;
    }
};

Why this is vulnerable: If init() isn't called before the object is used or destroyed, file and db still hold whatever the memory contained when the object was constructed - dereferencing or delete-ing them is undefined behavior.

The destructor is what makes this unavoidable rather than merely likely. A caller who constructs a ResourceManager and then takes an early return has still triggered two deletes on indeterminate pointers, without ever calling a method. There is no usage discipline that avoids it, which is why the fix has to be in the constructor rather than in a rule about calling init().

new T Where new T() Was Meant

// VULNERABLE - the parentheses are what decide whether members are initialized
struct Header {
    const char *name;   // no default member initializer
    int length;
};

void parse() {
    Header *a = new Header;      // default-initialized: name and length indeterminate
    Header *b = new Header();    // value-initialized: name is nullptr, length is 0

    Header onStack;              // indeterminate, same as `new Header`
    Header zeroed{};             // value-initialized, same as `new Header()`

    use(a->name);                // reads an indeterminate pointer
    use(onStack.name);           // and so does this
}

Why this is vulnerable: For a class with no user-provided default constructor and no default member initializers, new Header performs default-initialization, which for scalar members means no initialization at all - the members hold whatever was in the allocated bytes. new Header() and new Header{} perform value-initialization, which zero-initializes the object first, so name is a null pointer.

The two spellings differ by two characters, and the unsafe one is the shorter. Neither compiles with a warning, and -Wuninitialized sees nothing because from the compiler's point of view the object was initialized exactly as the source asked.

Worse, whether new Header() zero-initializes at all is a property of Header that a later edit can silently remove. Value-initialization only zero-initializes when the class's default constructor is not user-provided. So:

struct Header { const char *name; int length; };            // new Header() zeroes
struct Header { Header() {} const char *name; int length; }; // new Header() does NOT
struct Header { Header() = default; /* ... */ };             // zeroes (defaulted in-class)

Adding an empty Header() {} - the most harmless-looking edit in C++ - turns every new Header() in the codebase from zeroing into leaving the members indeterminate, with no diagnostic and no change at any call site. The same applies to = default moved out of the class body: Header() = default; declared in-class is not user-provided and keeps the zeroing, while Header::Header() = default; defined out-of-line is user-provided and loses it.

The rule that removes the whole question is to give every member a default member initializer. Then Header h;, Header h{};, new Header and new Header() all produce the same object, and no later constructor edit can change that.

Secure Patterns

Smart Pointer Members and a Member Initializer List

#include <memory>

class ResourceManager {
    std::unique_ptr<File> file;       // default-constructs to nullptr
    std::unique_ptr<Database> db;     // default-constructs to nullptr

public:
    ResourceManager()
        : file(std::make_unique<File>()),
          db(std::make_unique<Database>()) {
        // both members are fully constructed before the constructor body runs
    }

    // No destructor, no init(): unique_ptr releases both members automatically
};

Why this works: Two independent properties are doing the work, and they cover different failure paths.

std::unique_ptr's default constructor produces null, not an indeterminate value - so a member that some future constructor forgets to mention is null rather than garbage, and the implicit destructor's release of it is a no-op instead of a delete of a random address. That is what removes the vulnerable version's worst outcome: the destructor can no longer do damage on an object nobody finished building.

The member initializer list is what closes the window in which the members are unset. Members are initialized in declaration order before the constructor body runs, so file and db hold their final values from the first statement of the body onward; and if make_unique<Database>() throws, file - already fully constructed - is destroyed, db is never constructed, and ResourceManager's own destructor does not run. The object never comes into existence, so no caller can hold one whose members are unset.

Assigning in the constructor body costs less here than is often claimed. It would not leak: the members are unique_ptrs, so they are default-constructed to null before the body starts, and if the second assignment throws, the first member's destructor runs during unwinding and releases what it holds. Exception safety comes from the member type, not from where you assign it. What the body version actually gives up is narrower - the members are constructed twice over (null, then assigned), the class stops working the moment a member is const, a reference, or has no default constructor, and there is a window at the top of the body in which the members are null while this is a valid object that could be passed to a helper. Prefer the initializer list for those reasons, not for an exception-safety guarantee unique_ptr already provides.

Where the distinction is not cosmetic is a raw pointer member. File *file; Database *db; assigned with new in the constructor body leaks the first allocation if the second throws, because a raw pointer has no destructor to run - and that is the case where the usual "use the initializer list" advice is still not enough, since a member initializer list of raw new calls leaks in exactly the same way. The fix there is the member type, which is the point of this pattern.

In-Class Initializers for Raw Pointers

#include <memory>
#include <string>

class Config {
    std::string name = "default";
    std::unique_ptr<Data> data;            // already null; no initializer needed
    int *legacy_ptr = nullptr;             // required for any raw pointer member
    int  retries    = 3;                   // and for any scalar member
};

Why this works: A default member initializer applies to every constructor that does not name the member in its own initializer list, including constructors added years later by someone who never reads this class's invariants. That is the property no amount of constructor discipline provides: discipline has to be reapplied at each new constructor, and this does not.

It is also what makes Config c; and new Config safe. As the new T pattern above shows, a class whose members have no initializers gives different results for new Config and new Config(); once every member has one, both spellings produce the same fully-initialized object and the distinction stops mattering.

Raw pointers and scalars are the members that need this. std::string and std::unique_ptr already default-construct to a defined state, so = nullptr on the unique_ptr would be redundant - it is written here only where it changes something, because an initializer that restates the default trains readers to skim them, and the ones on legacy_ptr and retries are the ones that must not be skimmed.

Testing

  • Add a unit test that constructs the object via each constructor path - including delegating and copy/move constructors - and confirms every pointer member is either valid or null before any method is called. This is the assertion that catches what the tools below cannot, because a member left indeterminate is a bug the compiler believes it was asked for.
  • Exercise delegating constructors specifically, and construct the object through the delegating one rather than only the target. A delegating constructor may not list members in its own initializer list at all - the delegation is the whole list - so anything it needs to set has to be assigned in its body, which runs after the target constructor has finished. A member the target does not initialize is therefore unset for the whole of the target's execution, and stays unset if the delegating body throws or returns before reaching the assignment. A default member initializer is what closes this, and a test that only ever calls the target constructor never sees it.
  • Enable -Wall -Wextra -Wuninitialized -O2 and treat warnings as build failures. GCC's uninitialized analysis is driven by the optimizers and reports very little at -O0, so a clean unoptimised build is not evidence. Neither GCC nor Clang warns about new T versus new T(), so this catches only the local cases.
  • Run under valgrind --track-origins=yes, exercising every constructor path including ones that throw or return early. Valgrind needs no rebuild of dependencies, which makes it the practical choice for most projects.
  • MemorySanitizer (clang -fsanitize=memory -fsanitize-memory-track-origins=2) is more precise but has a prerequisite that bites hardest in C++: it requires every library in the process to be instrumented, and that includes the standard library, so it needs a libc++ built with MSan. Linking against a stock libstdc++ produces false positives rather than a clean run, and MSan cannot be combined with AddressSanitizer in the same binary.

Additional Resources