Klasseninstanz mit Selbstzerstörungskontrollelement

Beschreibung

Klasseninstanz mit Selbstzerstörungskontrollelement tritt auf, wenn Code eine Klasseninstanz enthält, die eine Methode oder Funktion aufruft, um sich selbst zu löschen oder zu zerstoren. Das klassische Beispiel ist C++-Code, der "delete this" verwendet, um ein Objekt aus dem Speicher zu entfernen. Obwohl technisch gültig in C++, ist dieses Muster gefährlich, da nach der Selbstzerstörung jede Verwendung des Objekts (einschließlich impliziter Verwendungen wie Rückkehr von der Methode oder Zugriff auf Mitgliedsvariablen) zu undefiniertem Verhalten führt. Dies erzeugt Code, der schwer zu warten und anfällig für Use-After-Free-Schwachstellen ist.

Risiko

Selbstzerstorende Objekte haben direkte Sicherheitsimplikationen. Jeder Code, der nach "delete this" ausgeführt wird, greift auf freigegebenen Speicher zu (Use-After-Free). Der Aufrufer hält möglicherweise noch eine Referenz auf das gelöschte Objekt, was zu Use-After-Free führt, wenn er es verwendet. Mehrere Pointer auf dasselbe Objekt erzeugen Dangling-Pointer-Szenarien. Das undefinierte Verhalten kann von Angreifern für Codeausführung ausgenutzt werden. Stack-Abwicklung nach Selbstzerstörung kann auf freigegebenen Speicher zugreifen. Das Muster macht es sehr schwierig, über Objektlebensdauern nachzudenken, was die Wahrscheinlichkeit von Speichersicherheitsschwachstellen erhöht.

Lösung

Vermeiden Sie das "delete this"-Muster vollständig - verwenden Sie stattdessen RAII und Smart Pointer. Verwenden Sie Referenzzählung (std::shared_ptr) für gemeinsame Eigentumsrechte. Implementieren Sie release()-Methoden, die die Absicht zum Löschen signalisieren, aber nicht selbst zerstoren. Verwenden Sie das Destroyer-Pattern, bei dem ein Manager-Objekt die Löschung behandelt. Bei COM-artigem Referenzzählen verwenden Sie Release(), das vor dem delete zurückkehrt, wenn der Zähler Null erreicht. Wenden Sie statische Analyse an, um "delete this"-Verwendung zu erkennen. Falls das Muster absolut notwendig ist, stellen Sie sicher, dass kein Code nach dem delete ausgeführt wird und keine Mitgliedsvariablen zugegriffen werden. Erwägen Sie, solche Methoden als [[noreturn]] zu markieren wenn möglich.

Häufige Auswirkungen

AuswirkungDetails
VerfügbarkeitBereich: Verfügbarkeit

DoS: Absturz - Use-After-Free durch unsachgemäße Selbstzerstörung verursacht Abstürze.
IntegritätBereich: Integrität

Nicht autorisierten Code ausführen - Use-After-Free kann für beliebige Codeausführung ausgenutzt werden.
AndereBereich: Ändere

Reduzierte Zuverlässigkeit - Selbstzerstörung erzeugt undefiniertes Verhalten, das schwer zu debuggen ist.

Beispielcode

Anfälliger Code

// Anfällig: Klasse mit Selbstzerstörung
class VulnerableObserver {
private:
    std::string name;
    int observedValue;
    bool active;

public:
    VulnerableObserver(const std::string& n) : name(n), observedValue(0), active(true) {}

    void onValueChanged(int newValue) {
        observedValue = newValue;

        // Anfällig: Bedingung prüfen dann selbst zerstoren
        if (newValue < 0) {
            std::cout << name << " registriert sich ab" << std::endl;

            delete this;  // GEFAHR: Selbstzerstörung!

            // Anfällig: Code nach delete greift auf freigegebenen Speicher zu!
            active = false;  // Use-After-Free!
            std::cout << "Observer " << name << " abgemeldet" << std::endl;  // UB!
        }

        // Anfällig: Sogar die implizite Rückkehr greift auf 'this' zu
    }

    bool isActive() const { return active; }
};

void vulnerableUsage() {
    VulnerableObserver* obs = new VulnerableObserver("Sensor1");

    obs->onValueChanged(100);  // OK
    obs->onValueChanged(-1);   // Selbstzerstörung!

    // Anfällig: obs ist jetzt ein Dangling Pointer
    if (obs->isActive()) {  // Use-After-Free!
        obs->onValueChanged(50);  // Absturz oder schlimmer!
    }
}
// Anfällig: Referenzgezählte Klasse mit gefährlichem Release
class VulnerableRefCounted {
private:
    int refCount;
    char* data;
    size_t dataSize;

public:
    VulnerableRefCounted(size_t size) : refCount(1), dataSize(size) {
        data = new char[size];
    }

    void AddRef() {
        refCount++;
    }

    void Release() {
        refCount--;

        if (refCount == 0) {
            // Anfällig: Selbstzerstörung in Release
            delete this;

            // Anfällig: Jeder Code hier ist Use-After-Free
            std::cout << "Objekt zerstört" << std::endl;  // UB!
        }

        // Anfällig: Wenn refCount > 0, ist Rückkehr OK
        // Aber wenn refCount 0 war, haben wir this bereits gelöscht
    }

    char* getData() {
        return data;  // Könnte Daten von freigegebenem Objekt zurückgeben
    }

    ~VulnerableRefCounted() {
        delete[] data;
    }
};

void vulnerableRefCountUsage() {
    VulnerableRefCounted* obj = new VulnerableRefCounted(1024);

    obj->AddRef();  // refCount = 2

    // Thread 1
    obj->Release();  // refCount = 1

    // Thread 2 - Race Condition!
    char* ptr = obj->getData();  // Könnte freigegeben sein!
    obj->Release();  // refCount = 0, löscht

    // Thread 1 fährt fort
    memcpy(ptr, "data", 4);  // Use-After-Free!
}
// Anfällig: Callback-Handler mit Selbstzerstörung
class VulnerableCallbackHandler {
private:
    std::function<void()> callback;
    std::string identifier;
    bool completed;

public:
    VulnerableCallbackHandler(const std::string& id, std::function<void()> cb)
        : identifier(id), callback(cb), completed(false) {}

    void execute() {
        // Callback ausführen
        callback();

        // Als abgeschlossen markieren und selbst zerstoren
        completed = true;

        // Anfällig: Selbstzerstörung nach Callback
        delete this;

        // Anfällig: Der Aufrufer könnte completed prüfen!
    }

    bool isCompleted() const { return completed; }
};

void vulnerableCallbackUsage() {
    auto handler = new VulnerableCallbackHandler("task1", []() {
        std::cout << "Callback ausgeführt" << std::endl;
    });

    handler->execute();  // Selbstzerstörung!

    // Anfällig: handler ist jetzt Dangling
    if (handler->isCompleted()) {  // Use-After-Free!
        std::cout << "Aufgabe abgeschlossen" << std::endl;
    }
}

Korrigierter Code

// Korrigiert: shared_ptr für gemeinsames Eigentum verwenden
class FixedObserver : public std::enable_shared_from_this<FixedObserver> {
private:
    std::string name;
    int observedValue;
    bool active;

public:
    FixedObserver(const std::string& n) : name(n), observedValue(0), active(true) {}

    // Factory-Methode gibt shared_ptr zurück
    static std::shared_ptr<FixedObserver> create(const std::string& name) {
        return std::make_shared<FixedObserver>(name);
    }

    // Korrigiert: Rückgabewert zeigt an, ob Observer entfernt werden soll
    bool onValueChanged(int newValue) {
        observedValue = newValue;

        if (newValue < 0) {
            std::cout << name << " fordert Abmeldung an" << std::endl;
            active = false;
            return false;  // Signalisiert Aufrufer, diesen Observer zu entfernen
        }

        return true;  // Weiter beobachten
    }

    bool isActive() const { return active; }
};

class ObserverManager {
private:
    std::vector<std::shared_ptr<FixedObserver>> observers;

public:
    void addObserver(std::shared_ptr<FixedObserver> obs) {
        observers.push_back(obs);
    }

    void notifyAll(int value) {
        // Korrigiert: Manager behandelt Entfernung, nicht der Observer
        observers.erase(
            std::remove_if(observers.begin(), observers.end(),
                [value](auto& obs) {
                    return !obs->onValueChanged(value);
                }),
            observers.end()
        );
    }
};

void fixedUsage() {
    ObserverManager manager;

    auto obs = FixedObserver::create("Sensor1");
    manager.addObserver(obs);

    manager.notifyAll(100);  // OK
    manager.notifyAll(-1);   // Observer wird vom Manager entfernt

    // Korrigiert: obs ist noch gültig (shared_ptr), nur als inaktiv markiert
    if (obs->isActive()) {
        // Wird nicht ausgeführt - isActive() gibt false zurück
    }
}
// Korrigiert: Sichere Referenzzählung
class FixedRefCounted {
private:
    std::atomic<int> refCount;
    char* data;
    size_t dataSize;

    // Privater Destruktor - nur Release kann löschen
    ~FixedRefCounted() {
        delete[] data;
    }

public:
    FixedRefCounted(size_t size) : refCount(1), dataSize(size) {
        data = new char[size];
    }

    void AddRef() {
        refCount.fetch_add(1, std::memory_order_relaxed);
    }

    // Korrigiert: Release kehrt VOR delete zurück
    void Release() {
        // Dekrementieren und vorherigen Wert abrufen
        int prev = refCount.fetch_sub(1, std::memory_order_acq_rel);

        if (prev == 1) {
            // refCount ist jetzt 0 - wir sind die letzte Referenz
            // Korrigiert: Kein Code nach delete
            delete this;
            return;  // Return-Anweisung ist technisch UB aber funktioniert üblich
            // Besser: Code so strukturieren, dass nichts auf delete folgt
        }
        // Wenn prev > 1, Objekt noch lebendig, sicher zurückzukehren
    }

    char* getData() {
        return data;
    }
};

// Noch besser: shared_ptr verwenden
class FixedRefCountedModern {
private:
    std::unique_ptr<char[]> data;
    size_t dataSize;

public:
    FixedRefCountedModern(size_t size) : data(new char[size]), dataSize(size) {}

    // Keine manuelle Referenzzählung nötig - shared_ptr verwenden

    char* getData() { return data.get(); }
};

void fixedRefCountUsage() {
    // Korrigiert: shared_ptr behandelt Referenzzählung sicher
    auto obj = std::make_shared<FixedRefCountedModern>(1024);

    auto obj2 = obj;  // Gemeinsames Eigentum

    char* ptr = obj->getData();

    obj.reset();   // Eine Referenz freigeben
    obj2.reset();  // Letzte Referenz freigeben, Objekt gelöscht

    // ptr ist jetzt Dangling, aber kein Use-After-Free bei Ref-Counting
}
// Korrigiert: Callback-Handler ohne Selbstzerstörung
class FixedCallbackHandler {
public:
    enum class State { Pending, Running, Completed, Failed };

private:
    std::function<void()> callback;
    std::string identifier;
    std::atomic<State> state;

public:
    FixedCallbackHandler(const std::string& id, std::function<void()> cb)
        : identifier(id), callback(cb), state(State::Pending) {}

    void execute() {
        State expected = State::Pending;
        if (!state.compare_exchange_strong(expected, State::Running)) {
            return;  // Bereits ausgeführt oder wird ausgeführt
        }

        try {
            callback();
            state = State::Completed;
        } catch (...) {
            state = State::Failed;
            throw;
        }

        // Korrigiert: Keine Selbstzerstörung - Aufrufer verwaltet Lebensdauer
    }

    State getState() const { return state.load(); }
    bool isCompleted() const { return state == State::Completed; }
};

// Korrigiert: Manager behandelt Handler-Lebenszyklus
class CallbackManager {
private:
    std::vector<std::unique_ptr<FixedCallbackHandler>> handlers;

public:
    void addHandler(const std::string& id, std::function<void()> cb) {
        handlers.push_back(
            std::make_unique<FixedCallbackHandler>(id, cb)
        );
    }

    void executeAll() {
        for (auto& handler : handlers) {
            handler->execute();
        }
    }

    void removeCompleted() {
        handlers.erase(
            std::remove_if(handlers.begin(), handlers.end(),
                [](const auto& h) { return h->isCompleted(); }),
            handlers.end()
        );
    }
};

void fixedCallbackUsage() {
    CallbackManager manager;

    manager.addHandler("task1", []() {
        std::cout << "Callback ausgeführt" << std::endl;
    });

    manager.executeAll();  // Führt Callback aus
    manager.removeCompleted();  // Manager behandelt Bereinigung

    // Keine Dangling Pointer, kein Use-After-Free
}

CVE-Beispiele

Use-After-Free-Schwachstellen durch unsachgemäße Objektlebensdauerverwaltung sind häufig, obwohl spezifische CVEs typischerweise die resultierende Schwachstelle beschreiben anstatt das Selbstzerstörungsmuster speziell.


Verwandte CWEs

  • CWE-1076: Insufficient Adherence to Expected Conventions (Eltern)
  • CWE-416: Use After Free (resultiert häufig daraus)
  • CWE-415: Double Free (kann bei unsachgemäßer Selbstzerstörung auftreten)

Referenzen

  1. MITRE Corporation. "CWE-1082: Class Instance Self Destruction Control Element." https://cwe.mitre.org/data/definitions/1082.html

  2. Meyers, Scott. "Effective C++." Item 27: Minimize casting (bezogen auf sicheres Downcasting).

  3. C++ Core Guidelines. R.11: Avoid calling new and delete explicitly.