The vulnerability described in ISO/IEC TR 24772-1:2019 clause 6.21 exists in C++. It can occur in particular when a used library changes its API. The situations where it exists are related to the following cases:
In the case of template specialization or non-identical definitions of the same entity in different translation units (ODR-violation), ill-formed code might be the result, however, a C++ compiler is not obliged to diagnose that situation, leading to undefined behaviour [EWF].
In the case of overloading and overriding cases, C++ compilers are required to diagnose an ambiguity if it exists.
However, overload resolution applies preference rules in order to select among multiple matching functions or function templates as a means to resolve the ambiguity among these functions. Hence, for calls that are not perfect matches, the user cannot guarantee in the presence of later changes which function is called, as another, better match can be introduced subsequently. The call in question then changes its binding without warning upon its next compilation. For cases, where the preference rules do not resolve the ambiguity, the resulting error message by the compiler avoids the vulnerability. Function template specializations are not considered during overload resolution, only the base template is considered.
void foo (long);
// void foo (int);
void bar ()
{0); // The call to 'foo(long)' requires implicitly conversion
foo (// from 'int' to 'long'. The function 'foo(int)'
// would be a "better match" and so would silently
// be chosen when subsequently introduced
}
A new declaration can impact existing code in a number of situations involving the addition of:
A using
directive broadens the possible scopes that will be examined for names during lookup. Where lookup searches a namespace referred to by a using
directive, all names in that namespace will be visible some of which may be unwanted. A using declaration, on the other hand, declares only the specified name into the scope of the using
declaration.
namespace NS1
{void f1 (int);
void f2 (int); // Added later
}namespace NS2
{using namespace NS1; // 'f1' needed
void f2 (long);
void bar ()
{0); // Calls 'NS1::f1'
f1(0); // Unintentionally calls 'NS1::f2'
f2(
}
}namespace NS3
{using NS1::f1; // 'f1' needed
void f2 (long);
void bar ()
{0); // Calls 'NS1::f1'
f1(0); // Calls 'NS3::f2' as expected
f2(
} }
Overload resolution only considers conversions for the explicitly specified arguments and does not take default parameters into account:
void f1 (short, int = 0);
void f1 (int, short = 0);
void f2 ()
{1); // calls 'f1(1, 0)' as '1 -> int' is better match than '1 -> short'
f1 (1, 0); // ambiguous, ill-formed, won't compile
f1 ( }
The following example demonstrates a situation where the late addition of a better matching overload causes a silent change in the semantics of an existing program.
namespace NS
{struct A
{
};
template < typename T > T foo ( T t )
{return t;
}
}
namespace NS2 // separately developed and included from a header file
{struct B
{
};// This code will be added later
// template < typename T > T * foo ( T * t )
// {
// return t;
// }
}
using namespace NS2;
using namespace NS;
void bar()
{
A * a;
B * b;// After the commented-out code is added to NS2, the binding of foo changes silently from NS::foo to NS2::foo
foo (a); }
This issue can be avoided by avoiding using namespace xxx{.cpp}
and explicitly qualifying each call, such as NS::foo(a){.cpp}
.
Analogously, when a more specialized template is added to an imported namespace where the more general template has already been provided in another namespace, preference rules will silently prefer the more specialized template.
A similar situation can occur when a conflict arises between compiler-synthesized or rewritten operators and explicitly created versions of those operators, as in the following example.
struct A
{bool operator==(A const &) const { return true; }
};
// Evil hijacking of !=
// bool operator != (A const &, A const &) { return true; } // #1
void bar (A const & a) {
// #2
a != a; }
In the above example, the declaration of operator==
will have a corresponding synthesised operator!=
generated by the compiler, since there is no suitable user-declared !=
. If the operator!=
becomes visible, then the code at #2 uses the user-declared operator!=
instead of the synthesized one, which can lead to a silent and unexpected change of behaviour. This is particularly risky when the operator is declared outside of the immediate visibility of the original definition.
To avoid the vulnerability or mitigate its ill effects, C++ software developers can:
Use the avoidance mechanisms of clauses 6.20.2, 6.40.2, and 6.41.2 as applicable.
Consider using fully qualified names for calls that rely on an implicit conversions.
Arguments to called functions should not be subject to implicit conversions.
Prefer using declarations to using directives.
Do not overload and use default arguments for the same set of functions.
Do not specialize function templates.
For template specialization, ensure that specializations are declared as follows:
In the same file as the primary template; or
In the same file as the user-defined type for which the specialization is declared.
Define an entity in only one file to prevent ODR-violations.
Ensure that no ODR-violations occur, i.e., through a static analysis tool.
Use a version-aware analysis tool to identify situations where preference rules cause a silent change of name binding between versions.
Only declare equaility or relational operators as member functions or friend functions of the class