/* Copyright (c) 2018, Google Inc. * * Permission to use, copy, modify, and/or distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ #include "abi_test.h" #include #include #include #include #include #include #include #include #if defined(OPENSSL_X86_64) && defined(SUPPORTS_ABI_TEST) #if defined(OPENSSL_LINUX) && defined(BORINGSSL_HAVE_LIBUNWIND) #define SUPPORTS_UNWIND_TEST #define UNW_LOCAL_ONLY #include #include #include #include #include #include #include #include #include #include #elif defined(OPENSSL_WINDOWS) #define SUPPORTS_UNWIND_TEST OPENSSL_MSVC_PRAGMA(warning(push, 3)) #include #include OPENSSL_MSVC_PRAGMA(warning(pop)) #endif #endif // X86_64 && SUPPORTS_ABI_TEST // FIPS mode breaks unwind tests. See https://crbug.com/boringssl/289. #if defined(BORINGSSL_FIPS) #undef SUPPORTS_UNWIND_TEST #endif namespace abi_test { namespace internal { static bool g_unwind_tests_enabled = false; std::string FixVAArgsString(const char *str) { std::string ret = str; size_t idx = ret.find(','); if (idx == std::string::npos) { return ret + "()"; } size_t idx2 = idx + 1; while (idx2 < ret.size() && ret[idx2] == ' ') { idx2++; } while (idx > 0 && ret[idx - 1] == ' ') { idx--; } return ret.substr(0, idx) + "(" + ret.substr(idx2) + ")"; } #if defined(SUPPORTS_ABI_TEST) // ForEachMismatch calls |func| for each register where |a| and |b| differ. template static void ForEachMismatch(const CallerState &a, const CallerState &b, const Func &func) { #define CALLER_STATE_REGISTER(type, name) \ if (a.name != b.name) { \ func(#name); \ } LOOP_CALLER_STATE_REGISTERS() #undef CALLER_STATE_REGISTER } // ReadUnwindResult adds the results of the most recent unwind test to |out|. static void ReadUnwindResult(Result *out); crypto_word_t RunTrampoline(Result *out, crypto_word_t func, const crypto_word_t *argv, size_t argc, bool unwind) { CallerState state; RAND_bytes(reinterpret_cast(&state), sizeof(state)); unwind &= g_unwind_tests_enabled; CallerState state2 = state; crypto_word_t ret = abi_test_trampoline(func, &state2, argv, argc, unwind); #if defined(OPENSSL_X86_64) || defined(OPENSSL_X86) // Query and clear the direction flag early, so negative tests do not // interfere with |malloc|. bool direction_flag = abi_test_get_and_clear_direction_flag(); #endif // OPENSSL_X86_64 || OPENSSL_X86 *out = Result(); ForEachMismatch(state, state2, [&](const char *reg) { out->errors.push_back(std::string(reg) + " was not restored after return"); }); #if defined(OPENSSL_X86_64) || defined(OPENSSL_X86) // Linux and Windows ABIs for x86 require the direction flag be cleared on // return. (Some OpenSSL assembly preserves it, which is stronger, but we only // require what is specified by the ABI so |CHECK_ABI| works with C compiler // output.) if (direction_flag) { out->errors.emplace_back("Direction flag set after return"); } #endif // OPENSSL_X86_64 || OPENSSL_X86 if (unwind) { ReadUnwindResult(out); } return ret; } #endif // SUPPORTS_ABI_TEST #if defined(SUPPORTS_UNWIND_TEST) // We test unwind metadata by running the function under test with the trap flag // set. This results in |SIGTRAP| and |EXCEPTION_SINGLE_STEP| on Linux and // Windows, respectively. We hande these and verify libunwind or the Windows // unwind APIs unwind successfully. // IsAncestorStackFrame returns true if |a_sp| is an ancestor stack frame of // |b_sp|. static bool IsAncestorStackFrame(crypto_word_t a_sp, crypto_word_t b_sp) { #if defined(OPENSSL_X86_64) // The stack grows down, so ancestor stack frames have higher addresses. return a_sp > b_sp; #else #error "unknown architecture" #endif } // Implement some string formatting utilties. Ideally we would use |snprintf|, // but this is called in a signal handler and |snprintf| is not async-signal- // safe. #if !defined(OPENSSL_WINDOWS) static std::array WordToDecimal( crypto_word_t v) { std::array ret; size_t len = 0; do { ret[len++] = '0' + v % 10; v /= 10; } while (v != 0); for (size_t i = 0; i < len / 2; i++) { std::swap(ret[i], ret[len - 1 - i]); } ret[len] = '\0'; return ret; } #endif // !OPENSSL_WINDOWS static std::array WordToHex( crypto_word_t v) { static const char kHex[] = "0123456789abcdef"; std::array ret; for (size_t i = sizeof(crypto_word_t) - 1; i < sizeof(crypto_word_t); i--) { uint8_t b = v & 0xff; v >>= 8; ret[i * 2] = kHex[b >> 4]; ret[i * 2 + 1] = kHex[b & 0xf]; } ret[sizeof(crypto_word_t) * 2] = '\0'; return ret; } static void StrCatSignalSafeImpl(bssl::Span out) {} template static void StrCatSignalSafeImpl(bssl::Span out, const char *str, Args... args) { BUF_strlcat(out.data(), str, out.size()); StrCatSignalSafeImpl(out, args...); } template static void StrCatSignalSafe(bssl::Span out, Args... args) { if (out.empty()) { return; } out[0] = '\0'; StrCatSignalSafeImpl(out, args...); } template [[noreturn]] static void FatalError(Args... args) { // We cannot use |snprintf| here because it is not async-signal-safe. char buf[512]; StrCatSignalSafe(buf, args..., "\n"); #if defined(OPENSSL_WINDOWS) HANDLE stderr_handle = GetStdHandle(STD_ERROR_HANDLE); if (stderr_handle != INVALID_HANDLE_VALUE) { DWORD unused; WriteFile(stderr_handle, buf, strlen(buf), &unused, nullptr); } #else write(STDERR_FILENO, buf, strlen(buf)); #endif abort(); } class UnwindStatus { public: UnwindStatus() : err_(nullptr) {} explicit UnwindStatus(const char *err) : err_(err) {} bool ok() const { return err_ == nullptr; } const char *Error() const { return err_; } private: const char *err_; }; template class UnwindStatusOr { public: UnwindStatusOr(UnwindStatus status) : status_(status) { assert(!status_.ok()); } UnwindStatusOr(const T &value) : status_(UnwindStatus()), value_(value) {} bool ok() const { return status_.ok(); } const char *Error() const { return status_.Error(); } const T &ValueOrDie(const char *msg = "Unexpected error") const { if (!ok()) { FatalError(msg, ": ", Error()); } return value_; } private: UnwindStatus status_; T value_; }; // UnwindCursor abstracts between libunwind and Windows unwind APIs. It is // async-signal-safe. #if defined(OPENSSL_WINDOWS) class UnwindCursor { public: explicit UnwindCursor(const CONTEXT &ctx) : ctx_(ctx) { starting_ip_ = ctx_.Rip; } crypto_word_t starting_ip() const { return starting_ip_; } // Step unwinds the cursor by one frame. On success, it returns whether there // were more frames to unwind. UnwindStatusOr Step() { bool is_top = is_top_; is_top_ = false; DWORD64 image_base; RUNTIME_FUNCTION *entry = RtlLookupFunctionEntry(ctx_.Rip, &image_base, nullptr); if (entry == nullptr) { // This is a leaf function. Leaf functions do not touch stack or // callee-saved registers, so they may be unwound by simulating a ret. if (!is_top) { return UnwindStatus("leaf function found below the top frame"); } memcpy(&ctx_.Rip, reinterpret_cast(ctx_.Rsp), sizeof(ctx_.Rip)); ctx_.Rsp += 8; return true; } // This is a frame function. Call into the Windows unwinder. void *handler_data; DWORD64 establisher_frame; RtlVirtualUnwind(UNW_FLAG_NHANDLER, image_base, ctx_.Rip, entry, &ctx_, &handler_data, &establisher_frame, nullptr); return ctx_.Rip != 0; } // GetIP returns the instruction pointer at the current frame. UnwindStatusOr GetIP() { return ctx_.Rip; } // GetSP returns the stack pointer at the current frame. UnwindStatusOr GetSP() { return ctx_.Rsp; } // GetCallerState returns the callee-saved registers at the current frame. UnwindStatusOr GetCallerState() { CallerState state; state.rbx = ctx_.Rbx; state.rbp = ctx_.Rbp; state.rdi = ctx_.Rdi; state.rsi = ctx_.Rsi; state.r12 = ctx_.R12; state.r13 = ctx_.R13; state.r14 = ctx_.R14; state.r15 = ctx_.R15; memcpy(&state.xmm6, &ctx_.Xmm6, sizeof(Reg128)); memcpy(&state.xmm7, &ctx_.Xmm7, sizeof(Reg128)); memcpy(&state.xmm8, &ctx_.Xmm8, sizeof(Reg128)); memcpy(&state.xmm9, &ctx_.Xmm9, sizeof(Reg128)); memcpy(&state.xmm10, &ctx_.Xmm10, sizeof(Reg128)); memcpy(&state.xmm11, &ctx_.Xmm11, sizeof(Reg128)); memcpy(&state.xmm12, &ctx_.Xmm12, sizeof(Reg128)); memcpy(&state.xmm13, &ctx_.Xmm13, sizeof(Reg128)); memcpy(&state.xmm14, &ctx_.Xmm14, sizeof(Reg128)); memcpy(&state.xmm15, &ctx_.Xmm15, sizeof(Reg128)); return state; } // ToString returns a human-readable representation of the address the cursor // started at. const char *ToString() { StrCatSignalSafe(starting_ip_buf_, "0x", WordToHex(starting_ip_).data()); return starting_ip_buf_; } private: CONTEXT ctx_; crypto_word_t starting_ip_; char starting_ip_buf_[64]; bool is_top_ = true; }; #else // !OPENSSL_WINDOWS class UnwindCursor { public: explicit UnwindCursor(unw_context_t *ctx) : ctx_(ctx) { int ret = InitAtSignalFrame(&cursor_); if (ret < 0) { FatalError("Error getting unwind context: ", unw_strerror(ret)); } starting_ip_ = GetIP().ValueOrDie("Error getting instruction pointer"); } // Step unwinds the cursor by one frame. On success, it returns whether there // were more frames to unwind. UnwindStatusOr Step() { int ret = unw_step(&cursor_); if (ret < 0) { return UNWError(ret); } return ret != 0; } // GetIP returns the instruction pointer at the current frame. UnwindStatusOr GetIP() { crypto_word_t ip; int ret = GetReg(&ip, UNW_REG_IP); if (ret < 0) { return UNWError(ret); } return ip; } // GetSP returns the stack pointer at the current frame. UnwindStatusOr GetSP() { crypto_word_t sp; int ret = GetReg(&sp, UNW_REG_SP); if (ret < 0) { return UNWError(ret); } return sp; } // GetCallerState returns the callee-saved registers at the current frame. UnwindStatusOr GetCallerState() { CallerState state; int ret = 0; #if defined(OPENSSL_X86_64) ret = ret < 0 ? ret : GetReg(&state.rbx, UNW_X86_64_RBX); ret = ret < 0 ? ret : GetReg(&state.rbp, UNW_X86_64_RBP); ret = ret < 0 ? ret : GetReg(&state.r12, UNW_X86_64_R12); ret = ret < 0 ? ret : GetReg(&state.r13, UNW_X86_64_R13); ret = ret < 0 ? ret : GetReg(&state.r14, UNW_X86_64_R14); ret = ret < 0 ? ret : GetReg(&state.r15, UNW_X86_64_R15); #else #error "unknown architecture" #endif if (ret < 0) { return UNWError(ret); } return state; } // ToString returns a human-readable representation of the address the cursor // started at, using debug information if available. const char *ToString() { // Use a new cursor. |cursor_| has already been unwound, and // |unw_get_proc_name| is slow so we do not sample it unconditionally in the // constructor. unw_cursor_t cursor; unw_word_t off; if (InitAtSignalFrame(&cursor) != 0 || unw_get_proc_name(&cursor, starting_ip_buf_, sizeof(starting_ip_buf_), &off) != 0) { StrCatSignalSafe(starting_ip_buf_, "0x", WordToHex(starting_ip_).data()); return starting_ip_buf_; } size_t len = strlen(starting_ip_buf_); // Print the offset in decimal, to match gdb's disassembly output and ease // debugging. StrCatSignalSafe(bssl::Span(starting_ip_buf_).subspan(len), "+", WordToDecimal(off).data(), " (0x", WordToHex(starting_ip_).data(), ")"); return starting_ip_buf_; } private: static UnwindStatus UNWError(int ret) { assert(ret < 0); const char *msg = unw_strerror(ret); return UnwindStatus(msg == nullptr ? "unknown error" : msg); } int InitAtSignalFrame(unw_cursor_t *cursor) { // Work around a bug in libunwind which breaks rax and rdx recovery. This // breaks functions which temporarily use rax as the CFA register. See // https://git.savannah.gnu.org/gitweb/?p=libunwind.git;a=commit;h=819bf51bbd2da462c2ec3401e8ac9153b6e725e3 OPENSSL_memset(cursor, 0, sizeof(*cursor)); int ret = unw_init_local(cursor, ctx_); if (ret < 0) { return ret; } for (;;) { ret = unw_is_signal_frame(cursor); if (ret < 0) { return ret; } if (ret != 0) { return 0; // Found the signal frame. } ret = unw_step(cursor); if (ret < 0) { return ret; } } } int GetReg(crypto_word_t *out, unw_regnum_t reg) { unw_word_t val; int ret = unw_get_reg(&cursor_, reg, &val); if (ret >= 0) { static_assert(sizeof(crypto_word_t) == sizeof(unw_word_t), "crypto_word_t and unw_word_t are inconsistent"); *out = val; } return ret; } unw_context_t *ctx_; unw_cursor_t cursor_; crypto_word_t starting_ip_; char starting_ip_buf_[64]; }; #endif // OPENSSL_WINDOWS // g_in_trampoline is true if we are in an instrumented |abi_test_trampoline| // call, in the region that triggers |SIGTRAP|. static bool g_in_trampoline = false; // g_unwind_function_done, if |g_in_trampoline| is true, is whether the function // under test has returned. It is undefined otherwise. static bool g_unwind_function_done; // g_trampoline_state, if |g_in_trampoline| is true, is the state the function // under test must preserve. It is undefined otherwise. static CallerState g_trampoline_state; // g_trampoline_sp, if |g_in_trampoline| is true, is the stack pointer of the // trampoline frame. It is undefined otherwise. static crypto_word_t g_trampoline_sp; // kMaxUnwindErrors is the maximum number of unwind errors reported per // function. If a function's unwind tables are wrong, we are otherwise likely to // repeat the same error at multiple addresses. static constexpr size_t kMaxUnwindErrors = 10; // Errors are saved in a signal handler. We use a static buffer to avoid // allocation. static size_t g_num_unwind_errors = 0; struct UnwindError { #if defined(OPENSSL_WINDOWS) crypto_word_t ip; #endif char str[512]; }; static UnwindError g_unwind_errors[kMaxUnwindErrors]; template static void AddUnwindError(UnwindCursor *cursor, Args... args) { if (g_num_unwind_errors >= kMaxUnwindErrors) { return; } #if defined(OPENSSL_WINDOWS) // Windows symbol functions should not be called when handling an // exception. Stash the instruction pointer, to be symbolized later. g_unwind_errors[g_num_unwind_errors].ip = cursor->starting_ip(); StrCatSignalSafe(g_unwind_errors[g_num_unwind_errors].str, args...); #else StrCatSignalSafe(g_unwind_errors[g_num_unwind_errors].str, "unwinding at ", cursor->ToString(), ": ", args...); #endif g_num_unwind_errors++; } static void CheckUnwind(UnwindCursor *cursor) { const crypto_word_t kStartAddress = reinterpret_cast(&abi_test_unwind_start); const crypto_word_t kReturnAddress = reinterpret_cast(&abi_test_unwind_return); const crypto_word_t kStopAddress = reinterpret_cast(&abi_test_unwind_stop); crypto_word_t sp = cursor->GetSP().ValueOrDie("Error getting stack pointer"); crypto_word_t ip = cursor->GetIP().ValueOrDie("Error getting instruction pointer"); if (!g_in_trampoline) { if (ip != kStartAddress) { FatalError("Unexpected SIGTRAP at ", cursor->ToString()); } // Save the current state and begin. g_in_trampoline = true; g_unwind_function_done = false; g_trampoline_sp = sp; g_trampoline_state = cursor->GetCallerState().ValueOrDie( "Error getting initial caller state"); } else { if (sp == g_trampoline_sp || g_unwind_function_done) { // |g_unwind_function_done| should imply |sp| is |g_trampoline_sp|, but // clearing the trap flag in x86 briefly displaces the stack pointer. // // Also note we check both |ip| and |sp| below, in case the function under // test is also |abi_test_trampoline|. if (ip == kReturnAddress && sp == g_trampoline_sp) { g_unwind_function_done = true; } if (ip == kStopAddress && sp == g_trampoline_sp) { // |SIGTRAP| is fatal again. g_in_trampoline = false; } } else if (IsAncestorStackFrame(sp, g_trampoline_sp)) { // This should never happen. We went past |g_trampoline_sp| without // stopping at |kStopAddress|. AddUnwindError(cursor, "stack frame is before caller"); g_in_trampoline = false; } else if (g_num_unwind_errors < kMaxUnwindErrors) { for (;;) { UnwindStatusOr step_ret = cursor->Step(); if (!step_ret.ok()) { AddUnwindError(cursor, "error unwinding: ", step_ret.Error()); break; } // |Step| returns whether there was a frame to unwind. if (!step_ret.ValueOrDie()) { AddUnwindError(cursor, "could not unwind to starting frame"); break; } UnwindStatusOr cur_sp = cursor->GetSP(); if (!cur_sp.ok()) { AddUnwindError(cursor, "error recovering stack pointer: ", cur_sp.Error()); break; } if (IsAncestorStackFrame(cur_sp.ValueOrDie(), g_trampoline_sp)) { AddUnwindError(cursor, "unwound past starting frame"); break; } if (cur_sp.ValueOrDie() == g_trampoline_sp) { // We found the parent frame. Check the return address. UnwindStatusOr cur_ip = cursor->GetIP(); if (!cur_ip.ok()) { AddUnwindError(cursor, "error recovering return address: ", cur_ip.Error()); } else if (cur_ip.ValueOrDie() != kReturnAddress) { AddUnwindError(cursor, "wrong return address"); } // Check the remaining registers. UnwindStatusOr state = cursor->GetCallerState(); if (!state.ok()) { AddUnwindError(cursor, "error recovering registers: ", state.Error()); } else { ForEachMismatch(state.ValueOrDie(), g_trampoline_state, [&](const char *reg) { AddUnwindError(cursor, reg, " was not recovered"); }); } break; } } } } } static void ReadUnwindResult(Result *out) { for (size_t i = 0; i < g_num_unwind_errors; i++) { #if defined(OPENSSL_WINDOWS) const crypto_word_t ip = g_unwind_errors[i].ip; char buf[256]; DWORD64 displacement; struct { SYMBOL_INFO info; char name_buf[128]; } symbol; memset(&symbol, 0, sizeof(symbol)); symbol.info.SizeOfStruct = sizeof(symbol.info); symbol.info.MaxNameLen = sizeof(symbol.name_buf); if (SymFromAddr(GetCurrentProcess(), ip, &displacement, &symbol.info)) { snprintf(buf, sizeof(buf), "unwinding at %s+%llu (0x%s): %s", symbol.info.Name, displacement, WordToHex(ip).data(), g_unwind_errors[i].str); } else { snprintf(buf, sizeof(buf), "unwinding at 0x%s: %s", WordToHex(ip).data(), g_unwind_errors[i].str); } out->errors.emplace_back(buf); #else out->errors.emplace_back(g_unwind_errors[i].str); #endif } if (g_num_unwind_errors == kMaxUnwindErrors) { out->errors.emplace_back("(additional errors omitted)"); } g_num_unwind_errors = 0; } #if defined(OPENSSL_WINDOWS) static DWORD g_main_thread; static long ExceptionHandler(EXCEPTION_POINTERS *info) { if (info->ExceptionRecord->ExceptionCode != EXCEPTION_SINGLE_STEP || GetCurrentThreadId() != g_main_thread) { return EXCEPTION_CONTINUE_SEARCH; } UnwindCursor cursor(*info->ContextRecord); CheckUnwind(&cursor); if (g_in_trampoline) { // Windows clears the trap flag, so we must restore it. info->ContextRecord->EFlags |= 0x100; } return EXCEPTION_CONTINUE_EXECUTION; } static void EnableUnwindTestsImpl() { if (IsDebuggerPresent()) { // Unwind tests drive logic via |EXCEPTION_SINGLE_STEP|, which conflicts with // debuggers. fprintf(stderr, "Debugger detected. Disabling unwind tests.\n"); return; } g_main_thread = GetCurrentThreadId(); SymSetOptions(SYMOPT_DEFERRED_LOADS); if (!SymInitialize(GetCurrentProcess(), nullptr, TRUE)) { fprintf(stderr, "Could not initialize symbols.\n"); } if (AddVectoredExceptionHandler(0, ExceptionHandler) == nullptr) { fprintf(stderr, "Error installing exception handler.\n"); abort(); } g_unwind_tests_enabled = true; } #else // !OPENSSL_WINDOWS // HandleEINTR runs |func| and returns the result, retrying the operation on // |EINTR|. template static auto HandleEINTR(const Func &func) -> decltype(func()) { decltype(func()) ret; do { ret = func(); } while (ret < 0 && errno == EINTR); return ret; } static bool ReadFileToString(std::string *out, const char *path) { out->clear(); int fd = HandleEINTR([&] { return open(path, O_RDONLY); }); if (fd < 0) { return false; } for (;;) { char buf[1024]; ssize_t ret = HandleEINTR([&] { return read(fd, buf, sizeof(buf)); }); if (ret < 0) { close(fd); return false; } if (ret == 0) { close(fd); return true; } out->append(buf, static_cast(ret)); } } static bool IsBeingDebugged() { std::string status; if (!ReadFileToString(&status, "/proc/self/status")) { perror("error reading /proc/self/status"); return false; } std::string key = "\nTracerPid:\t"; size_t idx = status.find(key); if (idx == std::string::npos) { return false; } idx += key.size(); return idx < status.size() && status[idx] != '0'; } static pthread_t g_main_thread; static void TrapHandler(int sig) { // Note this is a signal handler, so only async-signal-safe functions may be // used here. See signal-safety(7). libunwind promises local unwind is // async-signal-safe. // |pthread_equal| is not listed as async-signal-safe, but this is clearly an // oversight. if (!pthread_equal(g_main_thread, pthread_self())) { FatalError("SIGTRAP on background thread"); } unw_context_t ctx; int ret = unw_getcontext(&ctx); if (ret < 0) { FatalError("Error getting unwind context: ", unw_strerror(ret)); } UnwindCursor cursor(&ctx); CheckUnwind(&cursor); } static void EnableUnwindTestsImpl() { if (IsBeingDebugged()) { // Unwind tests drive logic via |SIGTRAP|, which conflicts with debuggers. fprintf(stderr, "Debugger detected. Disabling unwind tests.\n"); return; } g_main_thread = pthread_self(); struct sigaction trap_action; OPENSSL_memset(&trap_action, 0, sizeof(trap_action)); sigemptyset(&trap_action.sa_mask); trap_action.sa_handler = TrapHandler; if (sigaction(SIGTRAP, &trap_action, NULL) != 0) { perror("sigaction"); abort(); } g_unwind_tests_enabled = true; } #endif // OPENSSL_WINDOWS #else // !SUPPORTS_UNWIND_TEST #if defined(SUPPORTS_ABI_TEST) static void ReadUnwindResult(Result *) {} #endif static void EnableUnwindTestsImpl() {} #endif // SUPPORTS_UNWIND_TEST } // namespace internal void EnableUnwindTests() { internal::EnableUnwindTestsImpl(); } bool UnwindTestsEnabled() { return internal::g_unwind_tests_enabled; } } // namespace abi_test