Skip to content

Commit

Permalink
Update for libc++ 18
Browse files Browse the repository at this point in the history
Summary:
libc++ 18 adds `__cxa_init_primary_exception` and uses it to speed up
`std::make_exception_ptr`, so `exception_ptr` objects created by it
would not have stack traces attached (since they bypassed `__cxa_throw`,
which Lyra was using to attach the stack traces). `__cxa_throw` calls
`__cxa_init_primary_exception`, so we can hook the new function instead
where supported. Change Lyra's APIs a bit to abstract the choice of
hooking function.

Reviewed By: yozhu, thevinster

Differential Revision: D58622936

fbshipit-source-id: c7a8213c8cbcce847b70002130f133da07852b9f
  • Loading branch information
smeenai authored and facebook-github-bot committed Jun 17, 2024
1 parent 231bed1 commit 4ec7ce4
Show file tree
Hide file tree
Showing 2 changed files with 83 additions and 34 deletions.
87 changes: 67 additions & 20 deletions cxx/lyra/cxa_throw.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@
#include <mutex>
#include <stdexcept>
#include <unordered_map>
#ifndef _WIN32
#include <cxxabi.h>
#endif

#include <lyra/lyra_exceptions.h>

Expand Down Expand Up @@ -46,30 +49,28 @@ void enableCxaThrowHookBacktraces(bool enable) {
enableBacktraces.store(enable, std::memory_order_relaxed);
}

[[gnu::noreturn]] __attribute__((annotate("dynamic_fn_ptr"))) void (
*original_cxa_throw)(void*, const std::type_info*, void (*)(void*));

// We want to attach stack traces to C++ exceptions. Our API contract is that
// calling lyra::getExceptionTrace on the exception_ptr for an exception should
// return the stack trace for that exception.
//
// We accomplish this by providing a hook for __cxa_throw, which creates an
// We accomplish this by providing a hook for __cxa_init_primary_exception or
// __cxa_throw (depending on the libc++ version), which creates an
// ExceptionTraceHolder object (which captures the stack trace for the
// exception), and creates a mapping from the pointer to the exception object
// (which is a parameter to __cxa_throw) to its ExceptionTraceHolder object.
// This mapping can then be queried by lyra::getExceptionTrace to get the stack
// trace for the exception. We have a custom exception destructor to destroy the
// trace object and call the original destructor for the exception object, and
// our __cxa_throw hook calls the original __cxa_throw to perform the actual
// exception throwing and passes this custom destructor.
// (which is a function parameter) to its ExceptionTraceHolder object. This
// mapping can then be queried by lyra::getExceptionTrace to get the stack trace
// for the exception. We have a custom exception destructor to destroy the trace
// object and call the original destructor for the exception object, and our
// hook calls the original function for the actual exception functionality and
// passes this custom destructor.
//
// This works because __cxa_throw is only called when creating a new exception
// object (that has been freshly allocated via __cxa_allocate_exception), so at
// that point, we're able to capture the original stack trace for the exception.
// Even if that exception is later rethrown, we'll still maintain its original
// stack trace, assuming that std::current_exception creates a reference to the
// current exception instead of copying it (which is true for both libstdc++ and
// libc++), such that we can still look up the exception object via pointer.
// This works because the hooked function is only called when creating a new
// exception object, so at that point, we're able to capture the original stack
// trace for the exception. Even if that exception is later rethrown, we'll
// still maintain its original stack trace, assuming that std::current_exception
// creates a reference to the current exception instead of copying it (which is
// true for both libstdc++ and libc++), such that we can still look up the
// exception object via pointer.
//
// We don't have to worry about any pointer adjustments for the exception object
// (e.g. for converting to or from a base class subobject pointer), because a
Expand Down Expand Up @@ -122,19 +123,65 @@ void trace_destructor(void* exception_obj) {
original_destructor(exception_obj);
}
}
} // namespace

[[noreturn]] void
cxa_throw(void* obj, const std::type_info* type, destructor_type destructor) {
// always_inline to avoid an unnecessary stack frame in the trace.
[[gnu::always_inline]]
void add_exception_trace(void* obj, destructor_type destructor) {
if (enableBacktraces.load(std::memory_order_relaxed)) {
std::lock_guard<std::mutex> lock(*get_exception_state_map_mutex());
get_exception_state_map()->emplace(
obj, ExceptionState{ExceptionTraceHolder(), destructor});
}
}
} // namespace

#ifndef _WIN32
#if _LIBCPP_AVAILABILITY_HAS_INIT_PRIMARY_EXCEPTION
__attribute__((annotate("dynamic_fn_ptr"))) static abi::__cxa_exception* (
*original_cxa_init_primary_exception)(
void*,
std::type_info*,
destructor_type) = &abi::__cxa_init_primary_exception;

abi::__cxa_exception* cxa_init_primary_exception(
void* obj,
std::type_info* type,
destructor_type destructor) {
add_exception_trace(obj, destructor);
return original_cxa_init_primary_exception(obj, type, trace_destructor);
}

const HookInfo* getHookInfo() {
static const HookInfo info = {
.original =
reinterpret_cast<void**>(&original_cxa_init_primary_exception),
.replacement = reinterpret_cast<void*>(&cxa_init_primary_exception),
};
return &info;
}
#else
[[gnu::noreturn]]
__attribute__((annotate("dynamic_fn_ptr"))) static void (*original_cxa_throw)(
void*,
std::type_info*,
destructor_type) = &abi::__cxa_throw;

[[noreturn]] void
cxa_throw(void* obj, std::type_info* type, destructor_type destructor) {
add_exception_trace(obj, destructor);
original_cxa_throw(obj, type, trace_destructor);
}

const HookInfo* getHookInfo() {
static const HookInfo info = {
.original = reinterpret_cast<void**>(&original_cxa_throw),
.replacement = reinterpret_cast<void*>(&cxa_throw),
};
return &info;
}
#endif
#endif

const ExceptionTraceHolder* detail::getExceptionTraceHolder(
std::exception_ptr ptr) {
{
Expand Down
30 changes: 16 additions & 14 deletions cxx/lyra/lyra_exceptions.h
Original file line number Diff line number Diff line change
Expand Up @@ -79,21 +79,23 @@ void ensureRegisteredTerminateHandler();
std::string toString(std::exception_ptr exceptionPointer);

/**
* lyra's cxa_throw will delegate to the original cxa throw. That pointer must
* be set before lyra::cxa_throw is called.
*
* One example use would be to statically compile against something that
* overrides __cxa_throw. That would look something like:
*
* [[noreturn]] void __cxa_throw(void* obj, const std::type_info* type, void
* (*destructor) (void*)) { static auto initializer = lyra::original_cxa_throw =
* lookupOriginalCxaThrow(); lyra::cxa_throw(obj, type, destructor);
* }
* Lyra needs to hook either __cxa_init_primary_exception or __cxa_throw
* (depending on the libc++ version) in order to inject stack traces. Users are
* required to set up this hooking using the information in this struct:
* - original is a pointer to the address of the exception function to hook.
* This pointer should have the address of the unhooked function stored back
* to it (if the hooking mechanism produces it), so that Lyra can delegate to
* the original function.
* - replacement is the address of Lyra's replacement function. After the
* hooking, all callers to the original function should call the replacement
* function instead.
*/
[[gnu::noreturn]] extern void (
*original_cxa_throw)(void*, const std::type_info*, void (*)(void*));
[[noreturn]] void
cxa_throw(void* obj, const std::type_info* type, void (*destructor)(void*));
struct HookInfo {
void** original;
void* replacement;
};

const HookInfo* getHookInfo();

void enableCxaThrowHookBacktraces(bool enable);

Expand Down

0 comments on commit 4ec7ce4

Please sign in to comment.