In D144319, Clang tried to land a change that would cause some functions
that are not supposed to return nullptr to optimize better. As reported
in https://reviews.llvm.org/D144319#4203982, libc++ started seeing failures
in its CI shortly after this change was landed.
As explained in D146379, the reason for these failures is that libc++'s
throwing operator new can in fact return nullptr when compiled with
exceptions disabled. However, this contradicts the Standard, which
clearly says that the throwing version of operator new(size_t)
should never return nullptr. This is actually a long standing issue.
I've previously seen a case where LTO would optimize incorrectly based
on the assumption that operator new doesn't return nullptr, an
assumption that was violated in that case because libc++.dylib was
compiled with -fno-exceptions.
Unfortunately, fixing this is kind of tricky. The Standard has a few
requirements for the allocation functions, some of which are impossible
to satisfy under -fno-exceptions:
- operator new(size_t) must never return nullptr
- operator new(size_t, nothrow_t) must call the throwing version and return nullptr on failure to allocate
- We can't throw exceptions when compiled with -fno-exceptions
In the case where exceptions are enabled, things work nicely. new(size_t)
throws and new(size_t, nothrow_t) uses a try-catch to return nullptr.
However, when compiling the library with -fno-exceptions, we can't throw
an exception from new(size_t), and we can't catch anything from
new(size_t, nothrow_t). The only thing we can do from new(size_t)
is actually abort the program, which does not make it possible for
new(size_t, nothrow_t) to catch something and return nullptr.
This patch makes the following changes:
- When compiled with -fno-exceptions, the throwing version of operator new will now abort on failure instead of returning nullptr on failure. This resolves the issue that the compiler could mis-compile based on the assumption that nullptr is never returned. This constitutes an API and ABI breaking change for folks compiling the library with -fno-exceptions (which is not the general public, who merely uses libc++ headers but use a shared library that has already been compiled). This should mostly impact vendors and other folks who compile libc++.dylib themselves.
- When the library is compiled with -fexceptions, the nothrow version of operator new has no change. When the library is compiled with -fno-exceptions, the nothrow version of operator new will now check whether the throwing version of operator new has been overridden. If it has not been overridden, then it will use an implementation equivalent to that of the throwing operator new, except it will return nullptr on failure to allocate (instead of terminating). However, if the throwing operator new has been overridden, it is now an error NOT to also override the nothrow operator new. Indeed, there is no way for us to implement a valid nothrow operator new without knowing the exact implementation of the throwing version.
TODO: Missing test cases
- Add an assertion test to ensure that we catch the case where operator new has been overridden but operator new(nothrow) has not and the library is compiled with -fno-exceptions. We should fail with an assertion.
- Add a test to ensure that operator new(nothrow) returns nullptr on failure to allocate even when the library was compiled with -fno-exceptions.
- Add tests for the __is_function_overridden machinery.
rdar://103958777