Skip to content

Commit aed54f5

Browse files
smeenaifacebook-github-bot
authored andcommitted
Implement exception stack traces for libc++
Summary: Add an implementation for libc++, which just uses a map from exception objects to their stack traces. This implementation can in theory be used for libstdc++ as well, but I want it to be tested for some time before we make that switch. Reviewed By: cjhopman Differential Revision: D18716738 fbshipit-source-id: 3d9bc50494d196d6695c7974158fb4ce268bc275
1 parent 482ae6d commit aed54f5

File tree

3 files changed

+129
-15
lines changed

3 files changed

+129
-15
lines changed

cxx/lyra/cxa_throw.cpp

Lines changed: 127 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@
1515
*/
1616

1717
#include <atomic>
18+
#include <mutex>
1819
#include <stdexcept>
20+
#include <unordered_map>
1921
#include <cxxabi.h>
2022
#include <unwind.h>
2123
#include <cassert>
@@ -25,9 +27,22 @@
2527
namespace facebook {
2628
namespace lyra {
2729

30+
using namespace detail;
31+
2832
namespace {
2933
std::atomic<bool> enableBacktraces{true};
34+
35+
const ExceptionTraceHolder* getExceptionTraceHolderInException(
36+
std::exception_ptr ptr) {
37+
try {
38+
std::rethrow_exception(ptr);
39+
} catch (const ExceptionTraceHolder& holder) {
40+
return &holder;
41+
} catch (...) {
42+
return nullptr;
43+
}
3044
}
45+
} // namespace
3146

3247
void enableCxaThrowHookBacktraces(bool enable) {
3348
enableBacktraces.store(enable, std::memory_order_relaxed);
@@ -36,13 +51,115 @@ void enableCxaThrowHookBacktraces(bool enable) {
3651
[[gnu::noreturn]] void (*original_cxa_throw)(void*, const std::type_info*, void (*) (void *));
3752

3853
#if defined(_LIBCPP_VERSION)
39-
[[noreturn]] void cxa_throw(void* obj, const std::type_info* type, void (*destructor) (void *)) {
40-
// lyra doesn't have support yet for libc++.
41-
original_cxa_throw(obj, type, destructor);
54+
55+
// We want to attach stack traces to C++ exceptions. Our API contract is that
56+
// calling lyra::getExceptionTrace on the exception_ptr for an exception should
57+
// return the stack trace for that exception.
58+
//
59+
// We accomplish this by providing a hook for __cxa_throw, which creates an
60+
// ExceptionTraceHolder object (which captures the stack trace for the
61+
// exception), and creates a mapping from the pointer to the exception object
62+
// (which is a parameter to __cxa_throw) to its ExceptionTraceHolder object.
63+
// This mapping can then be queried by lyra::getExceptionTrace to get the stack
64+
// trace for the exception. We have a custom exception destructor to destroy the
65+
// trace object and call the original destructor for the exception object, and
66+
// our __cxa_throw hook calls the original __cxa_throw to perform the actual
67+
// exception throwing and passes this custom destructor.
68+
//
69+
// This works because __cxa_throw is only called when creating a new exception
70+
// object (that has been freshly allocated via __cxa_allocate_exception), so at
71+
// that point, we're able to capture the original stack trace for the exception.
72+
// Even if that exception is later rethrown, we'll still maintain its original
73+
// stack trace, assuming that std::current_exception creates a reference to the
74+
// current exception instead of copying it (which is true for both libstdc++ and
75+
// libc++), such that we can still look up the exception object via pointer.
76+
//
77+
// We don't have to worry about any pointer adjustments for the exception object
78+
// (e.g. for converting to or from a base class subobject pointer), because a
79+
// std::exception_ptr will always capture the pointer to the exception object
80+
// itself and not any subobjects.
81+
//
82+
// Our map must be global, since exceptions can be transferred across threads.
83+
// Consequently, we must use a mutex to guard all map operations.
84+
85+
typedef void (*destructor_type)(void*);
86+
87+
namespace {
88+
struct ExceptionState {
89+
ExceptionTraceHolder trace;
90+
destructor_type destructor;
91+
};
92+
93+
// We create our map and mutex as function statics and leak them intentionally,
94+
// to ensure they've been initialized before any global constructors and are
95+
// also available to use inside any global destructors.
96+
std::unordered_map<void*, ExceptionState>* get_exception_state_map() {
97+
static auto* exception_state_map =
98+
new std::unordered_map<void*, ExceptionState>();
99+
return exception_state_map;
42100
}
43-
#else
44101

45-
using namespace detail;
102+
std::mutex* get_exception_state_map_mutex() {
103+
static auto* exception_state_map_mutex = new std::mutex();
104+
return exception_state_map_mutex;
105+
}
106+
107+
void trace_destructor(void* exception_obj) {
108+
destructor_type original_destructor = nullptr;
109+
110+
{
111+
std::lock_guard<std::mutex> lock(*get_exception_state_map_mutex());
112+
auto* exception_state_map = get_exception_state_map();
113+
auto it = exception_state_map->find(exception_obj);
114+
if (it == exception_state_map->end()) {
115+
// This really shouldn't happen, but if it does, just leaking the trace
116+
// and exception object seems better than crashing.
117+
return;
118+
}
119+
120+
original_destructor = it->second.destructor;
121+
exception_state_map->erase(it);
122+
}
123+
124+
if (original_destructor) {
125+
original_destructor(exception_obj);
126+
}
127+
}
128+
} // namespace
129+
130+
[[noreturn]] void
131+
cxa_throw(void* obj, const std::type_info* type, destructor_type destructor) {
132+
if (enableBacktraces.load(std::memory_order_relaxed)) {
133+
std::lock_guard<std::mutex> lock(*get_exception_state_map_mutex());
134+
get_exception_state_map()->emplace(
135+
obj, ExceptionState{ExceptionTraceHolder(), destructor});
136+
}
137+
138+
original_cxa_throw(obj, type, trace_destructor);
139+
}
140+
141+
const ExceptionTraceHolder* detail::getExceptionTraceHolder(
142+
std::exception_ptr ptr) {
143+
{
144+
std::lock_guard<std::mutex> lock(*get_exception_state_map_mutex());
145+
// The exception object pointer isn't a public member of std::exception_ptr,
146+
// and there isn't any public method to get it. However, for both libstdc++
147+
// and libc++, it's the first pointer inside the exception_ptr, and we can
148+
// rely on the ABI of those libraries to remain stable, so we can just
149+
// access it directly.
150+
void* exception_obj = *reinterpret_cast<void**>(&ptr);
151+
auto* exception_state_map = get_exception_state_map();
152+
auto it = exception_state_map->find(exception_obj);
153+
if (it != exception_state_map->end()) {
154+
return &it->second.trace;
155+
}
156+
}
157+
158+
// Fall back to attempting to retrieve the ExceptionTraceHolder directly from
159+
// the exception (to support e.g. fbthrow).
160+
return getExceptionTraceHolderInException(ptr);
161+
}
162+
#else
46163

47164
namespace {
48165

@@ -110,6 +227,11 @@ struct HijackedExceptionTypeInfo : public abi::__class_type_info {
110227
original_cxa_throw(obj, type, destructor);
111228
}
112229

230+
const ExceptionTraceHolder* detail::getExceptionTraceHolder(
231+
std::exception_ptr ptr) {
232+
return getExceptionTraceHolderInException(ptr);
233+
}
234+
113235
#endif // libc++
114236

115237
} // namespace lyra

cxx/lyra/lyra_exceptions.cpp

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -31,16 +31,6 @@ using namespace detail;
3131
namespace {
3232
std::terminate_handler gTerminateHandler;
3333

34-
const ExceptionTraceHolder* getExceptionTraceHolder(std::exception_ptr ptr) {
35-
try {
36-
std::rethrow_exception(ptr);
37-
} catch (const ExceptionTraceHolder& holder) {
38-
return &holder;
39-
} catch (...) {
40-
return nullptr;
41-
}
42-
}
43-
4434
void logExceptionAndAbort() {
4535
if (auto ptr = std::current_exception()) {
4636
FBJNI_LOGE("Uncaught exception: %s", toString(ptr).c_str());

cxx/lyra/lyra_exceptions.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ namespace detail {
4444
struct Holder<E, true> : E {
4545
Holder(E&& e) : E{std::forward<E>(e)} {}
4646
};
47+
48+
const ExceptionTraceHolder* getExceptionTraceHolder(std::exception_ptr ptr);
4749
}
4850

4951
/**

0 commit comments

Comments
 (0)