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

AuswirkungDetails
VerfügbarkeitBereich: Verfügbarkeit

DoS: Ressourcenverbrauch - Speicher- und Ressourcenlecks durch unvollständige Zerstörung können Systemressourcen erschöpfen.
AndereBereich: Ändere

Reduzierte Zuverlässigkeit - Undefiniertes Verhalten durch unsachgemäße Zerstörung verursacht unvorhersehbare Abstürze.
VertraulichkeitBereich: 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

  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.