[libcxx-commits] [libcxx] (Draft to check CI, please ignore) (PR #78539)

Konstantin Varlamov via libcxx-commits libcxx-commits at lists.llvm.org
Wed Jan 17 19:08:49 PST 2024


https://github.com/var-const created https://github.com/llvm/llvm-project/pull/78539

None

>From eef9a28114a8b224a28d72c6f8ceb4112344a78c Mon Sep 17 00:00:00 2001
From: Konstantin Varlamov <varconsteq at gmail.com>
Date: Wed, 17 Jan 2024 19:07:30 -0800
Subject: [PATCH] DRAFT

---
 ...tomize_verbose_abort.compile-time.pass.cpp |   1 +
 ...customize_verbose_abort.link-time.pass.cpp |   1 +
 .../assertions/default_verbose_abort.pass.cpp |   1 +
 libcxx/test/support/check_assertion.h         | 305 ++++++++++++------
 .../vendor/llvm/default_assertion_handler.in  |  11 +-
 5 files changed, 211 insertions(+), 108 deletions(-)

diff --git a/libcxx/test/libcxx/assertions/customize_verbose_abort.compile-time.pass.cpp b/libcxx/test/libcxx/assertions/customize_verbose_abort.compile-time.pass.cpp
index d09881e8cecb9f..d176d8a12875e6 100644
--- a/libcxx/test/libcxx/assertions/customize_verbose_abort.compile-time.pass.cpp
+++ b/libcxx/test/libcxx/assertions/customize_verbose_abort.compile-time.pass.cpp
@@ -8,6 +8,7 @@
 
 // This compile-time customization requires cross-file macros, which doesn't work with modules.
 // UNSUPPORTED: clang-modules-build
+// REQUIRES: libcpp-hardening-mode=debug
 
 // Make sure that we can customize the verbose termination function at compile-time by
 // defining _LIBCPP_VERBOSE_ABORT ourselves. Note that this does not have any
diff --git a/libcxx/test/libcxx/assertions/customize_verbose_abort.link-time.pass.cpp b/libcxx/test/libcxx/assertions/customize_verbose_abort.link-time.pass.cpp
index 219c43874e77db..c82aaea6ba30df 100644
--- a/libcxx/test/libcxx/assertions/customize_verbose_abort.link-time.pass.cpp
+++ b/libcxx/test/libcxx/assertions/customize_verbose_abort.link-time.pass.cpp
@@ -11,6 +11,7 @@
 // We flag uses of the verbose termination function in older dylibs at compile-time to avoid runtime
 // failures when back-deploying.
 // XFAIL: availability-verbose_abort-missing
+// REQUIRES: libcpp-hardening-mode=debug
 
 #include <cstdlib>
 
diff --git a/libcxx/test/libcxx/assertions/default_verbose_abort.pass.cpp b/libcxx/test/libcxx/assertions/default_verbose_abort.pass.cpp
index 870f43da4b8f02..29ef10016c1a37 100644
--- a/libcxx/test/libcxx/assertions/default_verbose_abort.pass.cpp
+++ b/libcxx/test/libcxx/assertions/default_verbose_abort.pass.cpp
@@ -7,6 +7,7 @@
 //===----------------------------------------------------------------------===//
 
 // Test that the default verbose termination function aborts the program.
+// REQUIRES: libcpp-hardening-mode=debug
 
 #include <csignal>
 #include <cstdlib>
diff --git a/libcxx/test/support/check_assertion.h b/libcxx/test/support/check_assertion.h
index 83a46548fa9250..862e212806a9fd 100644
--- a/libcxx/test/support/check_assertion.h
+++ b/libcxx/test/support/check_assertion.h
@@ -16,6 +16,7 @@
 #include <cstdlib>
 #include <exception>
 #include <regex>
+#include <sstream>
 #include <string>
 #include <string_view>
 #include <utility>
@@ -31,33 +32,70 @@
 #endif
 
 struct AssertionInfoMatcher {
+  // 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 = "###";
+
   static const int any_line = -1;
   static constexpr const char* any_file = "*";
   static constexpr const char* any_msg = "*";
 
-  constexpr AssertionInfoMatcher() : is_empty_(true), msg_(any_msg, __builtin_strlen(any_msg)), file_(any_file, __builtin_strlen(any_file)), line_(any_line) { }
+  constexpr AssertionInfoMatcher()
+      : msg_(any_msg, __builtin_strlen(any_msg)),
+        file_(any_file, __builtin_strlen(any_file)),
+        line_(any_line) {}
   constexpr AssertionInfoMatcher(const char* msg, const char* file = any_file, int line = any_line)
-    : is_empty_(false), msg_(msg, __builtin_strlen(msg)), file_(file, __builtin_strlen(file)), line_(line) {}
+      : is_empty_(false), msg_(msg, __builtin_strlen(msg)), file_(file, __builtin_strlen(file)), line_(line) {}
+
+  bool CheckMatchInOutput(const std::string& output, std::string& error) const {
+    // Extract information from the error message. This has to stay synchronized with how we format assertions in the
+    // library.
+    std::regex message_format(".*###\\n(.*):(\\d+): assertion (.*) failed: (.*)\\n###");
+
+    std::smatch match_result;
+    bool has_match = std::regex_match(output, match_result, message_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& failure_reason = match_result[4];
+
+    bool result = Matches(file, line, failure_reason);
+    if (!result) {
+      error = FormatMatchingError(file, line, failure_reason);
+    }
+    return result;
+  }
 
-  bool Matches(char const* file, int line, char const* message) const {
-    assert(!empty() && "empty matcher");
+  bool Matches(const std::string& file, int line, const std::string& message) const {
+    assert(!empty() && "Empty matcher");
+    return CheckLineMatches(line) && CheckFileMatches(file) && CheckMessageMatches(message);
+  }
 
-    if (CheckLineMatches(line) && CheckFileMatches(file) && CheckMessageMatches(message))
-        return true;
-    // Write to stdout because that's the file descriptor captured by the parent
-    // process.
-    std::printf("Failed to match assertion info!\n%s\nVS\n%s:%d (%s)\n", ToString().data(), file, line, message);
-    return false;
+  std::string FormatMatchingError(const std::string& file, int line, const std::string& message) const {
+    std::stringstream output;
+    output //
+        << "Expected message:   '" << msg_.data() << "'\n" //
+        << "Actual message:     '" << message.c_str() << "'\n" //
+        << "Expected location:   " << FormatLocation(file_, line_) << "\n" //
+        << "Actual location:     " << FormatLocation(file, line) << "\n";
+    return output.str();
   }
 
-  std::string ToString() const {
-    std::string result = "msg = \""; result += msg_; result += "\"\n";
-    result += "line = " + (line_ == any_line ? "'*'" : std::to_string(line_)) + "\n";
-    result += "file = " + (file_ == any_file ? "'*'" : std::string(file_));
+  static std::string FormatLocation(std::string_view file, int line) {
+    std::string result;
+    result += (file == any_file ? "*" : std::string(file)) + ":";
+    result += (line == any_line ? "*" : std::to_string(line));
     return result;
   }
 
   bool empty() const { return is_empty_; }
+  bool IsAnyMatcher() const {
+    return msg_ == any_msg && file_ == any_file && line_ == any_line;
+  }
+
 private:
   bool CheckLineMatches(int got_line) const {
     if (line_ == any_line)
@@ -90,10 +128,12 @@ struct AssertionInfoMatcher {
     std::size_t found_at = got_msg.find(msg_);
     if (found_at == std::string_view::npos)
       return false;
-    return found_at == 0 && got_msg.size() == msg_.size();
+    // Allow any match
+    return true;
   }
+
 private:
-  bool is_empty_;
+  bool is_empty_ = true;;
   std::string_view msg_;
   std::string_view file_;
   int line_;
@@ -101,37 +141,59 @@ struct AssertionInfoMatcher {
 
 static constexpr AssertionInfoMatcher AnyMatcher(AssertionInfoMatcher::any_msg);
 
-inline AssertionInfoMatcher& GlobalMatcher() {
-  static AssertionInfoMatcher GMatch;
-  return GMatch;
-}
+enum class DeathCause {
+  // Valid causes
+  VerboseAbort = 1,
+  StdTerminate,
+  Trap,
+  // Errors
+  DidNotDie,
+  SetupFailure,
+  Unknown
+};
 
-struct DeathTest {
-  enum ResultKind {
-    RK_DidNotDie, RK_MatchFound, RK_MatchFailure, RK_Terminate, RK_SetupFailure, RK_Unknown
-  };
+bool IsValidCause(DeathCause cause) {
+  switch (cause) {
+    case DeathCause::VerboseAbort:
+    case DeathCause::StdTerminate:
+    case DeathCause::Trap:
+      return true;
+    default:
+      return false;
+  }
+}
 
-  static const char* ResultKindToString(ResultKind RK) {
-#define CASE(K) case K: return #K
-    switch (RK) {
-    CASE(RK_MatchFailure);
-    CASE(RK_DidNotDie);
-    CASE(RK_SetupFailure);
-    CASE(RK_MatchFound);
-    CASE(RK_Unknown);
-    CASE(RK_Terminate);
-    }
-    return "not a result kind";
+std::string ToString(DeathCause cause) {
+  switch (cause) {
+    case DeathCause::VerboseAbort:
+      return "verbose abort";
+    case DeathCause::StdTerminate:
+      return "`std::terminate`";
+    case DeathCause::Trap:
+      return "trap";
+    default:
+      return "<invalid cause>";
   }
+}
+
+TEST_NORETURN void StopChildProcess(DeathCause cause) {
+  std::exit(static_cast<int>(cause));
+}
 
-  static bool IsValidResultKind(int val) {
-    return val >= RK_DidNotDie && val <= RK_Unknown;
+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);
+}
 
-  DeathTest(AssertionInfoMatcher const& Matcher) : matcher_(Matcher) {}
+struct DeathTest {
+  DeathTest() = default;
+  DeathTest(DeathTest const&) = delete;
+  DeathTest& operator=(DeathTest const&) = delete;
 
   template <class Func>
-  ResultKind Run(Func&& f) {
+  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_);
@@ -150,6 +212,7 @@ struct DeathTest {
   int getChildExitCode() const { return exit_code_; }
   std::string const& getChildStdOut() const { return stdout_from_child_; }
   std::string const& getChildStdErr() const { return stderr_from_child_; }
+
 private:
   template <class Func>
   TEST_NORETURN void RunForChild(Func&& f) {
@@ -158,14 +221,13 @@ struct DeathTest {
     auto DupFD = [](int DestFD, int TargetFD) {
       int dup_result = dup2(DestFD, TargetFD);
       if (dup_result == -1)
-        std::exit(RK_SetupFailure);
+        StopChildProcess(DeathCause::SetupFailure);
     };
     DupFD(GetStdOutWriteFD(), STDOUT_FILENO);
     DupFD(GetStdErrWriteFD(), STDERR_FILENO);
 
-    GlobalMatcher() = matcher_;
     f();
-    std::exit(RK_DidNotDie);
+    StopChildProcess(DeathCause::DidNotDie);
   }
 
   static std::string ReadChildIOUntilEnd(int FD) {
@@ -190,7 +252,7 @@ struct DeathTest {
     close(GetStdErrReadFD());
   }
 
-  ResultKind RunForParent() {
+  DeathCause RunForParent() {
     CaptureIOFromChild();
 
     int status_value;
@@ -199,15 +261,18 @@ struct DeathTest {
 
     if (WIFEXITED(status_value)) {
       exit_code_ = WEXITSTATUS(status_value);
-      if (!IsValidResultKind(exit_code_))
-        return RK_Unknown;
-      return static_cast<ResultKind>(exit_code_);
+      return ConvertToDeathCause(exit_code_);
     }
-    return RK_Unknown;
-  }
 
-  DeathTest(DeathTest const&) = delete;
-  DeathTest& operator=(DeathTest const&) = delete;
+    if (WIFSIGNALED(status_value)) {
+      exit_code_ = WTERMSIG(status_value);
+      if (exit_code_ == SIGILL || exit_code_ == SIGTRAP) {
+        return DeathCause::Trap;
+      }
+    }
+
+    return DeathCause::Unknown;
+  }
 
   int GetStdOutReadFD() const {
     return stdout_pipe_fd_[0];
@@ -225,7 +290,6 @@ struct DeathTest {
     return stderr_pipe_fd_[1];
   }
 private:
-  AssertionInfoMatcher matcher_;
   pid_t child_pid_ = -1;
   int exit_code_ = -1;
   int stdout_pipe_fd_[2];
@@ -235,82 +299,111 @@ struct DeathTest {
 };
 
 #ifdef _LIBCPP_VERSION
-void std::__libcpp_verbose_abort(char const* printf_format, ...) {
-  // Extract information from the error message. This has to stay synchronized with how we format assertions in the
-  // library.
+void std::__libcpp_verbose_abort(char const* format, ...) {
   va_list args;
-  va_start(args, printf_format);
-  char const* message = va_arg(args, char const*);
-
-  std::regex message_format("(.*):(\\d+): assertion (.*) failed: (.*)\\n");
+  va_start(args, format);
 
-  std::cmatch match_result;
-  bool has_match = std::regex_match(message, match_result, message_format);
-  assert(has_match);
-  assert(match_result.size() == 5);
+  std::fprintf(stderr, "%s\n", AssertionInfoMatcher::Marker);
+  std::vfprintf(stderr, format, args);
+  std::fprintf(stderr, "%s", AssertionInfoMatcher::Marker);
 
-  std::string file = match_result[1];
-  int line         = std::stoi(match_result[2]);
-  // Omitting `expression` in `match_result[3]`
-  std::string failure_reason = match_result[4];
+  va_end(args);
 
-  if (GlobalMatcher().Matches(file.c_str(), line, failure_reason.c_str())) {
-    std::exit(DeathTest::RK_MatchFound);
-  }
-  std::exit(DeathTest::RK_MatchFailure);
+  StopChildProcess(DeathCause::VerboseAbort);
 }
 #endif // _LIBCPP_VERSION
 
 [[noreturn]] inline void terminate_handler() {
-  std::exit(DeathTest::RK_Terminate);
+  StopChildProcess(DeathCause::StdTerminate);
 }
 
 template <class Func>
-inline bool ExpectDeath(const char* stmt, Func&& func, AssertionInfoMatcher Matcher) {
+inline bool ExpectDeath(DeathCause expected_cause, const char* stmt, Func&& func, AssertionInfoMatcher matcher) {
+  assert(IsValidCause(expected_cause));
+
   std::set_terminate(terminate_handler);
-  DeathTest DT(Matcher);
-  DeathTest::ResultKind RK = DT.Run(func);
-  auto OnFailure = [&](const char* msg) {
-    std::fprintf(stderr, "EXPECT_DEATH( %s ) failed! (%s)\n\n", stmt, msg);
-    if (RK != DeathTest::RK_Unknown) {
-      std::fprintf(stderr, "child exit code: %d\n", DT.getChildExitCode());
-    }
-    if (!DT.getChildStdErr().empty()) {
-      std::fprintf(stderr, "---------- standard err ----------\n%s\n", DT.getChildStdErr().c_str());
-    }
-    if (!DT.getChildStdOut().empty()) {
-      std::fprintf(stderr, "---------- standard out ----------\n%s\n", DT.getChildStdOut().c_str());
+
+  DeathTest test_case;
+  DeathCause cause = test_case.Run(func);
+
+  auto OnFailure = [&](std::string_view msg) {
+    std::fprintf(stderr, "Failure: EXPECT_DEATH( %s ) failed!\n(cause: %s)\n\n", stmt, msg.data());
+
+    if (cause != DeathCause::Unknown) {
+      std::fprintf(stderr, "child exit code: %d\n", test_case.getChildExitCode());
     }
+    std::fprintf(stderr, "---------- standard err ----------\n%s", test_case.getChildStdErr().c_str());
+    std::fprintf(stderr, "\n----------------------------------\n");
+    std::fprintf(stderr, "---------- standard out ----------\n%s", test_case.getChildStdOut().c_str());
+    std::fprintf(stderr, "\n----------------------------------\n");
+
     return false;
   };
-  switch (RK) {
-  case DeathTest::RK_MatchFound:
-  case DeathTest::RK_Terminate:
-    return true;
-  case DeathTest::RK_SetupFailure:
-    return OnFailure("child failed to setup test environment");
-  case DeathTest::RK_Unknown:
-      return OnFailure("reason unknown");
-  case DeathTest::RK_DidNotDie:
-      return OnFailure("child did not die");
-  case DeathTest::RK_MatchFailure:
-      return OnFailure("matcher failed");
+
+  auto CauseMismatchMessage = [](DeathCause expected, DeathCause actual) {
+    std::stringstream output;
+    output                                                    //
+        << "Child died, but with a different death cause\n"   //
+        << "Expected cause:   " << ToString(expected) << "\n" //
+        << "Actual cause:     " << ToString(actual) << "\n";
+    return output.str();
+  };
+
+  switch (cause) {
+    case DeathCause::StdTerminate:
+    case DeathCause::Trap:
+      if (expected_cause != cause) {
+        return OnFailure(CauseMismatchMessage(expected_cause, cause));
+      }
+      return true;
+
+    case DeathCause::VerboseAbort: {
+      if (expected_cause != cause) {
+        return OnFailure(CauseMismatchMessage(expected_cause, cause));
+      }
+      std::string maybe_error;
+      if (matcher.CheckMatchInOutput(test_case.getChildStdErr(), maybe_error)) {
+        return true;
+      }
+      maybe_error = std::string("Child died, but with a different verbose abort message\n") + maybe_error;
+      return OnFailure(maybe_error);
+    }
+
+    // Unexpected causes.
+    case DeathCause::SetupFailure:
+      return OnFailure("Child failed to set up test environment");
+    case DeathCause::Unknown:
+      return OnFailure("Cause unknown");
+    case DeathCause::DidNotDie:
+      return OnFailure("Child did not die");
   }
+
   assert(false && "unreachable");
 }
 
 template <class Func>
-inline bool ExpectDeath(const char* stmt, Func&& func) {
-  return ExpectDeath(stmt, func, AnyMatcher);
+inline bool ExpectDeath(DeathCause expected_cause, const char* stmt, Func&& func) {
+  return ExpectDeath(expected_cause, stmt, func, AnyMatcher);
 }
 
-/// Assert that the specified expression throws a libc++ debug exception.
-#define EXPECT_DEATH(...) assert((ExpectDeath(#__VA_ARGS__, [&]() { __VA_ARGS__; } )))
-
-#define EXPECT_STD_TERMINATE(...) assert(ExpectDeath(#__VA_ARGS__, __VA_ARGS__))
-
-#define EXPECT_DEATH_MATCHES(Matcher, ...) assert((ExpectDeath(#__VA_ARGS__, [&]() { __VA_ARGS__; }, Matcher)))
-
-#define TEST_LIBCPP_ASSERT_FAILURE(expr, message) assert((ExpectDeath(#expr, [&]() { (void)(expr); }, AssertionInfoMatcher(message))))
+// clang-format off
+
+/// Assert that the specified expression aborts with the expected cause and, optionally, error message.
+#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_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); }, AssertionInfoMatcher(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
diff --git a/libcxx/vendor/llvm/default_assertion_handler.in b/libcxx/vendor/llvm/default_assertion_handler.in
index 111d305a16f7c4..60bbf27489097b 100644
--- a/libcxx/vendor/llvm/default_assertion_handler.in
+++ b/libcxx/vendor/llvm/default_assertion_handler.in
@@ -17,7 +17,14 @@
 #  pragma GCC system_header
 #endif
 
-// TODO(hardening): in production, trap rather than abort.
-#define _LIBCPP_ASSERTION_HANDLER(message) _LIBCPP_VERBOSE_ABORT("%s", message)
+#if _LIBCPP_HARDENING_MODE == _LIBCPP_HARDENING_MODE_DEBUG
+
+#  define _LIBCPP_ASSERTION_HANDLER(message) _LIBCPP_VERBOSE_ABORT("%s", message)
+
+#else
+
+#  define _LIBCPP_ASSERTION_HANDLER(message) ((void)message, __builtin_trap())
+
+#endif // _LIBCPP_HARDENING_MODE == _LIBCPP_HARDENING_MODE_DEBUG
 
 #endif // _LIBCPP___ASSERTION_HANDLER



More information about the libcxx-commits mailing list