mirror of
https://github.com/llvm/llvm-project.git
synced 2025-04-17 16:26:42 +00:00

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: 1. `operator new(size_t)` must never return nullptr 2. `operator new(size_t, nothrow_t)` must call the throwing version and return nullptr on failure to allocate 3. 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: 1. 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. 2. 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. In summary, this change will impact people who fall into the following intersection of conditions: - They use the libc++ shared/static library built with `-fno-exceptions` - They do not override `operator new(..., std::nothrow_t)` - They override `operator new(...)` (the throwing version) - They use `operator new(..., std::nothrow_t)` We believe this represents a small number of people. Fixes #60129 rdar://103958777 Differential Revision: https://reviews.llvm.org/D150610
405 lines
13 KiB
C++
405 lines
13 KiB
C++
//===----------------------------------------------------------------------===//
|
|
//
|
|
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
|
|
// See https://llvm.org/LICENSE.txt for license information.
|
|
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
|
|
//
|
|
//===----------------------------------------------------------------------===//
|
|
|
|
#ifndef TEST_SUPPORT_CHECK_ASSERTION_H
|
|
#define TEST_SUPPORT_CHECK_ASSERTION_H
|
|
|
|
#include <array>
|
|
#include <cassert>
|
|
#include <csignal>
|
|
#include <cstdarg>
|
|
#include <cstddef>
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <exception>
|
|
#include <functional>
|
|
#include <regex>
|
|
#include <sstream>
|
|
#include <string>
|
|
#include <string_view>
|
|
#include <utility>
|
|
|
|
#include <unistd.h>
|
|
#include <errno.h>
|
|
#include <signal.h>
|
|
#include <sys/wait.h>
|
|
|
|
#include "test_macros.h"
|
|
#include "test_allocator.h"
|
|
|
|
#if TEST_STD_VER < 11
|
|
# error "C++11 or greater is required to use this header"
|
|
#endif
|
|
|
|
// When printing the assertion message to `stderr`, delimit it with a marker to make it easier to match the message
|
|
// later.
|
|
static constexpr const char* Marker = "###";
|
|
|
|
// (success, error-message-if-failed)
|
|
using MatchResult = std::pair<bool, std::string>;
|
|
using Matcher = std::function<MatchResult(const std::string& /*text*/)>;
|
|
|
|
MatchResult MatchAssertionMessage(const std::string& text, std::string_view expected_message) {
|
|
// Extract information from the error message. This has to stay synchronized with how we format assertions in the
|
|
// library.
|
|
std::regex assertion_format(".*###\\n(.*):(\\d+): assertion (.*) failed: (.*)\\n###");
|
|
|
|
std::smatch match_result;
|
|
bool has_match = std::regex_match(text, match_result, assertion_format);
|
|
assert(has_match);
|
|
assert(match_result.size() == 5);
|
|
|
|
const std::string& file = match_result[1];
|
|
int line = std::stoi(match_result[2]);
|
|
// Omitting `expression` in `match_result[3]`
|
|
const std::string& assertion_message = match_result[4];
|
|
|
|
bool result = assertion_message == expected_message;
|
|
if (!result) {
|
|
std::stringstream matching_error;
|
|
matching_error //
|
|
<< "Expected message: '" << expected_message.data() << "'\n" //
|
|
<< "Actual message: '" << assertion_message.c_str() << "'\n" //
|
|
<< "Source location: " << file << ":" << std::to_string(line) << "\n";
|
|
return MatchResult(/*success=*/false, matching_error.str());
|
|
}
|
|
|
|
return MatchResult(/*success=*/true, /*maybe_error=*/"");
|
|
}
|
|
|
|
Matcher MakeAssertionMessageMatcher(std::string_view assertion_message) {
|
|
return [=](const std::string& text) { //
|
|
return MatchAssertionMessage(text, assertion_message);
|
|
};
|
|
}
|
|
|
|
Matcher MakeAnyMatcher() {
|
|
return [](const std::string&) { //
|
|
return MatchResult(/*success=*/true, /*maybe_error=*/"");
|
|
};
|
|
}
|
|
|
|
enum class DeathCause {
|
|
// Valid causes
|
|
VerboseAbort = 1,
|
|
StdAbort,
|
|
StdTerminate,
|
|
Trap,
|
|
// Invalid causes
|
|
DidNotDie,
|
|
SetupFailure,
|
|
Unknown
|
|
};
|
|
|
|
bool IsValidCause(DeathCause cause) {
|
|
switch (cause) {
|
|
case DeathCause::VerboseAbort:
|
|
case DeathCause::StdAbort:
|
|
case DeathCause::StdTerminate:
|
|
case DeathCause::Trap:
|
|
return true;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
std::string ToString(DeathCause cause) {
|
|
switch (cause) {
|
|
case DeathCause::VerboseAbort:
|
|
return "verbose abort";
|
|
case DeathCause::StdAbort:
|
|
return "`std::abort`";
|
|
case DeathCause::StdTerminate:
|
|
return "`std::terminate`";
|
|
case DeathCause::Trap:
|
|
return "trap";
|
|
case DeathCause::DidNotDie:
|
|
return "<invalid cause (child did not die)>";
|
|
case DeathCause::SetupFailure:
|
|
return "<invalid cause (child failed to set up test environment)>";
|
|
case DeathCause::Unknown:
|
|
return "<invalid cause (cause unknown)>";
|
|
}
|
|
|
|
assert(false && "Unreachable");
|
|
}
|
|
|
|
template <std::size_t N>
|
|
std::string ToString(std::array<DeathCause, N> const& causes) {
|
|
std::stringstream ss;
|
|
ss << "{";
|
|
for (std::size_t i = 0; i != N; ++i) {
|
|
ss << ToString(causes[i]);
|
|
if (i+1 != N)
|
|
ss << ", ";
|
|
}
|
|
ss << "}";
|
|
return ss.str();
|
|
}
|
|
|
|
TEST_NORETURN void StopChildProcess(DeathCause cause) { std::exit(static_cast<int>(cause)); }
|
|
|
|
DeathCause ConvertToDeathCause(int val) {
|
|
if (val < static_cast<int>(DeathCause::VerboseAbort) || val > static_cast<int>(DeathCause::Unknown)) {
|
|
return DeathCause::Unknown;
|
|
}
|
|
return static_cast<DeathCause>(val);
|
|
}
|
|
|
|
enum class Outcome {
|
|
Success,
|
|
UnexpectedCause,
|
|
UnexpectedErrorMessage,
|
|
InvalidCause,
|
|
};
|
|
|
|
std::string ToString(Outcome outcome) {
|
|
switch (outcome) {
|
|
case Outcome::Success:
|
|
return "success";
|
|
case Outcome::UnexpectedCause:
|
|
return "unexpected death cause";
|
|
case Outcome::UnexpectedErrorMessage:
|
|
return "unexpected error message";
|
|
case Outcome::InvalidCause:
|
|
return "invalid death cause";
|
|
}
|
|
|
|
assert(false && "Unreachable");
|
|
}
|
|
|
|
class DeathTestResult {
|
|
public:
|
|
DeathTestResult() = default;
|
|
DeathTestResult(Outcome set_outcome, DeathCause set_cause, const std::string& set_failure_description = "")
|
|
: outcome_(set_outcome), cause_(set_cause), failure_description_(set_failure_description) {}
|
|
|
|
bool success() const { return outcome() == Outcome::Success; }
|
|
Outcome outcome() const { return outcome_; }
|
|
DeathCause cause() const { return cause_; }
|
|
const std::string& failure_description() const { return failure_description_; }
|
|
|
|
private:
|
|
Outcome outcome_ = Outcome::Success;
|
|
DeathCause cause_ = DeathCause::Unknown;
|
|
std::string failure_description_;
|
|
};
|
|
|
|
class DeathTest {
|
|
public:
|
|
DeathTest() = default;
|
|
DeathTest(DeathTest const&) = delete;
|
|
DeathTest& operator=(DeathTest const&) = delete;
|
|
|
|
template <std::size_t N, class Func>
|
|
DeathTestResult Run(const std::array<DeathCause, N>& expected_causes, Func&& func, const Matcher& matcher) {
|
|
std::signal(SIGABRT, [](int) { StopChildProcess(DeathCause::StdAbort); });
|
|
std::set_terminate([] { StopChildProcess(DeathCause::StdTerminate); });
|
|
|
|
DeathCause cause = Run(func);
|
|
|
|
if (!IsValidCause(cause)) {
|
|
return DeathTestResult(Outcome::InvalidCause, cause, ToString(cause));
|
|
}
|
|
|
|
if (std::find(expected_causes.begin(), expected_causes.end(), cause) == expected_causes.end()) {
|
|
std::stringstream failure_description;
|
|
failure_description //
|
|
<< "Child died, but with a different death cause\n" //
|
|
<< "Expected cause(s): " << ToString(expected_causes) << "\n" //
|
|
<< "Actual cause: " << ToString(cause) << "\n";
|
|
return DeathTestResult(Outcome::UnexpectedCause, cause, failure_description.str());
|
|
}
|
|
|
|
MatchResult match_result = matcher(GetChildStdErr());
|
|
if (!match_result.first) {
|
|
auto failure_description = std::string("Child died, but with a different error message\n") + match_result.second;
|
|
return DeathTestResult(Outcome::UnexpectedErrorMessage, cause, failure_description);
|
|
}
|
|
|
|
return DeathTestResult(Outcome::Success, cause);
|
|
}
|
|
|
|
void PrintFailureDetails(std::string_view failure_description, std::string_view stmt, DeathCause cause) const {
|
|
std::fprintf(
|
|
stderr, "Failure: EXPECT_DEATH( %s ) failed!\n(reason: %s)\n\n", stmt.data(), failure_description.data());
|
|
|
|
if (cause != DeathCause::Unknown) {
|
|
std::fprintf(stderr, "child exit code: %d\n", GetChildExitCode());
|
|
}
|
|
std::fprintf(stderr, "---------- standard err ----------\n%s", GetChildStdErr().c_str());
|
|
std::fprintf(stderr, "\n----------------------------------\n");
|
|
std::fprintf(stderr, "---------- standard out ----------\n%s", GetChildStdOut().c_str());
|
|
std::fprintf(stderr, "\n----------------------------------\n");
|
|
};
|
|
|
|
private:
|
|
int GetChildExitCode() const { return exit_code_; }
|
|
std::string const& GetChildStdOut() const { return stdout_from_child_; }
|
|
std::string const& GetChildStdErr() const { return stderr_from_child_; }
|
|
|
|
template <class Func>
|
|
DeathCause Run(Func&& f) {
|
|
int pipe_res = pipe(stdout_pipe_fd_);
|
|
assert(pipe_res != -1 && "failed to create pipe");
|
|
pipe_res = pipe(stderr_pipe_fd_);
|
|
assert(pipe_res != -1 && "failed to create pipe");
|
|
pid_t child_pid = fork();
|
|
assert(child_pid != -1 && "failed to fork a process to perform a death test");
|
|
child_pid_ = child_pid;
|
|
if (child_pid_ == 0) {
|
|
RunForChild(std::forward<Func>(f));
|
|
assert(false && "unreachable");
|
|
}
|
|
return RunForParent();
|
|
}
|
|
|
|
template <class Func>
|
|
TEST_NORETURN void RunForChild(Func&& f) {
|
|
close(GetStdOutReadFD()); // don't need to read from the pipe in the child.
|
|
close(GetStdErrReadFD());
|
|
auto DupFD = [](int DestFD, int TargetFD) {
|
|
int dup_result = dup2(DestFD, TargetFD);
|
|
if (dup_result == -1)
|
|
StopChildProcess(DeathCause::SetupFailure);
|
|
};
|
|
DupFD(GetStdOutWriteFD(), STDOUT_FILENO);
|
|
DupFD(GetStdErrWriteFD(), STDERR_FILENO);
|
|
|
|
f();
|
|
StopChildProcess(DeathCause::DidNotDie);
|
|
}
|
|
|
|
static std::string ReadChildIOUntilEnd(int FD) {
|
|
std::string error_msg;
|
|
char buffer[256];
|
|
int num_read;
|
|
do {
|
|
while ((num_read = read(FD, buffer, 255)) > 0) {
|
|
buffer[num_read] = '\0';
|
|
error_msg += buffer;
|
|
}
|
|
} while (num_read == -1 && errno == EINTR);
|
|
return error_msg;
|
|
}
|
|
|
|
void CaptureIOFromChild() {
|
|
close(GetStdOutWriteFD()); // no need to write from the parent process
|
|
close(GetStdErrWriteFD());
|
|
stdout_from_child_ = ReadChildIOUntilEnd(GetStdOutReadFD());
|
|
stderr_from_child_ = ReadChildIOUntilEnd(GetStdErrReadFD());
|
|
close(GetStdOutReadFD());
|
|
close(GetStdErrReadFD());
|
|
}
|
|
|
|
DeathCause RunForParent() {
|
|
CaptureIOFromChild();
|
|
|
|
int status_value;
|
|
pid_t result = waitpid(child_pid_, &status_value, 0);
|
|
assert(result != -1 && "there is no child process to wait for");
|
|
|
|
if (WIFEXITED(status_value)) {
|
|
exit_code_ = WEXITSTATUS(status_value);
|
|
return ConvertToDeathCause(exit_code_);
|
|
}
|
|
|
|
if (WIFSIGNALED(status_value)) {
|
|
exit_code_ = WTERMSIG(status_value);
|
|
// `__builtin_trap` generqtes `SIGILL` on x86 and `SIGTRAP` on ARM.
|
|
if (exit_code_ == SIGILL || exit_code_ == SIGTRAP) {
|
|
return DeathCause::Trap;
|
|
}
|
|
}
|
|
|
|
return DeathCause::Unknown;
|
|
}
|
|
|
|
int GetStdOutReadFD() const { return stdout_pipe_fd_[0]; }
|
|
int GetStdOutWriteFD() const { return stdout_pipe_fd_[1]; }
|
|
int GetStdErrReadFD() const { return stderr_pipe_fd_[0]; }
|
|
int GetStdErrWriteFD() const { return stderr_pipe_fd_[1]; }
|
|
|
|
pid_t child_pid_ = -1;
|
|
int exit_code_ = -1;
|
|
int stdout_pipe_fd_[2];
|
|
int stderr_pipe_fd_[2];
|
|
std::string stdout_from_child_;
|
|
std::string stderr_from_child_;
|
|
};
|
|
|
|
#ifdef _LIBCPP_VERSION
|
|
void std::__libcpp_verbose_abort(char const* format, ...) {
|
|
va_list args;
|
|
va_start(args, format);
|
|
|
|
std::fprintf(stderr, "%s\n", Marker);
|
|
std::vfprintf(stderr, format, args);
|
|
std::fprintf(stderr, "%s", Marker);
|
|
|
|
va_end(args);
|
|
|
|
StopChildProcess(DeathCause::VerboseAbort);
|
|
}
|
|
#endif // _LIBCPP_VERSION
|
|
|
|
template <std::size_t N, class Func>
|
|
bool ExpectDeath(const std::array<DeathCause, N>& expected_causes, const char* stmt, Func&& func, const Matcher& matcher) {
|
|
for (auto cause : expected_causes)
|
|
assert(IsValidCause(cause));
|
|
|
|
DeathTest test_case;
|
|
DeathTestResult test_result = test_case.Run(expected_causes, func, matcher);
|
|
if (!test_result.success()) {
|
|
test_case.PrintFailureDetails(test_result.failure_description(), stmt, test_result.cause());
|
|
}
|
|
|
|
return test_result.success();
|
|
}
|
|
|
|
template <class Func>
|
|
bool ExpectDeath(DeathCause expected_cause, const char* stmt, Func&& func, const Matcher& matcher) {
|
|
return ExpectDeath(std::array<DeathCause, 1>{expected_cause}, stmt, func, matcher);
|
|
}
|
|
|
|
template <std::size_t N, class Func>
|
|
bool ExpectDeath(const std::array<DeathCause, N>& expected_causes, const char* stmt, Func&& func) {
|
|
return ExpectDeath(expected_causes, stmt, func, MakeAnyMatcher());
|
|
}
|
|
|
|
template <class Func>
|
|
bool ExpectDeath(DeathCause expected_cause, const char* stmt, Func&& func) {
|
|
return ExpectDeath(std::array<DeathCause, 1>{expected_cause}, stmt, func, MakeAnyMatcher());
|
|
}
|
|
|
|
// clang-format off
|
|
|
|
/// Assert that the specified expression aborts with the expected cause and, optionally, error message.
|
|
#define EXPECT_ANY_DEATH(...) \
|
|
assert(( ExpectDeath(std::array<DeathCause, 4>{DeathCause::VerboseAbort, DeathCause::StdAbort, DeathCause::StdTerminate, DeathCause::Trap}, #__VA_ARGS__, [&]() { __VA_ARGS__; } ) ))
|
|
#define EXPECT_DEATH(...) \
|
|
assert(( ExpectDeath(DeathCause::VerboseAbort, #__VA_ARGS__, [&]() { __VA_ARGS__; } ) ))
|
|
#define EXPECT_DEATH_MATCHES(matcher, ...) \
|
|
assert(( ExpectDeath(DeathCause::VerboseAbort, #__VA_ARGS__, [&]() { __VA_ARGS__; }, matcher) ))
|
|
#define EXPECT_STD_ABORT(...) \
|
|
assert( ExpectDeath(DeathCause::StdAbort, #__VA_ARGS__, [&]() { __VA_ARGS__; }) )
|
|
#define EXPECT_STD_TERMINATE(...) \
|
|
assert( ExpectDeath(DeathCause::StdTerminate, #__VA_ARGS__, __VA_ARGS__) )
|
|
|
|
#if _LIBCPP_HARDENING_MODE == _LIBCPP_HARDENING_MODE_DEBUG
|
|
#define TEST_LIBCPP_ASSERT_FAILURE(expr, message) \
|
|
assert(( ExpectDeath(DeathCause::VerboseAbort, #expr, [&]() { (void)(expr); }, MakeAssertionMessageMatcher(message)) ))
|
|
#else
|
|
#define TEST_LIBCPP_ASSERT_FAILURE(expr, message) \
|
|
assert(( ExpectDeath(DeathCause::Trap, #expr, [&]() { (void)(expr); }) ))
|
|
#endif // _LIBCPP_HARDENING_MODE == _LIBCPP_HARDENING_MODE_DEBUG
|
|
|
|
// clang-format on
|
|
|
|
#endif // TEST_SUPPORT_CHECK_ASSERTION_H
|