-
Notifications
You must be signed in to change notification settings - Fork 15.7k
[libc++][hardening] Rework how the assertion handler can be overridden. #77883
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 5 commits
e8e95c5
86fc352
216605f
692aac0
8981ef9
50282f3
deae2b8
d38fe7e
dc9d771
d83f5a1
48db22a
230de80
5dd7588
b6bcc16
1f5cab9
0bef0fb
cfa7d34
6cb68e6
8cc2fff
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -146,9 +146,9 @@ IWYU, you should run the tool like so: | |
| If you would prefer to not use that flag, then you can replace ``/path/to/include-what-you-use/share/libcxx.imp`` | ||
| file with the libc++-provided ``libcxx.imp`` file. | ||
|
|
||
| .. _termination-handler: | ||
| .. _assertion-handler: | ||
var-const marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| Overriding the default termination handler | ||
| Overriding the default assertion handler | ||
| ========================================== | ||
|
|
||
| When the library wants to terminate due to an unforeseen condition (such as a hardening assertion | ||
|
|
@@ -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 | ||
var-const marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| 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 | ||
var-const marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| 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 | ||
| =========================== | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
var-const marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| 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}" | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| // -*- C++ -*- | ||
var-const marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| //===----------------------------------------------------------------------===// | ||
| // | ||
| // 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 _LIBCPP___ASSERTION_HANDLER | ||
| #define _LIBCPP___ASSERTION_HANDLER | ||
|
|
||
| #include <__config> | ||
|
|
||
| #if !defined(_LIBCPP_HAS_NO_PRAGMA_SYSTEM_HEADER) | ||
| # pragma GCC system_header | ||
| #endif | ||
|
|
||
| @_LIBCPP_ASSERTION_HANDLER_OVERRIDE@ | ||
|
|
||
| #endif // _LIBCPP___ASSERTION_HANDLER | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| // -*- C++ -*- | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we want to prefix this filename with
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We need to, if it gets installed into
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sorry, I mean this specific file. During installation, it gets renamed to
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I guess I would leave it without |
||
| //===----------------------------------------------------------------------===// | ||
| // | ||
| // 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 | ||
| // | ||
| //===----------------------------------------------------------------------===// | ||
|
|
||
var-const marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| #include <__config> | ||
| #include <__verbose_abort> | ||
|
|
||
| #if _LIBCPP_HARDENING_MODE == _LIBCPP_HARDENING_MODE_DEBUG | ||
var-const marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| #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(error_message, ...) ((void)error_message, _LIBCPP_VERBOSE_ABORT(__VA_ARGS__)) | ||
|
|
||
| #endif // #if _LIBCPP_HARDENING_MODE == _LIBCPP_HARDENING_MODE_DEBUG | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking at the code it feels somewhat clunky to me.
Why would the following approach not work?
Then in
libcxx/include/CMakeLists.txtyou copy this file and no configuring at all.The current approach would result in a file below or am I missing something.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Injecting the contents of the header should give us a little more control over the custom handler. In particular, it is currently up to the vendor whether they want to allow users to override the macro. To support overriding, a custom implementation might first check that the macro isn't already defined, e.g. on the command line:
That way, a user can compile with
-D_LIBCPP_ASSERTION_HANDLER(...)=my_project::special_assert(__VA_ARGS__)and it will be honored. Or the vendor could do the opposite and explicitly forbid overriding:If in a future release we wanted to make sure that the handler can always be overridden by users, we could change our header to do something like:
We don't necessarily want to do that, but I think leaving us some degree of flexibility in this regard is useful.
Also, this allows us to provide some internal includes for the custom header in a supported manner. E.g. we can make
<__config>available without having the vendor include that internal header explicitly (which we generally don't support), and if in the future we decided to granularize<__config>, we could update the includes without the vendor having to update their code.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@mordante After writing this, I realized that we can just as well do something like:
which alleviates my concern. In fact, I think both approaches have equal expressive power and are not visible to the user/vendor. I think they're very similar, but having a single header seems to be a little simpler/cleaner, so I'll give it a shot based on your suggestion.