Klasse mit virtueller Methode ohne virtuellen Destruktor

Beschreibung

Klasse mit virtueller Methode ohne virtuellen Destruktor tritt auf, wenn eine Klasse, die eine oder mehrere virtuelle Methoden enthält, keinen zugehörigen virtuellen Destruktor hat. In C++, wenn eine Klasse virtuelle Methoden hat, wird sie wahrscheinlich polymorph verwendet (durch Basisklassenzeiger). Wenn solche Objekte durch einen Basiszeiger ohne virtuellen Destruktor gelöscht werden, wird nur der Basisdestruktor aufgerufen, was zu Ressourcenlecks und undefiniertem Verhalten führt. Dies ist eine bekannte C++-Falle, die das Prinzip verletzt, dass Klassen mit virtuellen Methoden auch virtuelle Destruktoren haben sollten.

Risiko

Fehlende virtuelle Destruktoren in Klassen mit virtuellen Methoden haben direkte Sicherheitsimplikationen. Ressourcenlecks durch unvollständige Zerstörung können Denial-of-Service verursachen. Speicherkorruption durch unsachgemäße Zerstörung kann ausgenutzt werden. Das undefinierte Verhalten durch teilweise Zerstörung ist unvorhersehbar und potenziell ausnutzbar. Sicherheitskritischer Bereinigungscode in abgeleiteten Klassen wird möglicherweise nicht ausgeführt, wodurch sensible Daten im Speicher oder Ressourcen entsperrt bleiben. Die Zuverlässigkeitsprobleme akkumulieren sich über die Zeit und verursachen schließlich Systemausfälle.

Lösung

Wenn eine Klasse irgendeine virtuelle Methode hat, machen Sie immer ihren Destruktor virtuell. Wenden Sie die C++ Core Guidelines an: "Wenn eine Klasse irgendeine virtuelle Funktion hat, sollte sie einen virtuellen Destruktor haben." Verwenden Sie statische Analysetools, die dieses Muster erkennen. Erwägen Sie die Verwendung des override-Schlüsselworts in C++11+, um Absichten klarzumachen. In Basisklassen, die zur Vererbung vorgesehen sind, markieren Sie den Destruktor als virtuell, auch wenn derzeit keine virtuellen Methoden existieren. Verwenden Sie Smart Pointer, die helfen, das Problem zu mindern (aber nicht vollständig zu lösen). Überprüfen Sie Klassenhierarchien auf fehlende virtuelle Destruktoren.

Häufige Auswirkungen

AuswirkungDetails
VerfügbarkeitBereich: Verfügbarkeit

DoS: Ressourcenverbrauch - Speicher- und Ressourcenlecks durch unvollständige Zerstörung.
AndereBereich: Ändere

Reduzierte Zuverlässigkeit - Undefiniertes Verhalten durch unsachgemäße Zerstörung verursacht unvorhersehbares Verhalten.
IntegritätBereich: Integrität

Unerwarteter Zustand - Bereinigung der abgeleiteten Klasse wird nicht ausgeführt, hinterlässt inkonsistenten Zustand.

Beispielcode

Anfälliger Code

// Anfällig: Virtuelle Methode aber nicht-virtueller Destruktor
class VulnerableShape {
protected:
    double x, y;
    char* name;

public:
    VulnerableShape(const char* n) {
        name = new char[strlen(n) + 1];
        strcpy(name, n);
    }

    // Nicht-virtueller Destruktor (FEHLER!)
    ~VulnerableShape() {
        delete[] name;
        std::cout << "Shape zerstört" << std::endl;
    }

    // Virtuelle Methode - zeigt polymorphe Verwendung an
    virtual double area() const = 0;

    virtual void draw() const {
        std::cout << "Zeichne Form: " << name << std::endl;
    }
};

class VulnerableCircle : public VulnerableShape {
private:
    double radius;
    double* computedValues;  // Zusätzliche Ressource

public:
    VulnerableCircle(const char* n, double r)
        : VulnerableShape(n), radius(r) {
        computedValues = new double[100];
        precomputeValues();
    }

    ~VulnerableCircle() {
        // Dieser Destruktor wird NIE aufgerufen beim Löschen über Basiszeiger!
        delete[] computedValues;
        std::cout << "Kreis zerstört" << std::endl;
    }

    double area() const override {
        return 3.14159 * radius * radius;
    }
};

void vulnerableUsage() {
    // Kreis durch Basiszeiger erstellen
    VulnerableShape* shape = new VulnerableCircle("MeinKreis", 5.0);

    shape->draw();
    std::cout << "Fläche: " << shape->area() << std::endl;

    // Anfällig: Nur VulnerableShape-Destruktor wird aufgerufen!
    delete shape;
    // Ausgabe: "Shape zerstört" (nur!)
    // Speicherleck: computedValues nie freigegeben!
}
// Anfällig: Interface mit virtuellen Methoden aber ohne virtuellen Destruktor
class VulnerableEventHandler {
public:
    // Nicht-virtueller Destruktor im Interface!
    ~VulnerableEventHandler() {}

    virtual void onEvent(const Event& event) = 0;
    virtual void onError(const std::string& error) = 0;
};

class VulnerableLoggingHandler : public VulnerableEventHandler {
private:
    std::ofstream logFile;
    std::mutex logMutex;
    std::vector<std::string> buffer;

public:
    VulnerableLoggingHandler(const std::string& filename) {
        logFile.open(filename, std::ios::app);
    }

    ~VulnerableLoggingHandler() {
        // Wird nie über Basiszeiger aufgerufen!
        flush();
        logFile.close();
    }

    void onEvent(const Event& event) override {
        std::lock_guard<std::mutex> lock(logMutex);
        buffer.push_back(event.toString());
    }

    void onError(const std::string& error) override {
        std::lock_guard<std::mutex> lock(logMutex);
        buffer.push_back("FEHLER: " + error);
        flush();  // Bei Fehlern flushen
    }

private:
    void flush() {
        for (const auto& entry : buffer) {
            logFile << entry << std::endl;
        }
        buffer.clear();
    }
};

void vulnerableHandlerUsage() {
    std::vector<VulnerableEventHandler*> handlers;

    handlers.push_back(new VulnerableLoggingHandler("app.log"));

    // Events verarbeiten...
    Event event;
    for (auto& handler : handlers) {
        handler->onEvent(event);
    }

    // Bereinigung - Destruktoren werden nicht ordnungsgemäß aufgerufen!
    for (auto& handler : handlers) {
        delete handler;
        // Log-Datei nie geflusht, nie geschlossen!
        // Gepufferte Log-Einträge verloren!
    }
}

Korrigierter Code

// Korrigiert: Virtueller Destruktor mit virtuellen Methoden
class FixedShape {
protected:
    double x, y;
    std::string name;  // std::string statt rohem Pointer verwenden

public:
    FixedShape(const std::string& n) : name(n) {}

    // Korrigiert: Virtueller Destruktor
    virtual ~FixedShape() {
        std::cout << "Shape zerstört: " << name << std::endl;
    }

    // Virtuelle Methode
    virtual double area() const = 0;

    virtual void draw() const {
        std::cout << "Zeichne Form: " << name << std::endl;
    }
};

class FixedCircle : public FixedShape {
private:
    double radius;
    std::unique_ptr<double[]> computedValues;  // Smart Pointer

public:
    FixedCircle(const std::string& n, double r)
        : FixedShape(n), radius(r),
          computedValues(std::make_unique<double[]>(100)) {
        precomputeValues();
    }

    // Override Destruktor - wird ordnungsgemäß aufgerufen
    ~FixedCircle() override {
        std::cout << "Kreis zerstört: " << getName() << std::endl;
        // computedValues automatisch durch unique_ptr bereinigt
    }

    double area() const override {
        return 3.14159 * radius * radius;
    }

private:
    void precomputeValues() {
        // Werte vorberechnen...
    }
};

void fixedUsage() {
    // Korrigiert: Ordnungsgemäße Zerstörung durch Basiszeiger
    FixedShape* shape = new FixedCircle("MeinKreis", 5.0);

    shape->draw();
    std::cout << "Fläche: " << shape->area() << std::endl;

    // Korrigiert: Beide Destruktoren in korrekter Reihenfolge aufgerufen
    delete shape;
    // Ausgabe: "Kreis zerstört: MeinKreis"
    //         "Shape zerstört: MeinKreis"
}

// Noch besser: Smart Pointer verwenden
void modernUsage() {
    std::unique_ptr<FixedShape> shape =
        std::make_unique<FixedCircle>("MeinKreis", 5.0);

    shape->draw();
    // Automatische Bereinigung wenn außerhalb des Gültigkeitsbereichs
}
// Korrigiert: Interface mit rein virtuellem Destruktor
class FixedEventHandler {
public:
    // Korrigiert: Rein virtueller Destruktor mit Definition
    virtual ~FixedEventHandler() = 0;

    virtual void onEvent(const Event& event) = 0;
    virtual void onError(const std::string& error) = 0;
};

// Definition für rein virtuellen Destruktor muss bereitgestellt werden
FixedEventHandler::~FixedEventHandler() = default;

class FixedLoggingHandler final : public FixedEventHandler {
private:
    std::ofstream logFile;
    std::mutex logMutex;
    std::vector<std::string> buffer;

public:
    FixedLoggingHandler(const std::string& filename) {
        logFile.open(filename, std::ios::app);
        if (!logFile) {
            throw std::runtime_error("Log-Datei kann nicht geöffnet werden");
        }
    }

    ~FixedLoggingHandler() override {
        // Korrigiert: Wird ordnungsgemäß aufgerufen
        flush();
        logFile.close();
    }

    void onEvent(const Event& event) override {
        std::lock_guard<std::mutex> lock(logMutex);
        buffer.push_back(event.toString());

        if (buffer.size() >= 100) {
            flush();
        }
    }

    void onError(const std::string& error) override {
        std::lock_guard<std::mutex> lock(logMutex);
        buffer.push_back("FEHLER: " + error);
        flush();
    }

private:
    void flush() {
        for (const auto& entry : buffer) {
            logFile << entry << std::endl;
        }
        buffer.clear();
        logFile.flush();
    }
};

void fixedHandlerUsage() {
    // Korrigiert: Smart Pointer für automatische Bereinigung verwenden
    std::vector<std::unique_ptr<FixedEventHandler>> handlers;

    handlers.push_back(std::make_unique<FixedLoggingHandler>("app.log"));

    // Events verarbeiten...
    Event event;
    for (auto& handler : handlers) {
        handler->onEvent(event);
    }

    // Korrigiert: Automatische Bereinigung - alle Destruktoren ordnungsgemäß aufgerufen
    handlers.clear();
    // Oder einfach außerhalb des Gültigkeitsbereichs gehen lassen
}
// C++ Core Guidelines konforme abstrakte Basisklasse
class AbstractBase {
public:
    // Regel: Eine polymorphe Klasse sollte Kopieren unterdrücken
    AbstractBase() = default;
    AbstractBase(const AbstractBase&) = delete;
    AbstractBase& operator=(const AbstractBase&) = delete;

    // Regel: Virtueller Destruktor für polymorphe Klasse
    virtual ~AbstractBase() = default;

    // Rein virtuelle Methode
    virtual void doSomething() = 0;
};

// Alternative: Geschützter nicht-virtueller Destruktor für nicht-polymorphes Löschen
class NonPolymorphicBase {
public:
    virtual void doSomething() = 0;

protected:
    // Nicht-virtuell aber geschützt - kann nicht durch Basiszeiger löschen
    ~NonPolymorphicBase() = default;
};

// Dies verhindert gefährliche Verwendung:
// NonPolymorphicBase* ptr = new Derived();
// delete ptr;  // Kompilierfehler! Destruktor ist geschützt

CVE-Beispiele

Speicherlecks und Ressourcenlecks durch fehlende virtuelle Destruktoren haben zu Denial-of-Service-Schwachstellen beigetragen, obwohl spezifische CVEs typischerweise die Auswirkung beschreiben anstatt diese spezifische Ursache.


Verwandte CWEs

  • CWE-1076: Insufficient Adherence to Expected Conventions (Eltern)
  • CWE-1079: Parent Class without Virtual Destructor Method (eng verwandt)
  • CWE-401: Missing Release of Memory after Effective Lifetime (kann resultieren aus)

Referenzen

  1. MITRE Corporation. "CWE-1087: Class with Virtual Method without a Virtual Destructor." https://cwe.mitre.org/data/definitions/1087.html

  2. C++ Core Guidelines. C.35: A base class destructor should be either public and virtual, or protected and non-virtual.

  3. Meyers, Scott. "Effective C++." Item 7: Declare destructors virtual in polymorphic base classes.