Missing virtual destructor
| Vulnerability potential | Low |
| DDoS potential | Low |
Class with a virtual method has a non-virtual destructor; deleting through a base pointer slices Derived destructors and leaks resources
Impact
When an object of a derived type is destroyed through a pointer to a base class
whose destructor is not virtual, the behavior is undefined ([expr.delete]).
In practice the compiler emits a static call to the base destructor only: the
derived destructor never runs, derived members are never destroyed, and any
resources they own (heap buffers, file handles, sockets, locks) are leaked.
On most ABIs delete also passes the wrong size to the deallocation function
because the static type is smaller than the actual object, which can corrupt
the allocator’s size class metadata. The symptoms range from a slow memory
leak to heap corruption and crashes.
Vulnerability potential
This is undefined behavior, so its security weight is real but usually modest.
- The leaked resources accumulate. A request path that creates and deletes such objects per operation gives an attacker a way to exhaust memory, file descriptors, or other handles, i.e. a denial-of-service primitive.
- When
operator deleteis handed a size that does not match the real allocation (sized-deallocation, jemalloc/tcmalloc size classes), allocator metadata can be corrupted, which in rare cases is exploitable for memory corruption.
Technical details
A destructor invoked through delete base_ptr uses virtual dispatch only
if the destructor is declared virtual. Without it, the call is resolved
statically to Base::~Base, the most-derived destructor and all intermediate
destructors are skipped, and no derived member destructors are run.
Sized deallocation
Since C++14 the compiler may call the sized form operator delete(void*,
std::size_t). The size is computed from the static type at the delete
site. For a derived object this size is wrong, which is exactly the kind of
mismatch that hardened allocators detect or that corrupts free-list metadata.
When it is harmless
If objects are always deleted through a pointer of their own most-derived
type, or are never deleted polymorphically (e.g. held by value, or owned by a
shared_ptr created from the derived type — shared_ptr stores a type-erased
deleter), the bug does not trigger. The danger is specifically delete (or a
unique_ptr<Base>) acting on a base pointer.
Catching the issue
Compiler
GCC and Clang warn with -Wnon-virtual-dtor (part of -Wall -Wextra on
Clang via -Weffc++); -Wdelete-non-virtual-dtor fires directly at the
delete site. Treat both as errors with -Werror.
Static analysis
clang-tidy cppcoreguidelines-virtual-class-destructor and the Core
Guidelines rule C.35 flag polymorphic base classes lacking a virtual or
protected destructor.
Runtime
AddressSanitizer with new-delete-type-mismatch checking and LeakSanitizer
report both the size mismatch and the leaked derived members at run time.
How to reproduce
Build with -fsanitize=address; ASan reports new-delete-type-mismatch and
the leaked std::string buffer.
#include <memory>
#include <string>
struct Base {
virtual void poll() {}
~Base() {} // BUG: not virtual
};
struct Derived : Base {
std::string big = std::string(1 << 20, 'x'); // 1 MiB, leaked
void poll() override {}
};
int main() {
Base* p = new Derived();
p->poll();
delete p; // runs ~Base only; ~Derived skipped
}