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
| Impact | Details |
|---|---|
| Availability | Scope: Availability DoS: Resource Consumption - Memory and resource leaks from incomplete destruction can exhaust system resources. |
| Other | Scope: Other Reduce Reliability - Undefined behavior from improper destruction causes unpredictable crashes. |
| Confidentiality | Scope: 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.
Related CWEs
- 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
- MITRE Corporation. "CWE-1079: Parent Class without Virtual Destructor Method." https://cwe.mitre.org/data/definitions/1079.html
- Meyers, Scott. "Effective C++, Third Edition." Item 7: Declare destructors virtual in polymorphic base classes.
- C++ Core Guidelines. C.35: A base class destructor should be either public and virtual, or protected and non-virtual.