The vulnerability as described in ISO/IEC TR 24772-1:2019 clause 6.41 is applicable to C++.
Inheritance as a mechanism in C++ serves multiple purposes and is defined differently than in most other languages supporting inheritance.
A
indirectly via different direct base classes. Without any special preparation, this means the base class A
object exists multiple times. Addressing members of A
explicitly requires to specify the differentiating base class as a prefix, otherwise the code will be ambiguous. If all classes in such a multiple inheritance hierarchy that directly inherit from A
use the keyword virtual
when inheriting from A
, there will be only one object of type A
in the most derived object. Inconsistently inheriting from A
with and without virtual
might lead to confusing behavior, because still multiple base objects of type A
exist. Inheriting from base classes without virtual member functions and without non-static data members (empty bases) does not suffer from the multiple object problem of multiple inheritance, because the empty base class object will be omitted (elided) by the compiler (empty base class optimization). Such empty bases are often used to mix-in functionality into derived classes.The compiler-provided default behaviour for copy and move operations as well as destruction favors value semantics which conflicts with object-oriented polymorphic behaviour.
Virtual Destructor: This means, base classes that define virtual member functions will need to also define a virtual destructor and in addition need to care about the copy and move operations, otherwise deleting a dynamically-allocated derived object via a base class pointer will cause undefined behaviour [EWF].
Slicing: A common failure is to not eliminate implicitly callable copy and move operations in base classes which will lead to accidental copying of a base class suboject via a base class reference that actually refers to a derived object. Preventing implicit copy and move operations in base classes defining virtual member functions is a common mitigation.
Incomplete Copy: When a derived class defines its own non-defaulted, non-deleted copy or move operations, care must be taken to actually copy and move all base class subobjects as well. Omitting a base class when defining copy or move constructors means the default construction of a base class object happens. Not invoking a base class assignment in the definition of copy and move assignment operators will cause the base class retaining its previous members and not obtaining the source object’s base members. None of these omissions are a compile error and none are an issue for empty bases.
If a base class overloads operator new
and operator delete
, any derived classes will inherit and therefore will use such. If the base class’ operator new
and/or operator delete
assume the size of the objects being allocated are all the size of the base class and they are not all the same size, then this will result in undefined behaviour such as access errors to memory that wasn’t allocated, overwriting of memory (if there are regions of memory immediately after the last byte allocated), memory leaks, etc. For example, consider,
#include <new>
#include <iostream>
class base
{public:
static void* operator new(std::size_t sz)
{std::cerr << "DEBUG: Base::" << __func__ << "(" << sz << ")" << '\n';
return ::operator new(sizeof(base));
}
static void operator delete(void *ptr, std::size_t sz)
{std::cerr << "DEBUG: Base::" << __func__ << "(" << ptr << ',' << sz << ")" << '\n';
operator delete(ptr);
::
}
};
class derived : public base
{double d;
};
int main()
{std::cerr << "DEBUG: sizeof(base): " << sizeof(base) << '\n';
std::cerr << "DEBUG: sizeof(derived): " << sizeof(derived) << '\n';
// new derived invokes base::operator new
new derived;
derived *p =
// delete p invokes base::operator delete
delete p;
}
If a class-overloaded operator new
and operator delete
can only handle fixed-sized allocations, then consider the following:
declare the class as a final
class to prohibit derived classes
call the global operator new
when the size parameter does not match what is expected, e.g.,
if (sz != sizeof(base))
return ::operator new(sz);
operator delete
that has a std::size_t
size parameter passed to it so that the global operator delete
can be properly called (if the global operator new
was called to allocate the data), e.g.,if (sz != sizeof(base))
operator delete(ptr); ::
It should also be mentioned that C++ requires operator new
to return a valid pointer should its size parameter be zero.
The mechanisms of failure from ISO/IEC TR 24772-1:2019 clause 6.41 manifest and can be mitigated in C++ as follows:
Execution of malicious redefinitions can be prevented by use of final
on each member function to generate compiler diagnostics when overriding is not permitted.
Accidental redefinition can be mitigated by a project mandate to use the override
or final
special identifiers when overriding a virtual member functions.
Accidental failure of redefinition can be prevented by using override
on each member function intended to be redefined to generate compiler diagnostics when overriding does not apply.
Breaking of class invariants can be avoided by proper initialization even with the default constructor and by defining data members private if the class invariant depends on them. If copy and move operations are user-defined in a derived class they must ensure to call the corresponding base class operations.
Direct reading and writing of visible class members of a base class can be avoided by declaring the data members private and only allowing the class-invariant-preserving member functions in the derived classes. If those member functions are not part of the public API, they can be declared as protected
.
To avoid the vulnerability or mitigate its ill effects, C++ software developers can:
Follow the avoidance mechanisms of ISO/IEC 24772-1 clause 41.5.
Except for mix-in empty bases avoid multiple inheritance.
Avoid defining copy or move operations (see clause 6.38 Deep vs. Shallow Copying [YAN]), and if the implementation of copy-operations or move-operations in a derived class is mandatory, then statically ensure that all calls are to the corresponding base classes’ operations.
Prefer composition over inheritance, and in general keep inheritance hierarchies shallow.
Restrict the use of virtual member functions to situations where unbounded run-time polymorphism is beneficial.
Use override
when overriding a virtual member function to generate compiler diagnostics for failures to override.
Mandate re-compilation of all derived classes when a base class changes.
Consider using fully-qualified names to address members of a base class.
When defining a potentially hiding overload in a derived class, consider adding a using declaration of the base class name.
Prohibit the use of public inheritance for “has-a” relationships; instead preferring composition or private/protected inheritance to “has-a”-relationships.
Prohibit mix virtual and non-virtual inheritance of the same base class in a hierarchy.