Skip to content

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.

Updated 02 Nov 2026Checked against cli@de9b52a
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

  1. If the class is meant to be a polymorphic base, declare the destructor virtual:
    virtual ~Base();
  2. If it is not meant to be deleted through a base pointer, make the destructor protected and non-virtual. This is cheaper, no vtable entry, and the compiler enforces the intent.
  3. If the class is not meant to be derived from at all, mark it final.
  4. For a confirmed false positive, add // vulnetix-ignore: VNX-1087 on 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

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.

Edit this pageSources: cli/internal/sast/rules/vnx-1087.rego, cli/internal/sast/docs/vnx-1087.md
esc
↑ ↓ move↵ openSearches in your browser. A key like autonomy.max or a code like threshold_required jumps straight to it.