Parent Class without Virtual Destructor Method

Description

Parent Class without Virtual Destructor Method occurs when a parent class that contains one or more child classes does not have a virtual destructor method. In C++, when an object of a derived class is deleted through a pointer to the base class, and the base class destructor is not virtual, only the base class destructor is called. This leads to incomplete destruction where derived class resources are not properly cleaned up, causing memory leaks, resource leaks, and potentially undefined behavior.

Risk

Missing virtual destructors have direct security implications. Resource leaks from incomplete destruction can lead to denial of service. Memory leaks accumulate over time, eventually exhausting system memory. File handles, network connections, or locks may not be released. The undefined behavior from improper destruction can be exploited. Security-sensitive cleanup code in derived classes may not execute, leaving sensitive data in memory. The reliability issues can be triggered by attackers to cause system instability.

Solution

Always declare destructors as virtual in base classes that are intended to be inherited from. If a class has any virtual method, its destructor should also be virtual. Consider using the C++11 override and final keywords for clarity. Use static analysis tools that detect missing virtual destructors. In modern C++ (C++11+), use smart pointers which help manage object lifetimes. Apply the Rule of Five: if you define any of destructor, copy constructor, copy assignment, move constructor, or move assignment, define all five. Use abstract base classes with pure virtual destructors when appropriate.

Common Consequences

ImpactDetails
AvailabilityScope: Availability

DoS: Resource Consumption - Memory and resource leaks from incomplete destruction can exhaust system resources.
OtherScope: Other

Reduce Reliability - Undefined behavior from improper destruction causes unpredictable crashes.
ConfidentialityScope: Confidentiality

Read Memory - Sensitive data in derived class may not be properly cleared.

Example Code

Vulnerable Code

// Vulnerable: Base class without virtual destructor
class VulnerableBase {
protected:
    char* buffer;
    size_t size;

public:
    VulnerableBase(size_t s) : size(s) {
        buffer = new char[size];
    }

    // Vulnerable: Non-virtual destructor!
    ~VulnerableBase() {
        delete[] buffer;
        std::cout << "Base destructor called" << std::endl;
    }

    virtual void process() {
        // Virtual method but non-virtual destructor
    }
};

class VulnerableDerived : public VulnerableBase {
private:
    char* sensitiveData;
    int* largeArray;
    std::ofstream logFile;

public:
    VulnerableDerived(size_t s) : VulnerableBase(s) {
        sensitiveData = new char[1024];
        strcpy(sensitiveData, "SECRET_KEY_12345");

        largeArray = new int[100000];  // Large allocation

        logFile.open("app.log");
    }

    ~VulnerableDerived() {
        // This destructor is NEVER called when deleting through base pointer!
        std::memset(sensitiveData, 0, 1024);  // Security: clear sensitive data
        delete[] sensitiveData;
        delete[] largeArray;
        logFile.close();
        std::cout << "Derived destructor called" << std::endl;
    }

    void process() override {
        // Process implementation
    }
};

void vulnerableUsage() {
    // Using base pointer to derived object
    VulnerableBase* obj = new VulnerableDerived(100);

    obj->process();

    // Vulnerable: Only VulnerableBase destructor called!
    delete obj;
    // Output: "Base destructor called" (only!)

    // Leaked: sensitiveData, largeArray
    // Not closed: logFile
    // Security: sensitive data remains in memory!
}
// Vulnerable: Abstract base without virtual destructor
class VulnerableInterface {
public:
    // Vulnerable: Non-virtual destructor in interface
    ~VulnerableInterface() {}

    virtual void doSomething() = 0;
    virtual void doSomethingElse() = 0;
};

class VulnerableImplementation : public VulnerableInterface {
private:
    std::unique_ptr<char[]> data;
    std::vector<std::string> logs;

public:
    VulnerableImplementation() : data(new char[4096]) {
        logs.reserve(1000);
    }

    ~VulnerableImplementation() {
        // Never called when deleted through interface pointer!
        clearLogs();
    }

    void doSomething() override {}
    void doSomethingElse() override {}

private:
    void clearLogs() {
        logs.clear();
        logs.shrink_to_fit();
    }
};

void vulnerableFactoryUsage() {
    std::vector<VulnerableInterface*> objects;

    // Create many objects
    for (int i = 0; i < 1000; i++) {
        objects.push_back(new VulnerableImplementation());
    }

    // Cleanup - only base destructors called!
    for (auto obj : objects) {
        delete obj;  // Memory leak for each object!
    }
    // 4MB+ leaked (4096 * 1000)
}
// Vulnerable: Template base class
template<typename T>
class VulnerableContainer {
protected:
    T* elements;
    size_t count;

public:
    VulnerableContainer(size_t n) : count(n) {
        elements = new T[count];
    }

    // Vulnerable: Non-virtual destructor in template
    ~VulnerableContainer() {
        delete[] elements;
    }

    virtual T& get(size_t index) {
        return elements[index];
    }
};

template<typename T>
class VulnerableSecureContainer : public VulnerableContainer<T> {
private:
    bool* accessFlags;
    T* encryptedCopy;

public:
    VulnerableSecureContainer(size_t n) : VulnerableContainer<T>(n) {
        accessFlags = new bool[n];
        encryptedCopy = new T[n];
    }

    ~VulnerableSecureContainer() {
        // Never called via base pointer!
        std::memset(encryptedCopy, 0, sizeof(T) * this->count);
        delete[] accessFlags;
        delete[] encryptedCopy;
    }
};

Fixed Code

// Fixed: Base class with virtual destructor
class FixedBase {
protected:
    char* buffer;
    size_t size;

public:
    FixedBase(size_t s) : size(s) {
        buffer = new char[size];
    }

    // Fixed: Virtual destructor
    virtual ~FixedBase() {
        delete[] buffer;
        std::cout << "Base destructor called" << std::endl;
    }

    virtual void process() {
        // Virtual method
    }
};

class FixedDerived : public FixedBase {
private:
    std::unique_ptr<char[]> sensitiveData;  // Smart pointer
    std::unique_ptr<int[]> largeArray;
    std::ofstream logFile;

public:
    FixedDerived(size_t s) : FixedBase(s),
        sensitiveData(new char[1024]),
        largeArray(new int[100000]) {

        std::strcpy(sensitiveData.get(), "SECRET_KEY_12345");
        logFile.open("app.log");
    }

    // Fixed: Destructor will be called properly
    ~FixedDerived() override {  // Use override for clarity
        // Security: clear sensitive data before destruction
        if (sensitiveData) {
            std::memset(sensitiveData.get(), 0, 1024);
        }
        // Smart pointers automatically clean up
        if (logFile.is_open()) {
            logFile.close();
        }
        std::cout << "Derived destructor called" << std::endl;
    }

    void process() override {
        // Process implementation
    }
};

void fixedUsage() {
    // Using base pointer to derived object
    FixedBase* obj = new FixedDerived(100);

    obj->process();

    // Fixed: Both destructors called in correct order!
    delete obj;
    // Output: "Derived destructor called"
    //         "Base destructor called"
}

// Even better: Use smart pointers
void modernUsage() {
    std::unique_ptr<FixedBase> obj = std::make_unique<FixedDerived>(100);
    obj->process();
    // Automatic cleanup when obj goes out of scope
}
// Fixed: Abstract interface with pure virtual destructor
class FixedInterface {
public:
    // Fixed: Pure virtual destructor with definition
    virtual ~FixedInterface() = 0;

    virtual void doSomething() = 0;
    virtual void doSomethingElse() = 0;
};

// Must provide definition for pure virtual destructor
FixedInterface::~FixedInterface() {
    // Base cleanup if needed
}

class FixedImplementation final : public FixedInterface {
private:
    std::unique_ptr<char[]> data;
    std::vector<std::string> logs;

public:
    FixedImplementation() : data(std::make_unique<char[]>(4096)) {
        logs.reserve(1000);
    }

    ~FixedImplementation() override {
        // Will be called properly
        clearLogs();
    }

    void doSomething() override {}
    void doSomethingElse() override {}

private:
    void clearLogs() {
        logs.clear();
        logs.shrink_to_fit();
    }
};

void fixedFactoryUsage() {
    std::vector<std::unique_ptr<FixedInterface>> objects;

    // Create many objects with smart pointers
    for (int i = 0; i < 1000; i++) {
        objects.push_back(std::make_unique<FixedImplementation>());
    }

    // Cleanup automatic - all destructors called properly
    objects.clear();
    // No memory leaks!
}
// Fixed: Template base class with virtual destructor
template<typename T>
class FixedContainer {
protected:
    std::unique_ptr<T[]> elements;
    size_t count;

public:
    FixedContainer(size_t n) : elements(std::make_unique<T[]>(n)), count(n) {
    }

    // Fixed: Virtual destructor
    virtual ~FixedContainer() = default;

    virtual T& get(size_t index) {
        if (index >= count) {
            throw std::out_of_range("Index out of bounds");
        }
        return elements[index];
    }

    size_t size() const { return count; }
};

template<typename T>
class FixedSecureContainer final : public FixedContainer<T> {
private:
    std::unique_ptr<bool[]> accessFlags;
    std::unique_ptr<T[]> encryptedCopy;

public:
    FixedSecureContainer(size_t n) : FixedContainer<T>(n),
        accessFlags(std::make_unique<bool[]>(n)),
        encryptedCopy(std::make_unique<T[]>(n)) {
    }

    ~FixedSecureContainer() override {
        // Secure cleanup
        if (encryptedCopy) {
            std::memset(encryptedCopy.get(), 0, sizeof(T) * this->count);
        }
    }

    T& get(size_t index) override {
        if (!accessFlags[index]) {
            throw std::runtime_error("Access denied");
        }
        return FixedContainer<T>::get(index);
    }

    void grantAccess(size_t index) {
        if (index < this->count) {
            accessFlags[index] = true;
        }
    }
};

// Usage with proper polymorphism
void safeTemplateUsage() {
    std::unique_ptr<FixedContainer<int>> container =
        std::make_unique<FixedSecureContainer<int>>(1000);

    auto* secure = dynamic_cast<FixedSecureContainer<int>*>(container.get());
    if (secure) {
        secure->grantAccess(0);
    }

    // Proper cleanup via virtual destructor
}

CVE Examples

Memory leaks and resource leaks from missing virtual destructors have contributed to denial-of-service vulnerabilities, though specific CVEs typically describe the impact (memory exhaustion, resource leak) rather than this specific cause.


  • CWE-1076: Insufficient Adherence to Expected Conventions (parent)
  • CWE-401: Missing Release of Memory after Effective Lifetime (related)
  • CWE-772: Missing Release of Resource after Effective Lifetime (related)

References

  1. MITRE Corporation. "CWE-1079: Parent Class without Virtual Destructor Method." https://cwe.mitre.org/data/definitions/1079.html
  2. Meyers, Scott. "Effective C++, Third Edition." Item 7: Declare destructors virtual in polymorphic base classes.
  3. C++ Core Guidelines. C.35: A base class destructor should be either public and virtual, or protected and non-virtual.