A bug in how GCC handles constructors

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).

Related

comments powered by Disqus