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++ requires manual memory management or RAII patterns to ensure resources are released, making memory leaks a critical security concern leading to denial of service and crashes.

Primary Defence: Use RAII (Resource Acquisition Is Initialization) with smart pointers (std::unique_ptr, std::shared_ptr) to automate memory management, avoid raw new/delete in favor of std::make_unique/std::make_shared, use standard containers (std::vector, std::string) instead of manual arrays, implement rule of five for classes managing resources, and use std::weak_ptr to break circular references.

Common Vulnerable Patterns

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 is this vulnerable: The circular reference between parent and children creates ambiguous ownership - it's unclear which object is responsible for deletion. When child is deleted, its destructor deletes its children, but the parent's children vector still holds a dangling pointer to the deleted child. If the parent later tries to access or delete that child, it causes use-after-free or double-free vulnerabilities. Conversely, if the programmer forgets to delete the root (or an exception occurs before deletion), the entire tree leaks because the parent holds strong pointers to children, and children hold pointers back to the parent - nothing is eligible for deletion even though it's unreachable from the rest of the program. This pattern is common in tree/graph data structures and is why modern C++ uses std::shared_ptr and std::weak_ptr - shared_ptr for parent-to-child (strong ownership), weak_ptr for child-to-parent (non-owning reference).

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 is this vulnerable: The function allocates a buffer with new[] but only deletes it at the end of the function. If any exception is thrown or early return executed before reaching delete[], the buffer leaks. Exception paths are particularly dangerous because they bypass normal control flow - a throw unwinds the stack immediately without executing subsequent statements. Early returns for error conditions also bypass cleanup. With multiple error checks, there are numerous code paths that leak memory. This pattern requires perfect discipline to ensure every exit path has matching cleanup, which is error-prone and hard to maintain. Even if initially correct, future modifications can introduce new error paths that forget cleanup.

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 is this vulnerable: Storing raw pointers in containers creates ambiguous ownership - the container holds pointers but doesn't manage lifetime, so it's unclear who is responsible for calling delete. When clear() is called or the vector is destroyed, it only destroys the pointers (trivial type), not the pointed-to objects, leaking all allocated Resources. Forgetting to manually delete before clearing is a common mistake, especially in destructors or error handlers. If an exception occurs before the manual cleanup loop, all objects leak. This pattern requires meticulous tracking of which pointers have been deleted and coordination between all code that uses the container.

Secure Patterns

RAII with Smart Pointers

#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 (Resource Acquisition Is Initialization) ties resource lifetime to object lifetime - resources are acquired in constructors and released in destructors. Smart pointers (std::unique_ptr, std::shared_ptr) automatically call delete in their destructors, ensuring memory is freed when the pointer goes out of scope or is reassigned. This works even during exception unwinding - C++ guarantees destructors are called for all fully-constructed objects during stack unwinding. std::unique_ptr enforces single ownership (move semantics prevent accidental copies), while std::shared_ptr uses reference counting for shared ownership. By eliminating manual delete calls, RAII prevents leaks from forgotten cleanup, early returns, and exception paths. The pattern extends beyond memory to any resource (files, sockets, locks) by wrapping them in classes with proper destructors.

Breaking Circular References with weak_ptr

#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 breaks circular references by providing a non-owning reference that doesn't prevent object destruction. In a tree structure, parents hold strong ownership (shared_ptr) of children, while children hold weak references (weak_ptr) to parents. When the root goes out of scope, it's destroyed because only strong references count toward keeping objects alive. As the root is destroyed, it releases its strong references to children, causing their reference counts to drop to zero, triggering their destruction. The weak_ptr in children doesn't prevent parent destruction - it simply becomes invalid (detectable via expired() or lock() returning null). This pattern eliminates the ambiguous ownership and manual cleanup complexity of raw pointers while maintaining bidirectional navigation.

Using Standard Containers Instead of Raw Arrays

#include <vector>
#include <string>
#include <array>

// AVOID: Manual memory management
char* buffer = new char[1024];
// ... use buffer ...
delete[] buffer;  // Easy to forget, especially in error paths

// BETTER: std::vector for dynamic arrays
std::vector<char> buffer(1024);
// Automatically freed when vector destroyed

// BETTER: std::string for text
std::string text;
text.resize(1024);
// Automatically freed when string destroyed

// BETTER: std::array for fixed-size arrays
std::array<char, 1024> buffer;
// Stack-allocated, no dynamic memory needed

void processData(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 (std::vector, std::string, std::array) manage their own memory automatically using RAII. When a vector goes out of scope or is destroyed, its destructor frees all allocated memory, guaranteeing cleanup even during exceptions or early returns. Unlike raw new[]/delete[], there's no way to forget cleanup - the language guarantees it. Containers also handle resizing, copying, and moving correctly, preventing leaks from manual reallocation. std::vector provides dynamic sizing with automatic cleanup, std::string is optimized for text, and std::array provides fixed-size arrays without heap allocation. By using containers, you eliminate an entire class of memory management bugs.

Custom Deleters for Non-Standard Resources

#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: Custom deleters extend RAII to resources not managed by new/delete, including C-style FILE handles, malloc'd memory, OS handles, and library-specific resources. The deleter functor or lambda is called automatically when the smart pointer is destroyed, ensuring proper cleanup. This brings exception safety and automatic resource management to legacy C APIs and third-party libraries. The deleter is stored as part of the unique_ptr's type (zero overhead for stateless functors like these), and is invoked exactly once when the resource is released. By wrapping C resources in smart pointers with custom deleters, you get C++ safety guarantees while interfacing with C code.

Implementing Rule of Five

class ResourceManager {
private:
    int* data;
    size_t size;

public:
    // Constructor
    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 states that if a class manages resources (and thus needs a destructor), it likely needs custom copy/move operations to prevent double-free and memory leaks. The destructor ensures cleanup, the copy constructor/assignment create deep copies (avoiding double-free when both copies are destroyed), and move constructor/assignment transfer ownership (nullifying the source to prevent double-free). Without proper copy operations, the default shallow copy would make two objects share the same pointer, causing double-free when both destructors run. Without proper move operations, moves would invoke copies (expensive) or cause undefined behavior. The copy-and-swap idiom provides exception safety - if memory allocation fails, the original object remains unchanged. This pattern is essential for any class managing resources, though using std::unique_ptr or std::vector as members (and declaring copy operations as deleted or defaulted) is often simpler.

Security Checklist

  • Use smart pointers (std::unique_ptr, std::shared_ptr) instead of raw new/delete
  • Prefer std::make_unique/std::make_shared over raw new for exception safety
  • Use standard containers (std::vector, std::string) instead of manual arrays
  • Break circular references with std::weak_ptr in tree/graph data structures
  • Implement Rule of Five for classes managing resources (or use smart pointers as members)
  • Avoid raw pointers in containers - use std::vector<std::unique_ptr<T>> instead
  • Use custom deleters for non-standard resources (FILE*, malloc, library handles)
  • Enable AddressSanitizer during development: -fsanitize=address
  • Run Valgrind to detect leaks: valgrind --leak-check=full ./program
  • Test exception paths explicitly - ensure RAII works during stack unwinding
  • Use static analysis tools like Clang-Tidy, cppcheck to detect potential leaks
  • Follow RAII everywhere - acquire resources in constructors, release in destructors
  • Avoid manual resource management - let C++ language features handle it
  • Use std::optional instead of nullable pointers where appropriate

Additional Resources