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
| Impact | Details |
|---|---|
| Availability | Scope: Availability DoS: Crash - Use-after-free from improper self-destruction causes crashes. |
| Integrity | Scope: Integrity Execute Unauthorized Code - Use-after-free can be exploited for arbitrary code execution. |
| Other | Scope: 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.
Related CWEs
- 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
- MITRE Corporation. "CWE-1082: Class Instance Self Destruction Control Element." https://cwe.mitre.org/data/definitions/1082.html
- Meyers, Scott. "Effective C++." Item 27: Minimize casting (related to safe downcasting).
- C++ Core Guidelines. R.11: Avoid calling new and delete explicitly.