Parent Class with a Virtual Destructor and a Child Class without a Virtual Destructor
Description
Parent Class with a Virtual Destructor and a Child Class without a Virtual Destructor occurs when a parent class properly declares a virtual destructor, but a derived (child) class does not override it with its own virtual destructor. In C++ and similar languages, when objects are deleted through a pointer to the base class, only the base class destructor is called if the destructors aren't properly virtual throughout the hierarchy. While the parent's virtual destructor helps, the child class may have resources that require cleanup, and inconsistent destructor declarations can lead to confusion and maintenance issues.
Risk
This issue can lead to memory leaks (CWE-401) and resource leaks when derived class destructors are not properly invoked. While this is primarily a reliability concern, it becomes a security issue when: (1) attackers can trigger the problematic code path repeatedly to exhaust system resources, (2) sensitive data in child class members is not properly cleared, or (3) the resource leak causes denial of service. The inconsistency in destructor declarations also indicates poor adherence to C++ best practices, suggesting other potential issues in the codebase.
Solution
Ensure all classes in an inheritance hierarchy that may be deleted polymorphically have virtual destructors. In C++11 and later, use the override keyword to make the relationship explicit. Consider using the final keyword for classes that should not be further derived. Follow the Rule of Five/Zero in C++ for proper resource management. Use smart pointers (unique_ptr, shared_ptr) instead of raw pointers to ensure proper cleanup. Employ static analysis tools to detect destructor inconsistencies.
Common Consequences
| Impact | Details |
|---|---|
| Availability | Scope: Availability DoS: Resource Consumption - Memory leaks from improper destruction can eventually exhaust system resources. |
| Confidentiality | Scope: Confidentiality Memory Exposure - Sensitive data in child class members may not be properly cleared if destructor isn't called. |
| Integrity | Scope: Integrity Quality Degradation - Resource leaks reduce reliability and can cause unpredictable behavior. |
Example Code
Vulnerable Code
// Vulnerable: Child class lacks virtual destructor
#include <iostream>
#include <cstring>
class Base {
public:
virtual ~Base() {
std::cout << "Base destructor called" << std::endl;
}
virtual void doSomething() = 0;
};
class VulnerableChild : public Base {
private:
char* sensitiveData;
size_t dataSize;
int* largeArray;
public:
VulnerableChild(const char* data) {
dataSize = strlen(data) + 1;
sensitiveData = new char[dataSize];
strcpy(sensitiveData, data);
largeArray = new int[10000]; // Additional resource
}
// Vulnerable: No virtual destructor declared
// The destructor should be virtual to ensure proper cleanup
~VulnerableChild() {
// This may not be called when deleting through Base pointer!
std::cout << "VulnerableChild destructor called" << std::endl;
// Memory clearing won't happen
memset(sensitiveData, 0, dataSize);
delete[] sensitiveData;
delete[] largeArray;
}
void doSomething() override {
std::cout << "Processing: " << sensitiveData << std::endl;
}
};
// Usage that triggers the problem
void vulnerableUsage() {
Base* obj = new VulnerableChild("secret password");
obj->doSomething();
delete obj; // Only Base destructor called!
// VulnerableChild destructor NOT called
// Memory leak: sensitiveData and largeArray not freed
// Security issue: sensitive data remains in memory
}
// Vulnerable: Multiple inheritance with inconsistent destructors
class Interface1 {
public:
virtual ~Interface1() {}
virtual void method1() = 0;
};
class Interface2 {
public:
virtual ~Interface2() {}
virtual void method2() = 0;
};
class VulnerableMultiChild : public Interface1, public Interface2 {
private:
void* resource;
public:
VulnerableMultiChild() {
resource = malloc(1024);
}
// Vulnerable: Destructor not explicitly virtual
// Though it's implicitly virtual, the inconsistency is problematic
~VulnerableMultiChild() {
free(resource);
}
void method1() override {}
void method2() override {}
};
// Vulnerable: Template inheritance without proper destructor handling
template<typename T>
class BaseContainer {
protected:
T* data;
size_t size;
public:
BaseContainer(size_t s) : size(s) {
data = new T[size];
}
virtual ~BaseContainer() {
delete[] data;
}
virtual T& operator[](size_t index) {
return data[index];
}
};
// Vulnerable child template class
template<typename T>
class VulnerableSecureContainer : public BaseContainer<T> {
private:
char* encryptionKey;
public:
VulnerableSecureContainer(size_t s, const char* key)
: BaseContainer<T>(s) {
encryptionKey = strdup(key);
}
// Missing virtual destructor - key may leak!
~VulnerableSecureContainer() {
// Secure clearing
memset(encryptionKey, 0, strlen(encryptionKey));
free(encryptionKey);
}
};
Fixed Code
// Fixed: Proper virtual destructor in child class
#include <iostream>
#include <cstring>
#include <memory>
class Base {
public:
virtual ~Base() {
std::cout << "Base destructor called" << std::endl;
}
virtual void doSomething() = 0;
};
class FixedChild : public Base {
private:
char* sensitiveData;
size_t dataSize;
int* largeArray;
public:
FixedChild(const char* data) {
dataSize = strlen(data) + 1;
sensitiveData = new char[dataSize];
strcpy(sensitiveData, data);
largeArray = new int[10000];
}
// Fixed: Virtual destructor with override keyword
~FixedChild() override {
std::cout << "FixedChild destructor called" << std::endl;
// Secure memory clearing
memset(sensitiveData, 0, dataSize);
delete[] sensitiveData;
delete[] largeArray;
}
void doSomething() override {
std::cout << "Processing: " << sensitiveData << std::endl;
}
};
// Better: Use smart pointers (Rule of Zero)
class BetterChild : public Base {
private:
std::unique_ptr<char[]> sensitiveData;
size_t dataSize;
std::unique_ptr<int[]> largeArray;
public:
BetterChild(const char* data) {
dataSize = strlen(data) + 1;
sensitiveData = std::make_unique<char[]>(dataSize);
strcpy(sensitiveData.get(), data);
largeArray = std::make_unique<int[]>(10000);
}
// Fixed: Virtual destructor (explicitly declared for clarity)
// Resources automatically cleaned up by smart pointers
~BetterChild() override {
// Secure clearing still needed for sensitive data
if (sensitiveData) {
memset(sensitiveData.get(), 0, dataSize);
}
}
void doSomething() override {
std::cout << "Processing: " << sensitiveData.get() << std::endl;
}
};
// Correct usage
void fixedUsage() {
// Option 1: Raw pointer with explicit delete
Base* obj1 = new FixedChild("secret password");
obj1->doSomething();
delete obj1; // Both destructors called correctly
// Option 2: Smart pointer (preferred)
std::unique_ptr<Base> obj2 = std::make_unique<BetterChild>("secret");
obj2->doSomething();
// Automatically destroyed when out of scope
}
// Fixed: Multiple inheritance with consistent virtual destructors
class Interface1 {
public:
virtual ~Interface1() = default;
virtual void method1() = 0;
};
class Interface2 {
public:
virtual ~Interface2() = default;
virtual void method2() = 0;
};
class FixedMultiChild : public Interface1, public Interface2 {
private:
std::unique_ptr<char[]> resource;
public:
FixedMultiChild() : resource(std::make_unique<char[]>(1024)) {}
// Fixed: Explicitly virtual destructor with override
~FixedMultiChild() override = default;
void method1() override {}
void method2() override {}
};
// Fixed: Template inheritance with proper destructor
template<typename T>
class BaseContainer {
protected:
std::unique_ptr<T[]> data;
size_t size;
public:
explicit BaseContainer(size_t s) : size(s), data(std::make_unique<T[]>(s)) {}
virtual ~BaseContainer() = default;
virtual T& operator[](size_t index) {
return data[index];
}
};
template<typename T>
class FixedSecureContainer : public BaseContainer<T> {
private:
std::unique_ptr<char[]> encryptionKey;
size_t keyLength;
public:
FixedSecureContainer(size_t s, const char* key)
: BaseContainer<T>(s),
keyLength(strlen(key) + 1),
encryptionKey(std::make_unique<char[]>(keyLength)) {
strcpy(encryptionKey.get(), key);
}
// Fixed: Virtual destructor
~FixedSecureContainer() override {
// Secure key clearing before destruction
if (encryptionKey) {
memset(encryptionKey.get(), 0, keyLength);
}
}
};
// Final class - cannot be inherited
class FinalSecureContainer final : public BaseContainer<int> {
private:
std::string metadata;
public:
explicit FinalSecureContainer(size_t s) : BaseContainer<int>(s) {}
// Destructor doesn't need to be virtual for final class
// but being explicit is good practice
~FinalSecureContainer() override = default;
};
// Fixed: RAII wrapper for secure resource management
class SecureBuffer {
private:
std::unique_ptr<char[]> buffer;
size_t size;
public:
explicit SecureBuffer(size_t s) : size(s), buffer(std::make_unique<char[]>(s)) {
// Zero-initialize
memset(buffer.get(), 0, size);
}
// Virtual destructor for potential inheritance
virtual ~SecureBuffer() {
// Secure clear before destruction
if (buffer) {
volatile char* p = buffer.get();
for (size_t i = 0; i < size; ++i) {
p[i] = 0;
}
}
}
// Delete copy operations
SecureBuffer(const SecureBuffer&) = delete;
SecureBuffer& operator=(const SecureBuffer&) = delete;
// Allow move operations
SecureBuffer(SecureBuffer&&) = default;
SecureBuffer& operator=(SecureBuffer&&) = default;
char* data() { return buffer.get(); }
size_t length() const { return size; }
};
class DerivedSecureBuffer : public SecureBuffer {
private:
std::string label;
public:
DerivedSecureBuffer(size_t s, const std::string& l)
: SecureBuffer(s), label(l) {}
// Fixed: Virtual destructor with override
~DerivedSecureBuffer() override {
// Label cleared by std::string destructor
// Base class SecureBuffer destructor handles buffer
}
};
CVE Examples
This CWE is primarily a code quality and reliability issue. While no specific CVEs are directly attributed to this pattern, improper destructor handling has contributed to memory leak vulnerabilities that were exploited for denial-of-service attacks.
Related CWEs
- CWE-1076: Insufficient Adherence to Expected Conventions (parent)
- CWE-401: Missing Release of Memory after Effective Lifetime (can lead to)
- CWE-1006: Bad Coding Practices (category member)
References
- MITRE Corporation. "CWE-1045: Parent Class with a Virtual Destructor and a Child Class without a Virtual Destructor." https://cwe.mitre.org/data/definitions/1045.html
- Stroustrup, Bjarne. "The C++ Programming Language."
- C++ Core Guidelines. "C.35: A base class destructor should be either public and virtual, or protected and non-virtual."