1 /*
2 * Copyright (c) Facebook, Inc. and its affiliates.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 #include <atomic>
18 #include <cassert>
19 #include <mutex>
20 #include <stdexcept>
21 #include <unordered_map>
22 #ifndef _WIN32
23 #include <cxxabi.h>
24 #endif
25
26 #include <lyra/lyra_exceptions.h>
27
28 namespace facebook {
29 namespace lyra {
30
31 using namespace detail;
32
33 namespace {
34 std::atomic<bool> enableBacktraces{true};
35
getExceptionTraceHolderInException(std::exception_ptr ptr)36 const ExceptionTraceHolder* getExceptionTraceHolderInException(
37 std::exception_ptr ptr) {
38 try {
39 std::rethrow_exception(ptr);
40 } catch (const ExceptionTraceHolder& holder) {
41 return &holder;
42 } catch (...) {
43 return nullptr;
44 }
45 }
46 } // namespace
47
enableCxaThrowHookBacktraces(bool enable)48 void enableCxaThrowHookBacktraces(bool enable) {
49 enableBacktraces.store(enable, std::memory_order_relaxed);
50 }
51
52 // We want to attach stack traces to C++ exceptions. Our API contract is that
53 // calling lyra::getExceptionTrace on the exception_ptr for an exception should
54 // return the stack trace for that exception.
55 //
56 // We accomplish this by providing a hook for __cxa_init_primary_exception or
57 // __cxa_throw (depending on the libc++ version), which creates an
58 // ExceptionTraceHolder object (which captures the stack trace for the
59 // exception), and creates a mapping from the pointer to the exception object
60 // (which is a function parameter) to its ExceptionTraceHolder object. This
61 // mapping can then be queried by lyra::getExceptionTrace to get the stack trace
62 // for the exception. We have a custom exception destructor to destroy the trace
63 // object and call the original destructor for the exception object, and our
64 // hook calls the original function for the actual exception functionality and
65 // passes this custom destructor.
66 //
67 // This works because the hooked function is only called when creating a new
68 // exception object, so at that point, we're able to capture the original stack
69 // trace for the exception. Even if that exception is later rethrown, we'll
70 // still maintain its original stack trace, assuming that std::current_exception
71 // creates a reference to the current exception instead of copying it (which is
72 // true for both libstdc++ and libc++), such that we can still look up the
73 // exception object via pointer.
74 //
75 // We don't have to worry about any pointer adjustments for the exception object
76 // (e.g. for converting to or from a base class subobject pointer), because a
77 // std::exception_ptr will always capture the pointer to the exception object
78 // itself and not any subobjects.
79 //
80 // Our map must be global, since exceptions can be transferred across threads.
81 // Consequently, we must use a mutex to guard all map operations.
82
83 typedef void (*destructor_type)(void*);
84
85 namespace {
86 struct ExceptionState {
87 ExceptionTraceHolder trace;
88 destructor_type destructor;
89 };
90
91 // We create our map and mutex as function statics and leak them intentionally,
92 // to ensure they've been initialized before any global constructors and are
93 // also available to use inside any global destructors.
get_exception_state_map()94 std::unordered_map<void*, ExceptionState>* get_exception_state_map() {
95 static auto* exception_state_map =
96 new std::unordered_map<void*, ExceptionState>();
97 return exception_state_map;
98 }
99
get_exception_state_map_mutex()100 std::mutex* get_exception_state_map_mutex() {
101 static auto* exception_state_map_mutex = new std::mutex();
102 return exception_state_map_mutex;
103 }
104
trace_destructor(void * exception_obj)105 void trace_destructor(void* exception_obj) {
106 destructor_type original_destructor = nullptr;
107
108 {
109 std::lock_guard<std::mutex> lock(*get_exception_state_map_mutex());
110 auto* exception_state_map = get_exception_state_map();
111 auto it = exception_state_map->find(exception_obj);
112 if (it == exception_state_map->end()) {
113 // This really shouldn't happen, but if it does, just leaking the trace
114 // and exception object seems better than crashing.
115 return;
116 }
117
118 original_destructor = it->second.destructor;
119 exception_state_map->erase(it);
120 }
121
122 if (original_destructor) {
123 original_destructor(exception_obj);
124 }
125 }
126
127 // always_inline to avoid an unnecessary stack frame in the trace.
128 [[gnu::always_inline]]
add_exception_trace(void * obj,destructor_type destructor)129 void add_exception_trace(void* obj, destructor_type destructor) {
130 if (enableBacktraces.load(std::memory_order_relaxed)) {
131 std::lock_guard<std::mutex> lock(*get_exception_state_map_mutex());
132 get_exception_state_map()->emplace(
133 obj, ExceptionState{ExceptionTraceHolder(), destructor});
134 }
135 }
136 } // namespace
137
138 #ifndef _WIN32
139 #if _LIBCPP_AVAILABILITY_HAS_INIT_PRIMARY_EXCEPTION
140 __attribute__((annotate("dynamic_fn_ptr"))) static abi::__cxa_exception* (
141 *original_cxa_init_primary_exception)(
142 void*,
143 std::type_info*,
144 destructor_type) = &abi::__cxa_init_primary_exception;
145
cxa_init_primary_exception(void * obj,std::type_info * type,destructor_type destructor)146 abi::__cxa_exception* cxa_init_primary_exception(
147 void* obj,
148 std::type_info* type,
149 destructor_type destructor) {
150 add_exception_trace(obj, destructor);
151 return original_cxa_init_primary_exception(obj, type, trace_destructor);
152 }
153
getHookInfo()154 const HookInfo* getHookInfo() {
155 static const HookInfo info = {
156 .original =
157 reinterpret_cast<void**>(&original_cxa_init_primary_exception),
158 .replacement = reinterpret_cast<void*>(&cxa_init_primary_exception),
159 };
160 return &info;
161 }
162 #else
163 [[gnu::noreturn]]
164 __attribute__((annotate("dynamic_fn_ptr"))) static void (*original_cxa_throw)(
165 void*,
166 std::type_info*,
167 destructor_type) = &abi::__cxa_throw;
168
169 [[noreturn]] void
cxa_throw(void * obj,std::type_info * type,destructor_type destructor)170 cxa_throw(void* obj, std::type_info* type, destructor_type destructor) {
171 add_exception_trace(obj, destructor);
172 original_cxa_throw(obj, type, trace_destructor);
173 }
174
getHookInfo()175 const HookInfo* getHookInfo() {
176 static const HookInfo info = {
177 .original = reinterpret_cast<void**>(&original_cxa_throw),
178 .replacement = reinterpret_cast<void*>(&cxa_throw),
179 };
180 return &info;
181 }
182 #endif
183 #endif
184
getExceptionTraceHolder(std::exception_ptr ptr)185 const ExceptionTraceHolder* detail::getExceptionTraceHolder(
186 std::exception_ptr ptr) {
187 {
188 std::lock_guard<std::mutex> lock(*get_exception_state_map_mutex());
189 // The exception object pointer isn't a public member of std::exception_ptr,
190 // and there isn't any public method to get it. However, for both libstdc++
191 // and libc++, it's the first pointer inside the exception_ptr, and we can
192 // rely on the ABI of those libraries to remain stable, so we can just
193 // access it directly.
194 void* exception_obj = *reinterpret_cast<void**>(&ptr);
195 auto* exception_state_map = get_exception_state_map();
196 auto it = exception_state_map->find(exception_obj);
197 if (it != exception_state_map->end()) {
198 return &it->second.trace;
199 }
200 }
201
202 // Fall back to attempting to retrieve the ExceptionTraceHolder directly from
203 // the exception (to support e.g. fbthrow).
204 return getExceptionTraceHolderInException(ptr);
205 }
206
207 } // namespace lyra
208 } // namespace facebook
209