Skip to content

CWE-401: Missing Release of Memory after Effective Lifetime - C++

Overview

Memory leaks in C++ occur when dynamically allocated memory (malloc/new) is not freed (free/delete), causing gradual memory exhaustion. Unlike garbage-collected languages, C++ has no runtime that reclaims unreachable memory, so releasing it is the program's own job - either by explicit deallocation or by RAII. In a long-running process that exhaustion is a denial of service.

Primary Defence: Use RAII (Resource Acquisition Is Initialization) so a destructor releases memory instead of a delete you have to remember: smart pointers (std::unique_ptr, std::shared_ptr) created with std::make_unique/std::make_shared rather than raw new/delete, standard containers (std::vector, std::string) rather than manually allocated arrays, and std::weak_ptr to break circular references. A class that still manages memory directly needs the rule of five.

Common Vulnerable Patterns

Circular References in Manual Memory Management

// VULNERABLE - Circular References in Manual Memory Management
class Node {
public:
    std::vector<Node*> children;
    Node* parent;

    Node(Node* p) : parent(p) {
        if (parent) {
            parent->children.push_back(this);
        }
    }

    ~Node() {
        // Delete children
        for (Node* child : children) {
            delete child;  // This deletes the child
        }
        // But parent still holds pointer to this!
    }
};

Node* root = new Node(nullptr);
Node* child = new Node(root);
delete child;  // Deletes child, but root->children still has dangling pointer
delete root;   // Tries to delete already-deleted child - double free!

// Or: forget to delete root entirely - leak

Why this is vulnerable: The parent-child cycle leaves ownership ambiguous - nothing in the code says which object is responsible for the delete. When child is deleted, the parent's children vector still holds a dangling pointer to it, so a later access there is a use-after-free and delete root frees it a second time. The leak is the same ambiguity seen from the other side: if nobody deletes the root - because the code forgets, or because an exception unwinds past the call - the entire tree stays allocated for the life of the process. Trees and graphs run into this constantly, which is why modern C++ expresses the relationship with std::shared_ptr for parent-to-child (strong ownership) and std::weak_ptr for child-to-parent (non-owning reference).

Missing Delete in Exception Paths

// VULNERABLE - Missing Delete in Exception Paths
void processData(const std::string& filename) {
    char* buffer = new char[1024];

    std::ifstream file(filename);
    if (!file.is_open()) {
        // Exception or early return - buffer leaked!
        throw std::runtime_error("Cannot open file");
    }

    file.read(buffer, 1024);

    if (file.gcount() < 100) {
        // Another early return - buffer leaked!
        return;
    }

    process(buffer);
    delete[] buffer;  // Only reached on normal path
}

Why this is vulnerable: The function allocates a buffer with new[] but only deletes it on the last line. Every path that leaves earlier skips the delete[] and leaks the buffer: the throw when the file will not open, and the return on a short read. The throw is the worse of the two, because it unwinds the stack immediately and no later statement in the function runs at all. Keeping this correct means pairing every exit path with cleanup by hand, and each error check added later is another path that can forget it.

Raw Pointers in Containers

// VULNERABLE - Raw Pointers in Containers
std::vector<Resource*> resources;

void addResource() {
    Resource* res = new Resource();
    resources.push_back(res);
    // Who owns res? Who deletes it?
}

void clearResources() {
    resources.clear();  // Clears pointers, but doesn't delete objects!
    // All Resource objects leaked
}

// Need manual cleanup:
for (Resource* res : resources) {
    delete res;
}
resources.clear();

Why this is vulnerable: The container holds pointers but does not manage the lifetime of what they point at, so nothing in the code says who calls delete. clear() and the vector's own destructor destroy the pointers - a trivial type - and leave every Resource allocated. The manual cleanup loop that would fix that is easy to forget in a destructor or an error handler, and an exception thrown before that loop runs leaks everything the container holds. Correctness ends up depending on every piece of code that touches the container agreeing on which pointers have already been freed.

Secure Patterns

RAII with Smart Pointers

// SECURE - RAII with smart pointers: the destructor runs on every exit path, including an exception
#include <memory>
#include <fstream>
#include <vector>

class FileProcessor {
private:
    std::unique_ptr<std::ifstream> file;
    std::vector<std::unique_ptr<Record>> records;

public:
    FileProcessor(const std::string& path) 
        : file(std::make_unique<std::ifstream>(path)) {

        if (!file->is_open()) {
            throw std::runtime_error("Cannot open file");
        }
    }

    void addRecord(std::unique_ptr<Record> record) {
        records.push_back(std::move(record));
    }

    // Destructor automatically called
    ~FileProcessor() {
        // Smart pointers automatically cleaned up
        // records vector destroyed -> all unique_ptr<Record> destroyed
        // file unique_ptr destroyed -> ifstream closed
    }

    // No manual cleanup needed!
};

void processFiles() {
    FileProcessor processor("data.txt");
    processor.addRecord(std::make_unique<Record>("data"));
    // Exception here? No leak - RAII handles cleanup

    // processor destroyed automatically at scope exit
    // All resources freed in correct order
}

Why this works: RAII ties resource lifetime to object lifetime: the resource is acquired in a constructor and released in a destructor. Smart pointers call delete in their own destructors, so memory is freed when the pointer goes out of scope or is reassigned - including during exception unwinding, because C++ guarantees destructors run for every fully constructed object as the stack unwinds. std::unique_ptr enforces single ownership, with move semantics preventing accidental copies; std::shared_ptr uses reference counting for shared ownership. Removing the manual delete calls removes the paths that can forget them. The same pattern extends beyond memory to any resource - files, sockets, locks - wrapped in a class whose destructor releases it.

Breaking Circular References with weak_ptr

// SECURE - breaking circular references with weak_ptr: the back-reference holds no strong count, so the cycle can drop to zero
#include <memory>
#include <vector>

class Node : public std::enable_shared_from_this<Node> {
private:
    std::vector<std::shared_ptr<Node>> children;  // Strong ownership
    std::weak_ptr<Node> parent;  // Non-owning reference

public:
    void addChild(std::shared_ptr<Node> child) {
        children.push_back(child);
        child->parent = shared_from_this();  // Set weak reference
    }

    std::shared_ptr<Node> getParent() const {
        return parent.lock();  // Convert weak_ptr to shared_ptr safely
    }

    // Destructor automatically frees children
    // No circular reference - weak_ptr doesn't prevent deletion
};

void buildTree() {
    auto root = std::make_shared<Node>();
    auto child1 = std::make_shared<Node>();
    auto child2 = std::make_shared<Node>();

    root->addChild(child1);
    root->addChild(child2);

    // When root goes out of scope:
    // 1. root destroyed -> children vector destroyed
    // 2. child1 and child2 shared_ptr count goes to 0
    // 3. child1 and child2 destroyed automatically
    // No leak, no manual cleanup needed
}

Why this works: std::weak_ptr is a non-owning reference: it does not contribute to the reference count, so it cannot keep an object alive. Parents hold their children by shared_ptr (strong ownership) and children hold their parent by weak_ptr, which leaves only one direction counting. When the root goes out of scope nothing else holds a strong reference to it, so it is destroyed; that releases its strong references to the children, whose counts drop to zero and whose destructors run in turn. The children's weak_ptr does not delay any of that - it simply becomes expired, which the holder detects through expired() or a null result from lock(). Navigation still works in both directions, without the ambiguous ownership and manual cleanup of raw pointers.

Using Standard Containers Instead of Raw Arrays

// SECURE - standard containers instead of raw arrays: the container owns its storage and frees it
#include <array>
#include <cstddef>
#include <string>
#include <vector>

void use(char* data, std::size_t size);          // may throw
void process(const std::vector<int>& values);    // may throw

// AVOID: manual memory management
void manual() {
    char* buffer = new char[1024];
    use(buffer, 1024);
    delete[] buffer;  // easy to forget, and skipped entirely if use() throws
}

// BETTER: std::vector for a dynamically sized buffer
void withVector() {
    std::vector<char> buffer(1024);
    use(buffer.data(), buffer.size());
    // freed when the vector is destroyed, including during stack unwinding
}

// BETTER: std::string for text
void withString() {
    std::string text;
    text.resize(1024);
    use(text.data(), text.size());
    // freed when the string is destroyed
}

// BETTER: std::array when the size is a compile-time constant
void withArray() {
    std::array<char, 1024> buffer{};
    use(buffer.data(), buffer.size());
    // stack-allocated: no dynamic memory to release at all
}

void processData(std::size_t size) {
    std::vector<int> data(size);  // Allocate dynamically

    if (size < 10) {
        return;  // data automatically freed - no leak!
    }

    process(data);

    // data automatically freed here too
}

Why this works: Standard containers manage their own memory through RAII. When a vector goes out of scope its destructor frees what it allocated, so cleanup still happens on an early return or during exception unwinding, and unlike raw new[]/delete[] there is no call left for the programmer to forget. Containers also resize, copy and move correctly, so manual reallocation cannot leak the old buffer. Pick by what the data is: std::vector for a dynamically sized buffer, std::string for text, and std::array when the size is a compile-time constant and the storage can live on the stack.

Custom Deleters for Non-Standard Resources

// SECURE - custom deleters: a non-memory resource is released by the same ownership rule as memory
#include <memory>
#include <cstdio>

// Custom deleter for FILE*
struct FileCloser {
    void operator()(FILE* fp) const {
        if (fp != nullptr) {
            fclose(fp);
        }
    }
};

using FilePtr = std::unique_ptr<FILE, FileCloser>;

FilePtr openFile(const char* path, const char* mode) {
    return FilePtr(fopen(path, mode));
}

void processFile(const char* filename) {
    FilePtr file = openFile(filename, "r");

    if (!file) {
        return;  // File not opened, no cleanup needed
    }

    // Process file...

    // Automatically closed when file goes out of scope
    // Even if exception thrown
}

// Custom deleter for malloc'd memory
struct MallocDeleter {
    void operator()(void* ptr) const {
        free(ptr);
    }
};

template<typename T>
using MallocPtr = std::unique_ptr<T, MallocDeleter>;

MallocPtr<char> allocateBuffer(size_t size) {
    return MallocPtr<char>(static_cast<char*>(malloc(size)));
}

Why this works: A custom deleter extends RAII to resources that delete does not release - C FILE handles, malloc'd memory, OS handles, and library-specific resources. The functor or lambda runs automatically when the smart pointer is destroyed, and exactly once, which brings the exception safety of RAII to legacy C APIs and third-party libraries. The deleter is part of the unique_ptr's type, which costs nothing at runtime for stateless functors like these.

Implementing Rule of Five

// SECURE - Rule of Five: copy and move are defined, so no two objects ever free the same allocation
class ResourceManager {
private:
    int* data;
    size_t size;

public:
    // Constructor
    // new int[n] would leave the elements indeterminate, and the copy
    // constructor below reads every one of them - value-initialize with {}
    ResourceManager(size_t n) : data(new int[n]{}), size(n) {}

    // Destructor
    ~ResourceManager() {
        delete[] data;
    }

    // Copy constructor
    ResourceManager(const ResourceManager& other) 
        : data(new int[other.size]), size(other.size) {
        std::copy(other.data, other.data + size, data);
    }

    // Copy assignment
    ResourceManager& operator=(const ResourceManager& other) {
        if (this != &other) {
            // Copy-and-swap idiom for exception safety
            ResourceManager temp(other);
            std::swap(data, temp.data);
            std::swap(size, temp.size);
            // temp's destructor frees old data
        }
        return *this;
    }

    // Move constructor
    ResourceManager(ResourceManager&& other) noexcept 
        : data(other.data), size(other.size) {
        other.data = nullptr;
        other.size = 0;
    }

    // Move assignment
    ResourceManager& operator=(ResourceManager&& other) noexcept {
        if (this != &other) {
            delete[] data;  // Free existing resource
            data = other.data;
            size = other.size;
            other.data = nullptr;
            other.size = 0;
        }
        return *this;
    }
};

Why this works: The Rule of Five says that a class needing a destructor because it manages a resource almost certainly needs custom copy and move operations too. The compiler's default copy is shallow: two objects would hold the same pointer and both destructors would free it, so the copy constructor and copy assignment here allocate a new buffer and copy into it. The move constructor and move assignment transfer the pointer and null out the source, leaving exactly one owner and avoiding the copy's cost. Copy assignment uses copy-and-swap, so if the allocation inside temp fails the original object is left unchanged. Writing all five by hand is only necessary while the class owns raw memory; a std::unique_ptr or std::vector member, with the copy operations defaulted or deleted, buys the same guarantees for less code.

Detecting Leaks

A leak does not fail a build or throw an exception, so reviewing the code is not enough on its own. Run the program under a tool that tracks allocations:

# AddressSanitizer: build with instrumentation, then run normally.
# LeakSanitizer is included and reports leaks on exit.
g++ -fsanitize=address -g -O1 program.cpp -o program
./program

# Valgrind needs no rebuild, but is slower
valgrind --leak-check=full --show-leak-kinds=all ./program

Exercise the error paths deliberately. RAII holds during stack unwinding, but a class that still manages memory manually usually leaks only when an exception fires partway through, which normal test runs never reach.

Clang-Tidy and cppcheck catch some of these statically, before the code runs:

clang-tidy program.cpp -checks='clang-analyzer-cplusplus*,cppcoreguidelines-owning-memory'
cppcheck --enable=warning program.cpp

Two structural habits keep leaks out of containers and interfaces in the first place: store std::vector<std::unique_ptr<T>> rather than a container of raw pointers, so clearing the container frees the elements; and express "may be absent" with std::optional rather than a nullable pointer, so absence does not imply an ownership question.

Additional Resources