[libcxx-commits] [libcxx] [libc++][hardening] Rework how the assertion handler can be overridden. (PR #77883)

Konstantin Varlamov via libcxx-commits libcxx-commits at lists.llvm.org
Fri Jan 12 11:10:05 PST 2024


https://github.com/var-const updated https://github.com/llvm/llvm-project/pull/77883

>From e8e95c57dc5599fc1d389bdce250c3a6cfad46b2 Mon Sep 17 00:00:00 2001
From: Konstantin Varlamov <varconsteq at gmail.com>
Date: Thu, 11 Jan 2024 17:05:58 -0800
Subject: [PATCH 1/3] [libc++][hardening] Rework how the assertion handler can
 be overridden.

Previously there were two ways to override the verbose abort function
which gets called when a hardening assertion is triggered:
- compile-time: define the `_LIBCPP_VERBOSE_ABORT` macro;
- link-time: provide a definition of `__libcpp_verbose_abort` function.

This patch adds a new configure-time approach: a vendor can provide
a path to a custom header file whose contents will get injected into the
build by CMake. The header must provide a definition of the
`_LIBCPP_ASSERTION_HANDLER` macro which is what will get called should
a hardening assertion fail. As of this patch, overriding
`_LIBCPP_VERBOSE_ABORT` will still work, but the previous mechanisms
will be effectively removed in a follow-up patch, making the
configure-time mechanism the sole way of overriding the default handler.

Note that `_LIBCPP_ASSERTION_HANDLER` only gets invoked when a hardening
assertion fails. It does not affect other cases where
`_LIBCPP_VERBOSE_ABORT` is currently used (e.g. when an exception is
thrown in the `-fno-exceptions` mode).

The library provides a default version of the custom header file that
will get used if it's not overridden by the vendor. That allows us to
always test the override mechanism and reduces the difference in
configuration between the pristine version of the library and
a platform-specific version.
---
 libcxx/CMakeLists.txt                           |  7 +++++++
 libcxx/include/CMakeLists.txt                   |  9 +++++++++
 libcxx/include/__assert                         |  4 ++--
 libcxx/include/__assertion_handler.in           | 13 +++++++++++++
 libcxx/test/libcxx/lint/lint_headers.sh.py      |  1 +
 libcxx/utils/generate_iwyu_mapping.py           |  2 ++
 libcxx/utils/libcxx/header_information.py       |  3 +++
 libcxx/vendor/llvm/default_assertion_handler.in | 14 ++++++++++++++
 8 files changed, 51 insertions(+), 2 deletions(-)
 create mode 100644 libcxx/include/__assertion_handler.in
 create mode 100644 libcxx/vendor/llvm/default_assertion_handler.in

diff --git a/libcxx/CMakeLists.txt b/libcxx/CMakeLists.txt
index 75cb63222da35c..b09836a6ab69c8 100644
--- a/libcxx/CMakeLists.txt
+++ b/libcxx/CMakeLists.txt
@@ -69,6 +69,13 @@ if (NOT "${LIBCXX_HARDENING_MODE}" IN_LIST LIBCXX_SUPPORTED_HARDENING_MODES)
   message(FATAL_ERROR
     "Unsupported hardening mode: '${LIBCXX_HARDENING_MODE}'. Supported values are ${LIBCXX_SUPPORTED_HARDENING_MODES}.")
 endif()
+set(LIBCXX_ASSERTION_HANDLER_FILE
+  "${CMAKE_CURRENT_SOURCE_DIR}/vendor/llvm/default_assertion_handler.in"
+  CACHE STRING
+  "Specify the path to a header that contains a custom implementation of the
+   assertion handler that gets invoked when a hardening assertion fails. If
+   provided, the contents of this header will get injected into the library code
+   and override the default assertion handler.")
 option(LIBCXX_ENABLE_RANDOM_DEVICE
   "Whether to include support for std::random_device in the library. Disabling
    this can be useful when building the library for platforms that don't have
diff --git a/libcxx/include/CMakeLists.txt b/libcxx/include/CMakeLists.txt
index 0fe3ab44d2466e..efbce0fee847d7 100644
--- a/libcxx/include/CMakeLists.txt
+++ b/libcxx/include/CMakeLists.txt
@@ -1019,9 +1019,12 @@ foreach(feature LIBCXX_ENABLE_FILESYSTEM LIBCXX_ENABLE_LOCALIZATION LIBCXX_ENABL
 endforeach()
 
 configure_file("__config_site.in" "${LIBCXX_GENERATED_INCLUDE_TARGET_DIR}/__config_site" @ONLY)
+file(READ ${LIBCXX_ASSERTION_HANDLER_FILE} _LIBCPP_ASSERTION_HANDLER_OVERRIDE)
+configure_file("__assertion_handler.in" "${LIBCXX_GENERATED_INCLUDE_TARGET_DIR}/__assertion_handler" @ONLY)
 configure_file("module.modulemap.in" "${LIBCXX_GENERATED_INCLUDE_DIR}/module.modulemap" @ONLY)
 
 set(_all_includes "${LIBCXX_GENERATED_INCLUDE_TARGET_DIR}/__config_site"
+                  "${LIBCXX_GENERATED_INCLUDE_TARGET_DIR}/__assertion_handler"
                   "${LIBCXX_GENERATED_INCLUDE_DIR}/module.modulemap")
 foreach(f ${files})
   set(src "${CMAKE_CURRENT_SOURCE_DIR}/${f}")
@@ -1059,6 +1062,12 @@ if (LIBCXX_INSTALL_HEADERS)
     PERMISSIONS OWNER_READ OWNER_WRITE GROUP_READ WORLD_READ
     COMPONENT cxx-headers)
 
+  # Install the generated __assertion_handler file to the per-target include dir.
+  install(FILES "${LIBCXX_GENERATED_INCLUDE_TARGET_DIR}/__assertion_handler"
+    DESTINATION "${LIBCXX_INSTALL_INCLUDE_TARGET_DIR}"
+    PERMISSIONS OWNER_READ OWNER_WRITE GROUP_READ WORLD_READ
+    COMPONENT cxx-headers)
+
   # Install the generated modulemap file to the generic include dir.
   install(FILES "${LIBCXX_GENERATED_INCLUDE_DIR}/module.modulemap"
     DESTINATION "${LIBCXX_INSTALL_INCLUDE_DIR}"
diff --git a/libcxx/include/__assert b/libcxx/include/__assert
index d4af7e6c7192ab..3c49c96f65a3e4 100644
--- a/libcxx/include/__assert
+++ b/libcxx/include/__assert
@@ -10,8 +10,8 @@
 #ifndef _LIBCPP___ASSERT
 #define _LIBCPP___ASSERT
 
+#include <__assertion_handler>
 #include <__config>
-#include <__verbose_abort>
 
 #if !defined(_LIBCPP_HAS_NO_PRAGMA_SYSTEM_HEADER)
 #  pragma GCC system_header
@@ -20,7 +20,7 @@
 #define _LIBCPP_ASSERT(expression, message)                                                                            \
   (__builtin_expect(static_cast<bool>(expression), 1)                                                                  \
        ? (void)0                                                                                                       \
-       : _LIBCPP_VERBOSE_ABORT(                                                                                        \
+       : _LIBCPP_ASSERTION_HANDLER(                                                                                    \
              "%s:%d: assertion %s failed: %s\n", __builtin_FILE(), __builtin_LINE(), #expression, message))
 
 // TODO: __builtin_assume can currently inhibit optimizations. Until this has been fixed and we can add
diff --git a/libcxx/include/__assertion_handler.in b/libcxx/include/__assertion_handler.in
new file mode 100644
index 00000000000000..59342a3b0be891
--- /dev/null
+++ b/libcxx/include/__assertion_handler.in
@@ -0,0 +1,13 @@
+// -*- C++ -*-
+#ifndef _LIBCPP___ASSERTION_HANDLER
+#define _LIBCPP___ASSERTION_HANDLER
+
+#include <__config>
+
+#if !defined(_LIBCPP_HAS_NO_PRAGMA_SYSTEM_HEADER)
+#  pragma GCC system_header
+#endif
+
+ at _LIBCPP_ASSERTION_HANDLER_OVERRIDE@
+
+#endif // _LIBCPP___ASSERTION_HANDLER
diff --git a/libcxx/test/libcxx/lint/lint_headers.sh.py b/libcxx/test/libcxx/lint/lint_headers.sh.py
index ab237c968da7e1..aee10e13b9560c 100644
--- a/libcxx/test/libcxx/lint/lint_headers.sh.py
+++ b/libcxx/test/libcxx/lint/lint_headers.sh.py
@@ -14,6 +14,7 @@ def exclude_from_consideration(path):
         or path.endswith(".modulemap.in")
         or os.path.basename(path) == "__config"
         or os.path.basename(path) == "__config_site.in"
+        or os.path.basename(path) == "__assertion_handler.in"
         or os.path.basename(path) == "libcxx.imp"
         or os.path.basename(path).startswith("__pstl")
         or not os.path.isfile(path)  # TODO: Remove once PSTL integration is finished
diff --git a/libcxx/utils/generate_iwyu_mapping.py b/libcxx/utils/generate_iwyu_mapping.py
index 343538a6cae481..d5d1577e4a2ad6 100644
--- a/libcxx/utils/generate_iwyu_mapping.py
+++ b/libcxx/utils/generate_iwyu_mapping.py
@@ -43,6 +43,8 @@ def generate_map(include):
         public = []
         if i == "__assert":
             continue
+        elif i == "__assertion_handler.in":
+            continue
         elif i == "__availability":
             continue
         elif i == "__bit_reference":
diff --git a/libcxx/utils/libcxx/header_information.py b/libcxx/utils/libcxx/header_information.py
index 54e18b5ea533dd..326931b0081c00 100644
--- a/libcxx/utils/libcxx/header_information.py
+++ b/libcxx/utils/libcxx/header_information.py
@@ -168,6 +168,9 @@ def is_modulemap_header(header):
     if header == "__config_site":
         return False
 
+    if header == "__assertion_handler":
+        return False
+
     # exclude libc++abi files
     if header in ["cxxabi.h", "__cxxabi_config.h"]:
         return False
diff --git a/libcxx/vendor/llvm/default_assertion_handler.in b/libcxx/vendor/llvm/default_assertion_handler.in
new file mode 100644
index 00000000000000..2422b6dadbe644
--- /dev/null
+++ b/libcxx/vendor/llvm/default_assertion_handler.in
@@ -0,0 +1,14 @@
+// -*- C++ -*-
+#include <__config>
+#include <__verbose_abort>
+
+#if _LIBCPP_HARDENING_MODE == _LIBCPP_HARDENING_MODE_DEBUG
+
+#define _LIBCPP_ASSERTION_HANDLER(...) _LIBCPP_VERBOSE_ABORT(__VA_ARGS__)
+
+#else
+
+// TODO(hardening): in production, trap rather than abort.
+#define _LIBCPP_ASSERTION_HANDLER(...) _LIBCPP_VERBOSE_ABORT(__VA_ARGS__)
+
+#endif // #if _LIBCPP_HARDENING_MODE == _LIBCPP_HARDENING_MODE_DEBUG

>From 86fc352301f8d6503fb3388fd5288a30f1ff6011 Mon Sep 17 00:00:00 2001
From: Konstantin Varlamov <varconsteq at gmail.com>
Date: Thu, 11 Jan 2024 23:28:42 -0800
Subject: [PATCH 2/3] Pass the original error message as the first argument.

---
 libcxx/include/__assert                         | 1 +
 libcxx/vendor/llvm/default_assertion_handler.in | 4 ++--
 2 files changed, 3 insertions(+), 2 deletions(-)

diff --git a/libcxx/include/__assert b/libcxx/include/__assert
index 3c49c96f65a3e4..a56e098b52328b 100644
--- a/libcxx/include/__assert
+++ b/libcxx/include/__assert
@@ -21,6 +21,7 @@
   (__builtin_expect(static_cast<bool>(expression), 1)                                                                  \
        ? (void)0                                                                                                       \
        : _LIBCPP_ASSERTION_HANDLER(                                                                                    \
+             message,                                                                                                  \
              "%s:%d: assertion %s failed: %s\n", __builtin_FILE(), __builtin_LINE(), #expression, message))
 
 // TODO: __builtin_assume can currently inhibit optimizations. Until this has been fixed and we can add
diff --git a/libcxx/vendor/llvm/default_assertion_handler.in b/libcxx/vendor/llvm/default_assertion_handler.in
index 2422b6dadbe644..b72b0be5b5f4b5 100644
--- a/libcxx/vendor/llvm/default_assertion_handler.in
+++ b/libcxx/vendor/llvm/default_assertion_handler.in
@@ -4,11 +4,11 @@
 
 #if _LIBCPP_HARDENING_MODE == _LIBCPP_HARDENING_MODE_DEBUG
 
-#define _LIBCPP_ASSERTION_HANDLER(...) _LIBCPP_VERBOSE_ABORT(__VA_ARGS__)
+#define _LIBCPP_ASSERTION_HANDLER(error_message, ...) ((void)error_message, _LIBCPP_VERBOSE_ABORT(__VA_ARGS__))
 
 #else
 
 // TODO(hardening): in production, trap rather than abort.
-#define _LIBCPP_ASSERTION_HANDLER(...) _LIBCPP_VERBOSE_ABORT(__VA_ARGS__)
+#define _LIBCPP_ASSERTION_HANDLER(error_message, ...) ((void)error_message, _LIBCPP_VERBOSE_ABORT(__VA_ARGS__))
 
 #endif // #if _LIBCPP_HARDENING_MODE == _LIBCPP_HARDENING_MODE_DEBUG

>From 216605f0cf09c7b27361e9e8d61942c595e32eb5 Mon Sep 17 00:00:00 2001
From: Konstantin Varlamov <varconsteq at gmail.com>
Date: Fri, 12 Jan 2024 11:09:31 -0800
Subject: [PATCH 3/3] Update the documentation about overriding the default
 assertion handler.

---
 libcxx/docs/UsingLibcxx.rst | 82 ++++++++++++++-----------------------
 1 file changed, 31 insertions(+), 51 deletions(-)

diff --git a/libcxx/docs/UsingLibcxx.rst b/libcxx/docs/UsingLibcxx.rst
index e1bbf39b9634a3..91d5b148bc12a9 100644
--- a/libcxx/docs/UsingLibcxx.rst
+++ b/libcxx/docs/UsingLibcxx.rst
@@ -158,57 +158,37 @@ provided by the static or shared library, so it is only available when deploying
 the compiled library is sufficiently recent. On older platforms, the program will terminate in an
 unspecified unsuccessful manner, but the quality of diagnostics won't be great.
 
-However, users can also override that mechanism at two different levels. First, the mechanism can be
-overridden at compile time by defining the ``_LIBCPP_VERBOSE_ABORT(format, args...)`` variadic macro.
-When that macro is defined, it will be called with a format string as the first argument, followed by
-a series of arguments to format using printf-style formatting. Compile-time customization may be
-useful to get precise control over code generation, however it is also inconvenient to use in
-some cases. Indeed, compile-time customization of the verbose termination function requires that all
-translation units be compiled with a consistent definition for ``_LIBCPP_VERBOSE_ABORT`` to avoid ODR
-violations, which can add complexity in the build system of users.
-
-Otherwise, if compile-time customization is not necessary, link-time customization of the handler is also
-possible, similarly to how replacing ``operator new`` works. This mechanism trades off fine-grained control
-over the call site where the termination is initiated in exchange for better ergonomics. Link-time
-customization is done by simply defining the following function in exactly one translation unit of your
-program:
-
-.. code-block:: cpp
-
-  void __libcpp_verbose_abort(char const* format, ...)
-
-This mechanism is similar to how one can replace the default definition of ``operator new``
-and ``operator delete``. For example:
-
-.. code-block:: cpp
-
-  // In HelloWorldHandler.cpp
-  #include <version> // must include any libc++ header before defining the function (C compatibility headers excluded)
-
-  void std::__libcpp_verbose_abort(char const* format, ...) {
-    std::va_list list;
-    va_start(list, format);
-    std::vfprintf(stderr, format, list);
-    va_end(list);
-
-    std::abort();
-  }
-
-  // In HelloWorld.cpp
-  #include <vector>
-
-  int main() {
-    std::vector<int> v;
-    int& x = v[0]; // Your termination function will be called here if hardening is enabled.
-  }
-
-Also note that the verbose termination function should never return. Since assertions in libc++
-catch undefined behavior, your code will proceed with undefined behavior if your function is called
-and does return.
-
-Furthermore, exceptions should not be thrown from the function. Indeed, many functions in the
-library are ``noexcept``, and any exception thrown from the termination function will result
-in ``std::terminate`` being called.
+However, vendors can also override that mechanism at CMake configuration time. When a hardening
+assertion fails, the library invokes the ``_LIBCPP_ASSERTION_HANDLER`` macro. A vendor may provide
+a header that contains a custom definition of this macro and specify the path to the header via the
+``LIBCXX_ASSERTION_HANDLER_FILE`` CMake variable. If provided, the contents of this header will be
+injected into library configuration headers, replacing the default implementation. The header must not
+include any standard library headers (directly or transitively) because doing so will almost always
+create a circular dependency.
+
+``_LIBCPP_ASSERTION_HANDLER(error_message, format, args...)`` is a variadic macro that takes the
+following parameters:
+
+* ``error_message`` -- the original error message that explains the hardening failure. In general, it
+  does not contain information about the source location that triggered the failure.
+* ``format`` -- a printf-style format string that contains a general description of the failure with
+  placeholders for the error message as well as details about the source location.
+* ``args...`` -- arguments to substitute in the ``format`` string. The exact order and meaning of the
+  arguments is unspecified and subject to change (but is always in sync with the format string). Note
+  that for convenience, ``args`` contain the error message as well.
+
+Programs that wish to terminate as fast as possible may use the ``error_message`` parameter that
+doesn't require any formatting. Programs that prefer having more information about the failure (such as
+the filename and the line number of the code that triggered the assertion) should use the printf-style
+formatting with ``format`` and ``args...``.
+
+When a hardening assertion fails, it means that the program is about to invoke undefined behavior. For
+this reason, the custom assertion handler is generally expected to terminate the program. If a custom
+assertion handler decides to avoid doing so (e.g. it chooses to log and continue instead), it does so
+at its own risk -- this approach should only be used in non-production builds and with an understanding
+of potential consequences. Furthermore, the custom assertion handler should not throw any exceptions as
+it may be invoked from standard library functions that are marked ``noexcept`` (so throwing will result
+in ``std::terminate`` being called).
 
 Libc++ Configuration Macros
 ===========================



More information about the libcxx-commits mailing list