VNX-1087: Class with Virtual Method without a Virtual Destructor
A C++ class declares virtual methods but no virtual destructor, so deleting it through a base-class pointer never runs the derived destructor.
| Field | Value |
|---|---|
| Rule ID | VNX-1087 |
| Severity | Medium |
| SARIF level | warning |
| Kind | sast |
| Languages | C++ |
| CWE | CWE-1087 |
| CWSS vector | CWSS:1.0/TI:M/AP:RU/AL:S/IC:N/FC:LT/RP:RU/RL:S/AV:L/AS:N/IN:A/SC:A/BI:M/DI:H/EX:M/EC:N/P:C |
| Tags | cwe-1087 object-lifetime cpp |
| Source | internal/sast/rules/vnx-1087.rego |
Overview
VNX-1087 maps to CWE-1087: Class with Virtual Method without a Virtual Destructor.
A class that declares virtual methods is meant to be used polymorphically, held and deleted through a pointer to a base class. If its destructor is not virtual, delete basePtr runs only the base destructor. The derived class’s destructor never runs, so whatever it owned is leaked, and the standard defines the behaviour as undefined.
What Gets Flagged
A class with at least one virtual member function and no virtual destructor:
// FLAGGED: virtual method, non-virtual destructor
class Base {
public:
virtual void handle();
~Base();
};
// FLAGGED: virtual method, no destructor declared, so it inherits a non-virtual one
class NoDtor {
public:
virtual int compute() const;
};
// FLAGGED: a struct interface is the same defect
struct Listener {
virtual void onEvent(int code) = 0;
};
What Does Not Get Flagged
Three shapes are correct and are deliberately not reported:
// Destructor is virtual, the usual fix.
class Good {
public:
virtual void handle();
virtual ~Good();
};
// `final`: cannot be derived from, so it can never be deleted through a base
// pointer of its own type.
class Sealed final {
public:
virtual void handle();
~Sealed();
};
// Protected non-virtual destructor: the other sanctioned resolution. Callers
// cannot delete through a base pointer at all, so there is nothing to slice.
class Guarded {
public:
virtual void handle();
protected:
~Guarded();
};
A destructor marked override or final also counts as virtual, since only a virtual function can override one.
Remediation
- If the class is meant to be a polymorphic base, declare the destructor
virtual:virtual ~Base(); - If it is not meant to be deleted through a base pointer, make the destructor
protectedand non-virtual. This is cheaper, no vtable entry, and the compiler enforces the intent. - If the class is not meant to be derived from at all, mark it
final. - For a confirmed false positive, add
// vulnetix-ignore: VNX-1087on the line.
Scope
The rule analyses class and struct bodies up to 400 lines; a longer class is skipped rather than guessed at. The body ends at the }; at the class’s own indentation, so a nested enum class or struct does not cut it short, and the virtual members of a nested class are judged on that class, not the outer one. Forward declarations (class X;), class heads inside /* */ comments and virtual base classes (class D : virtual public B) are ignored.
A derived class’s destructor is virtual whenever its base’s is, even without the keyword, so a derived class is only reported when that can be read from the same file: one of its bases is a root class defined there and no base declares a virtual destructor. A derived class that marks members override or final is never reported; its base is checked where it is defined.
Note on earlier versions
Before 2026-08, this rule shipped as a Python “function has no docstring” check, unrelated to its declared CWE, byte-identical to VNX-1117, and the largest single source of findings on any Python repository. VNX-1117 remains the general documentation lint; VNX-1087 now checks what its identifier says it checks.
References
- CWE-1087
- C++ Core Guidelines C.35: A base class destructor should be either public and virtual, or protected and non-virtual
Suppressing this rule
Put a nosec comment on the line, or on line 1 to cover the whole file. Use the comment marker of the file’s language (#, //, -- or ;):
# nosec vnx-1087
// nosec vnx-1087
Or record an ignore rule: vulnetix ignore add --rule vnx-1087 --reason "...". See Suppressing findings.