Elternklasse ohne virtuelle Destruktor-Methode
Beschreibung
Elternklasse ohne virtuelle Destruktor-Methode tritt auf, wenn eine Elternklasse, die eine oder mehrere Kindklassen enthält, keine virtuelle Destruktor-Methode hat. In C++, wenn ein Objekt einer abgeleiteten Klasse durch einen Pointer auf die Basisklasse gelöscht wird und der Basisklassen-Destruktor nicht virtuell ist, wird nur der Basisklassen-Destruktor aufgerufen. Dies führt zu unvollständiger Zerstörung, bei der Ressourcen der abgeleiteten Klasse nicht ordnungsgemäß bereinigt werden, was zu Speicherlecks, Ressourcenlecks und potenziell undefiniertem Verhalten führt.
Risiko
Fehlende virtuelle Destruktoren haben direkte Sicherheitsimplikationen. Ressourcenlecks durch unvollständige Zerstörung können zu Denial-of-Service führen. Speicherlecks akkumulieren sich über die Zeit und erschöpfen schließlich den Systemspeicher. Datei-Handles, Netzwerkverbindungen oder Sperren werden möglicherweise nicht freigegeben. Das undefinierte Verhalten durch unsachgemäße Zerstörung kann ausgenutzt werden. Sicherheitsrelevanter Bereinigungscode in abgeleiteten Klassen wird möglicherweise nicht ausgeführt, wodurch sensible Daten im Speicher verbleiben. Die Zuverlässigkeitsprobleme können von Angreifern ausgelöst werden, um Systeminstabilität zu verursachen.
Lösung
Deklarieren Sie Destruktoren in Basisklassen immer als virtuell, wenn sie zur Vererbung vorgesehen sind. Wenn eine Klasse irgendeine virtuelle Methode hat, sollte ihr Destruktor auch virtuell sein. Erwägen Sie die Verwendung der C++11-Schlüsselwörter override und final für Klarheit. Verwenden Sie statische Analysetools, die fehlende virtuelle Destruktoren erkennen. In modernem C++ (C++11+) verwenden Sie Smart Pointer, die bei der Verwaltung von Objektlebensdauern helfen. Wenden Sie die Fünferregel an: Wenn Sie eines von Destruktor, Kopierkonstruktor, Kopierzuweisung, Move-Konstruktor oder Move-Zuweisung definieren, definieren Sie alle fünf. Verwenden Sie abstrakte Basisklassen mit rein virtuellen Destruktoren wenn angemessen.
Häufige Auswirkungen
| Auswirkung | Details |
|---|---|
| Verfügbarkeit | Bereich: Verfügbarkeit DoS: Ressourcenverbrauch - Speicher- und Ressourcenlecks durch unvollständige Zerstörung können Systemressourcen erschöpfen. |
| Andere | Bereich: Ändere Reduzierte Zuverlässigkeit - Undefiniertes Verhalten durch unsachgemäße Zerstörung verursacht unvorhersehbare Abstürze. |
| Vertraulichkeit | Bereich: Vertraulichkeit Speicher lesen - Sensible Daten in abgeleiteter Klasse werden möglicherweise nicht ordnungsgemäß gelöscht. |
Beispielcode
Anfälliger Code
// Anfällig: Basisklasse ohne virtuellen Destruktor
class VulnerableBase {
protected:
char* buffer;
size_t size;
public:
VulnerableBase(size_t s) : size(s) {
buffer = new char[size];
}
// Anfällig: Nicht-virtueller Destruktor!
~VulnerableBase() {
delete[] buffer;
std::cout << "Basis-Destruktor aufgerufen" << std::endl;
}
virtual void process() {
// Virtuelle Methode aber nicht-virtueller Destruktor
}
};
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, "GEHEIMER_SCHLÜSSEL_12345");
largeArray = new int[100000]; // Größe Allokation
logFile.open("app.log");
}
~VulnerableDerived() {
// Dieser Destruktor wird NIE aufgerufen beim Löschen über Basiszeiger!
std::memset(sensitiveData, 0, 1024); // Sicherheit: sensible Daten löschen
delete[] sensitiveData;
delete[] largeArray;
logFile.close();
std::cout << "Abgeleiteter Destruktor aufgerufen" << std::endl;
}
void process() override {
// Verarbeitungsimplementierung
}
};
void vulnerableUsage() {
// Verwendung von Basiszeiger auf abgeleitetes Objekt
VulnerableBase* obj = new VulnerableDerived(100);
obj->process();
// Anfällig: Nur VulnerableBase-Destruktor wird aufgerufen!
delete obj;
// Ausgabe: "Basis-Destruktor aufgerufen" (nur!)
// Lecks: sensitiveData, largeArray
// Nicht geschlossen: logFile
// Sicherheit: sensible Daten verbleiben im Speicher!
}
// Anfällig: Abstrakte Basis ohne virtuellen Destruktor
class VulnerableInterface {
public:
// Anfällig: Nicht-virtueller Destruktor 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() {
// Wird nie aufgerufen beim Löschen über Interface-Pointer!
clearLogs();
}
void doSomething() override {}
void doSomethingElse() override {}
private:
void clearLogs() {
logs.clear();
logs.shrink_to_fit();
}
};
void vulnerableFactoryUsage() {
std::vector<VulnerableInterface*> objects;
// Viele Objekte erstellen
for (int i = 0; i < 1000; i++) {
objects.push_back(new VulnerableImplementation());
}
// Bereinigung - nur Basis-Destruktoren werden aufgerufen!
for (auto obj : objects) {
delete obj; // Speicherleck für jedes Objekt!
}
// 4MB+ Leck (4096 * 1000)
}
// Anfällig: Template-Basisklasse
template<typename T>
class VulnerableContainer {
protected:
T* elements;
size_t count;
public:
VulnerableContainer(size_t n) : count(n) {
elements = new T[count];
}
// Anfällig: Nicht-virtueller Destruktor 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() {
// Wird nie über Basiszeiger aufgerufen!
std::memset(encryptedCopy, 0, sizeof(T) * this->count);
delete[] accessFlags;
delete[] encryptedCopy;
}
};
Korrigierter Code
// Korrigiert: Basisklasse mit virtuellem Destruktor
class FixedBase {
protected:
char* buffer;
size_t size;
public:
FixedBase(size_t s) : size(s) {
buffer = new char[size];
}
// Korrigiert: Virtueller Destruktor
virtual ~FixedBase() {
delete[] buffer;
std::cout << "Basis-Destruktor aufgerufen" << std::endl;
}
virtual void process() {
// Virtuelle Methode
}
};
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(), "GEHEIMER_SCHLÜSSEL_12345");
logFile.open("app.log");
}
// Korrigiert: Destruktor wird ordnungsgemäß aufgerufen
~FixedDerived() override { // override für Klarheit verwenden
// Sicherheit: sensible Daten vor Zerstörung löschen
if (sensitiveData) {
std::memset(sensitiveData.get(), 0, 1024);
}
// Smart Pointer räumen automatisch auf
if (logFile.is_open()) {
logFile.close();
}
std::cout << "Abgeleiteter Destruktor aufgerufen" << std::endl;
}
void process() override {
// Verarbeitungsimplementierung
}
};
void fixedUsage() {
// Verwendung von Basiszeiger auf abgeleitetes Objekt
FixedBase* obj = new FixedDerived(100);
obj->process();
// Korrigiert: Beide Destruktoren werden in korrekter Reihenfolge aufgerufen!
delete obj;
// Ausgabe: "Abgeleiteter Destruktor aufgerufen"
// "Basis-Destruktor aufgerufen"
}
// Noch besser: Smart Pointer verwenden
void modernUsage() {
std::unique_ptr<FixedBase> obj = std::make_unique<FixedDerived>(100);
obj->process();
// Automatische Bereinigung wenn obj den Gültigkeitsbereich verlässt
}
// Korrigiert: Abstraktes Interface mit rein virtuellem Destruktor
class FixedInterface {
public:
// Korrigiert: Rein virtueller Destruktor mit Definition
virtual ~FixedInterface() = 0;
virtual void doSomething() = 0;
virtual void doSomethingElse() = 0;
};
// Definition für rein virtuellen Destruktor muss bereitgestellt werden
FixedInterface::~FixedInterface() {
// Basisbereinigung falls benötigt
}
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 {
// Wird ordnungsgemäß aufgerufen
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;
// Viele Objekte mit Smart Pointern erstellen
for (int i = 0; i < 1000; i++) {
objects.push_back(std::make_unique<FixedImplementation>());
}
// Bereinigung automatisch - alle Destruktoren ordnungsgemäß aufgerufen
objects.clear();
// Keine Speicherlecks!
}
// Korrigiert: Template-Basisklasse mit virtuellem Destruktor
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) {
}
// Korrigiert: Virtueller Destruktor
virtual ~FixedContainer() = default;
virtual T& get(size_t index) {
if (index >= count) {
throw std::out_of_range("Index außerhalb der Grenzen");
}
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 {
// Sichere Bereinigung
if (encryptedCopy) {
std::memset(encryptedCopy.get(), 0, sizeof(T) * this->count);
}
}
T& get(size_t index) override {
if (!accessFlags[index]) {
throw std::runtime_error("Zugriff verweigert");
}
return FixedContainer<T>::get(index);
}
void grantAccess(size_t index) {
if (index < this->count) {
accessFlags[index] = true;
}
}
};
// Verwendung mit ordnungsgemäßem Polymorphismus
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);
}
// Ordnungsgemäße Bereinigung via virtuellen Destruktor
}
CVE-Beispiele
Speicherlecks und Ressourcenlecks durch fehlende virtuelle Destruktoren haben zu Denial-of-Service-Schwachstellen beigetragen, obwohl spezifische CVEs typischerweise die Auswirkung (Speichererschöpfung, Ressourcenleck) beschreiben anstatt diese spezifische Ursache.
Verwandte CWEs
- CWE-1076: Insufficient Adherence to Expected Conventions (Eltern)
- CWE-401: Missing Release of Memory after Effective Lifetime (verwandt)
- CWE-772: Missing Release of Resource after Effective Lifetime (verwandt)
Referenzen
-
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.