Class Instance Self Destruction Control Element

Description

Class Instance Self Destruction Control Element occurs when code contains a class instance that calls a method or function to delete or destroy itself. The classic example is C++ code using "delete this" to remove an object from memory. While technically valid in C++, this pattern is dangerous because after self-destruction, any use of the object (including implicit uses like returning from the method or accessing member variables) results in undefined behavior. This creates code that is difficult to maintain and prone to use-after-free vulnerabilities.

Risk

Self-destructing objects have direct security implications. Any code that executes after "delete this" accesses freed memory (use-after-free). The caller may still hold a reference to the deleted object, leading to use-after-free when they use it. Multiple pointers to the same object create dangling pointer scenarios. The undefined behavior can be exploited by attackers for code execution. Stack unwinding after self-destruction can access freed memory. The pattern makes it very difficult to reason about object lifetimes, increasing the likelihood of memory safety vulnerabilities.

Solution

Avoid "delete this" pattern entirely - use RAII and smart pointers instead. Use reference counting (std::shared_ptr) for shared ownership. Implement release() methods that signal intent to delete but don't self-destruct. Use the destroyer pattern where a manager object handles deletion. In COM-style reference counting, use Release() that returns before delete if count reaches zero. Apply static analysis to detect "delete this" usage. If the pattern is absolutely necessary, ensure no code runs after the delete and no member variables are accessed. Consider marking such methods as [[noreturn]] if possible.

Common Consequences

ImpactDetails
AvailabilityScope: Availability

DoS: Crash - Use-after-free from improper self-destruction causes crashes.
IntegrityScope: Integrity

Execute Unauthorized Code - Use-after-free can be exploited for arbitrary code execution.
OtherScope: Other

Reduce Reliability - Self-destruction creates undefined behavior that's hard to debug.

Example Code

Vulnerable Code

// Vulnerable: Class with self-destruction
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;

        // Vulnerable: Check condition then self-destruct
        if (newValue < 0) {
            std::cout << name << " is unregistering" << std::endl;

            delete this;  // DANGER: Self-destruction!

            // Vulnerable: Code after delete accesses freed memory!
            active = false;  // Use-after-free!
            std::cout << "Observer " << name << " unregistered" << std::endl;  // UB!
        }

        // Vulnerable: Even the implicit return accesses 'this'
    }

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

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

    obs->onValueChanged(100);  // OK
    obs->onValueChanged(-1);   // Self-destructs!

    // Vulnerable: obs is now a dangling pointer
    if (obs->isActive()) {  // Use-after-free!
        obs->onValueChanged(50);  // Crash or worse!
    }
}
// Vulnerable: Reference-counted class with dangerous 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) {
            // Vulnerable: Self-destruction in Release
            delete this;

            // Vulnerable: Any code here is use-after-free
            std::cout << "Object destroyed" << std::endl;  // UB!
        }

        // Vulnerable: If refCount > 0, returning is OK
        // But if refCount was 0, we've already deleted this
    }

    char* getData() {
        return data;  // Might return data from freed object
    }

    ~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();  // Might be freed!
    obj->Release();  // refCount = 0, deletes

    // Thread 1 continues
    memcpy(ptr, "data", 4);  // Use-after-free!
}
// Vulnerable: Callback handler with self-destruction
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() {
        // Execute the callback
        callback();

        // Mark as completed and self-destruct
        completed = true;

        // Vulnerable: Self-destruction after callback
        delete this;

        // Vulnerable: The caller might check completed!
    }

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

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

    handler->execute();  // Self-destructs!

    // Vulnerable: handler is now dangling
    if (handler->isCompleted()) {  // Use-after-free!
        std::cout << "Task completed" << std::endl;
    }
}

Fixed Code

// Fixed: Use shared_ptr for shared ownership
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 method returns shared_ptr
    static std::shared_ptr<FixedObserver> create(const std::string& name) {
        return std::make_shared<FixedObserver>(name);
    }

    // Fixed: Return value indicates if observer should be removed
    bool onValueChanged(int newValue) {
        observedValue = newValue;

        if (newValue < 0) {
            std::cout << name << " requesting unregister" << std::endl;
            active = false;
            return false;  // Signal to caller to remove this observer
        }

        return true;  // Continue observing
    }

    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) {
        // Fixed: Manager handles removal, not the 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 removed by manager

    // Fixed: obs is still valid (shared_ptr), just marked inactive
    if (obs->isActive()) {
        // Won't execute - isActive() returns false
    }
}
// Fixed: Safe reference counting
class FixedRefCounted {
private:
    std::atomic<int> refCount;
    char* data;
    size_t dataSize;

    // Private destructor - only Release can delete
    ~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);
    }

    // Fixed: Release returns BEFORE delete
    void Release() {
        // Decrement and get previous value
        int prev = refCount.fetch_sub(1, std::memory_order_acq_rel);

        if (prev == 1) {
            // refCount is now 0 - we're the last reference
            // Fixed: No code after delete
            delete this;
            return;  // Return statement is technically UB but commonly works
            // Better: structure code so nothing follows delete
        }
        // If prev > 1, object still alive, safe to return
    }

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

// Even better: Use shared_ptr
class FixedRefCountedModern {
private:
    std::unique_ptr<char[]> data;
    size_t dataSize;

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

    // No manual reference counting needed - use shared_ptr

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

void fixedRefCountUsage() {
    // Fixed: shared_ptr handles reference counting safely
    auto obj = std::make_shared<FixedRefCountedModern>(1024);

    auto obj2 = obj;  // Shared ownership

    char* ptr = obj->getData();

    obj.reset();   // Release one reference
    obj2.reset();  // Release last reference, object deleted

    // ptr is now dangling, but no use-after-free in ref counting
}
// Fixed: Callback handler without self-destruction
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;  // Already executed or executing
        }

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

        // Fixed: No self-destruction - caller manages lifetime
    }

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

// Fixed: Manager handles handler lifecycle
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 executed" << std::endl;
    });

    manager.executeAll();  // Executes callback
    manager.removeCompleted();  // Manager handles cleanup

    // No dangling pointers, no use-after-free
}

CVE Examples

Use-after-free vulnerabilities from improper object lifetime management are common, though specific CVEs typically describe the resulting vulnerability rather than the self-destruction pattern specifically.


  • CWE-1076: Insufficient Adherence to Expected Conventions (parent)
  • CWE-416: Use After Free (commonly results from)
  • CWE-415: Double Free (can occur with improper self-destruction)

References

  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 (related to safe downcasting).
  3. C++ Core Guidelines. R.11: Avoid calling new and delete explicitly.