Some days ago, I became aware of a bug in GCC that has apparently existed since 2015. As this is a bug that deals with memory leaks, it is fairly serious.
In this case, I really don’t want to re-post the entire content of the source, so I will only list my experiments with reproducing the bug once I became aware of it.
C++ assumes that if a constructor fails, then no memory is allocated for the object at all. This means that if a nested object was constructed, C++ will implicitly call the destructor for that object so that the programmer does not have to worry about partially constructed states. This bug in GCC exists because under certain circumstances, GCC fails this assumption.
Let’s start off with a simple example trying to reproduce this bug.
#include <iostream>
#include <exception>
class inner {
private:
char* array;
public:
inner() : array(new char[128])
{
std::cout << "inner constructed" << std::endl;
}
inner(const inner& other) : array(other.array)
{
std::cout << "inner copy constructed" << std::endl;
}
~inner()
{
delete[] array;
std::cout << "inner destructed" << std::endl;
}
};
inner make_inner_1() {return inner();}
inner make_inner_2() {throw std::runtime_error("exception");}
struct outer {
inner i1;
inner i2;
};
int main(void)
{
try {
outer o{make_inner_1(), make_inner_2()};
}
catch (const std::exception& e) {
std::cout << "Abort after exception " << e.what() << std::endl;
}
return 0;
}
Upon compiling and running this code, the output is
inner constructed
inner destructed
Abort after exception exception
This behaviour is as is expected given C++ standards. The constructor
for object outer
tries to construct i1
, succeeds, then tries to
construct i2
, fails, and destructs i1
.
Well, what if we try to create a nameless temporary object?
#include <iostream>
#include <exception>
class inner {
private:
char* array;
public:
inner() : array(new char[128])
{
std::cout << "inner constructed" << std::endl;
}
inner(const inner& other) : array(other.array)
{
std::cout << "inner copy constructed" << std::endl;
}
~inner()
{
delete[] array;
std::cout << "inner destructed" << std::endl;
}
};
inner make_inner_1() {return inner();}
inner make_inner_2() {throw std::runtime_error("exception");}
struct outer {
inner i1;
inner i2;
};
struct foo {
outer o;
};
int main(void)
{
try {
foo{outer({make_inner_1(), make_inner_2()})};
}
catch (const std::exception& e) {
std::cout << "Abort after exception " << e.what() << std::endl;
}
return 0;
}
In this case, the output is
inner constructed
Abort after exception exception
This introduces a memory leak, because while i1
is constructed, it is
never destroyed. If I compile using clang (version 3.8.0-2ubuntu4), the
expected output is still correct.
inner constructed
inner destructed
Abort after exception exception
The fact that this bug has remained unresolved for over two years is surprising. At this point, I’m tempted to point out a paper by Ken Thompson, Reflections on trusting trust, which points out that an untrustworthy compiler could introduce a bug in all or a few select programs that it compiles. Moreover, the compiler could be engineered to introduce a bug in its own binary, so compiling the compiler from source wouldn’t help either.
This is a subtle bug that took me some time to reproduce. However, I can
easily imagine how frustrating it could be if this bug were to manifest
in a larger program that would show up through the use of memcheck
or
a similar tool. In the meanwhile, we cannot do much except wait for a
patch. This bug hasn’t shown up yet in code I’ve written, but I can
always switch my build system to clang/Makefiles (the joys of writing
platform-independent code
with CMake as a build system).