| // Licensed to the Apache Software Foundation (ASF) under one | |
| // or more contributor license agreements. See the NOTICE file | |
| // distributed with this work for additional information | |
| // regarding copyright ownership. The ASF licenses this file | |
| // to you under the Apache License, Version 2.0 (the | |
| // "License"); you may not use this file except in compliance | |
| // with the License. You may obtain a copy of the License at | |
| // | |
| // http://www.apache.org/licenses/LICENSE-2.0 | |
| // | |
| // Unless required by applicable law or agreed to in writing, | |
| // software distributed under the License is distributed on an | |
| // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | |
| // KIND, either express or implied. See the License for the | |
| // specific language governing permissions and limitations | |
| // under the License. | |
| // This file is copied from | |
| // https://github.com/ClickHouse/ClickHouse/blob/master/src/Common/StackTrace.cpp | |
| // and modified by Doris | |
| #include "common/stack_trace.h" | |
| #include <fmt/format.h> | |
| #if defined(USE_UNWIND) && USE_UNWIND && defined(__x86_64__) | |
| #include <libunwind.h> | |
| #else | |
| #include <execinfo.h> | |
| #endif | |
| #include <atomic> | |
| #include <filesystem> | |
| #include <limits> | |
| #include <map> | |
| #include <mutex> | |
| #include <sstream> | |
| #include <string_view> | |
| #include <unordered_map> | |
| #include "common/config.h" | |
| #include "common/demangle.h" | |
| #include "common/dwarf.h" | |
| #include "common/elf.h" | |
| #include "common/memory_sanitizer.h" | |
| #include "common/symbol_index.h" | |
| #include "exec/common/hex.h" | |
| #include "util/debug_points.h" | |
| #include "util/string_util.h" | |
| namespace { | |
| /// Currently this variable is set up once on server startup. | |
| /// But we use atomic just in case, so it is possible to be modified at runtime. | |
| std::atomic<bool> show_addresses = true; | |
| // #if defined(__ELF__) && !defined(__FreeBSD__) | |
| // void writePointerHex(const void* ptr, std::stringstream& buf) { | |
| // buf.write("0x", 2); | |
| // char hex_str[2 * sizeof(ptr)]; | |
| // doris::write_hex_uint_lowercase(reinterpret_cast<uintptr_t>(ptr), hex_str); | |
| // buf.write(hex_str, 2 * sizeof(ptr)); | |
| // } | |
| // #endif | |
| bool shouldShowAddress(const void* addr) { | |
| /// If the address is less than 4096, most likely it is a nullptr dereference with offset, | |
| /// and showing this offset is secure nevertheless. | |
| /// NOTE: 4096 is the page size on x86 and it can be different on other systems, | |
| /// but for the purpose of this branch, it does not matter. | |
| if (reinterpret_cast<uintptr_t>(addr) < 4096) { | |
| return true; | |
| } | |
| return show_addresses.load(std::memory_order_relaxed); | |
| } | |
| } // namespace | |
| void StackTrace::setShowAddresses(bool show) { | |
| show_addresses.store(show, std::memory_order_relaxed); | |
| } | |
| std::string SigsegvErrorString(const siginfo_t& info, [[maybe_unused]] const ucontext_t& context) { | |
| using namespace std::string_literals; | |
| std::string address = | |
| info.si_addr == nullptr | |
| ? "NULL pointer"s | |
| : (shouldShowAddress(info.si_addr) ? fmt::format("{}", info.si_addr) : ""s); | |
| const std::string_view access = | |
| #if defined(__x86_64__) && !defined(__FreeBSD__) && !defined(__APPLE__) && !defined(__arm__) && \ | |
| !defined(__powerpc__) | |
| (context.uc_mcontext.gregs[REG_ERR] & 0x02) ? "write" : "read"; | |
| #else | |
| ""; | |
| #endif | |
| std::string_view message; | |
| switch (info.si_code) { | |
| case SEGV_ACCERR: | |
| message = "Attempted access has violated the permissions assigned to the memory area"; | |
| break; | |
| case SEGV_MAPERR: | |
| message = "Address not mapped to object"; | |
| break; | |
| default: | |
| message = "Unknown si_code"; | |
| break; | |
| } | |
| return fmt::format("Address: {}. Access: {}. {}.", std::move(address), access, message); | |
| } | |
| constexpr std::string_view SigbusErrorString(int si_code) { | |
| switch (si_code) { | |
| case BUS_ADRALN: | |
| return "Invalid address alignment."; | |
| case BUS_ADRERR: | |
| return "Non-existent physical address."; | |
| case BUS_OBJERR: | |
| return "Object specific hardware error."; | |
| // Linux specific | |
| #if defined(BUS_MCEERR_AR) | |
| case BUS_MCEERR_AR: | |
| return "Hardware memory error: action required."; | |
| #endif | |
| #if defined(BUS_MCEERR_AO) | |
| case BUS_MCEERR_AO: | |
| return "Hardware memory error: action optional."; | |
| #endif | |
| default: | |
| return "Unknown si_code."; | |
| } | |
| } | |
| constexpr std::string_view SigfpeErrorString(int si_code) { | |
| switch (si_code) { | |
| case FPE_INTDIV: | |
| return "Integer divide by zero."; | |
| case FPE_INTOVF: | |
| return "Integer overflow."; | |
| case FPE_FLTDIV: | |
| return "Floating point divide by zero."; | |
| case FPE_FLTOVF: | |
| return "Floating point overflow."; | |
| case FPE_FLTUND: | |
| return "Floating point underflow."; | |
| case FPE_FLTRES: | |
| return "Floating point inexact result."; | |
| case FPE_FLTINV: | |
| return "Floating point invalid operation."; | |
| case FPE_FLTSUB: | |
| return "Subscript out of range."; | |
| default: | |
| return "Unknown si_code."; | |
| } | |
| } | |
| constexpr std::string_view SigillErrorString(int si_code) { | |
| switch (si_code) { | |
| case ILL_ILLOPC: | |
| return "Illegal opcode."; | |
| case ILL_ILLOPN: | |
| return "Illegal operand."; | |
| case ILL_ILLADR: | |
| return "Illegal addressing mode."; | |
| case ILL_ILLTRP: | |
| return "Illegal trap."; | |
| case ILL_PRVOPC: | |
| return "Privileged opcode."; | |
| case ILL_PRVREG: | |
| return "Privileged register."; | |
| case ILL_COPROC: | |
| return "Coprocessor error."; | |
| case ILL_BADSTK: | |
| return "Internal stack error."; | |
| default: | |
| return "Unknown si_code."; | |
| } | |
| } | |
| std::string signalToErrorMessage(int sig, const siginfo_t& info, | |
| [[maybe_unused]] const ucontext_t& context) { | |
| switch (sig) { | |
| case SIGSEGV: | |
| return SigsegvErrorString(info, context); | |
| case SIGBUS: | |
| return std::string {SigbusErrorString(info.si_code)}; | |
| case SIGILL: | |
| return std::string {SigillErrorString(info.si_code)}; | |
| case SIGFPE: | |
| return std::string {SigfpeErrorString(info.si_code)}; | |
| case SIGTSTP: | |
| return "This is a signal used for debugging purposes by the user."; | |
| default: | |
| return ""; | |
| } | |
| } | |
| static void* getCallerAddress(const ucontext_t& context) { | |
| #if defined(__x86_64__) | |
| /// Get the address at the time the signal was raised from the RIP (x86-64) | |
| #if defined(__FreeBSD__) | |
| return reinterpret_cast<void*>(context.uc_mcontext.mc_rip); | |
| #elif defined(__APPLE__) | |
| return reinterpret_cast<void*>(context.uc_mcontext->__ss.__rip); | |
| #else | |
| return reinterpret_cast<void*>(context.uc_mcontext.gregs[REG_RIP]); | |
| #endif | |
| #elif defined(__APPLE__) && defined(__aarch64__) | |
| return reinterpret_cast<void*>(context.uc_mcontext->__ss.__pc); | |
| #elif defined(__FreeBSD__) && defined(__aarch64__) | |
| return reinterpret_cast<void*>(context.uc_mcontext.mc_gpregs.gp_elr); | |
| #elif defined(__aarch64__) | |
| return reinterpret_cast<void*>(context.uc_mcontext.pc); | |
| #elif defined(__powerpc64__) && defined(__linux__) | |
| return reinterpret_cast<void*>(context.uc_mcontext.gp_regs[PT_NIP]); | |
| #elif defined(__powerpc64__) && defined(__FreeBSD__) | |
| return reinterpret_cast<void*>(context.uc_mcontext.mc_srr0); | |
| #elif defined(__riscv) | |
| return reinterpret_cast<void*>(context.uc_mcontext.__gregs[REG_PC]); | |
| #elif defined(__s390x__) | |
| return reinterpret_cast<void*>(context.uc_mcontext.psw.addr); | |
| #else | |
| return nullptr; | |
| #endif | |
| } | |
| // FIXME: looks like this is used only for Sentry but duplicates the whole algo, maybe replace? | |
| void StackTrace::symbolize(const StackTrace::FramePointers& frame_pointers, | |
| [[maybe_unused]] size_t offset, size_t size, | |
| StackTrace::Frames& frames) { | |
| #if defined(__ELF__) && !defined(__FreeBSD__) | |
| auto symbol_index_ptr = doris::SymbolIndex::instance(); | |
| const doris::SymbolIndex& symbol_index = *symbol_index_ptr; | |
| std::unordered_map<std::string, doris::Dwarf> dwarfs; | |
| for (size_t i = 0; i < offset; ++i) { | |
| frames[i].virtual_addr = frame_pointers[i]; | |
| } | |
| for (size_t i = offset; i < size; ++i) { | |
| StackTrace::Frame& current_frame = frames[i]; | |
| current_frame.virtual_addr = frame_pointers[i]; | |
| const auto* object = symbol_index.findObject(current_frame.virtual_addr); | |
| uintptr_t virtual_offset = object ? uintptr_t(object->address_begin) : 0; | |
| current_frame.physical_addr = | |
| reinterpret_cast<void*>(uintptr_t(current_frame.virtual_addr) - virtual_offset); | |
| if (object) { | |
| current_frame.object = object->name; | |
| if (std::error_code ec; | |
| std::filesystem::exists(current_frame.object.value(), ec) && !ec) { | |
| auto dwarf_it = dwarfs.try_emplace(object->name, object->elf).first; | |
| doris::Dwarf::LocationInfo location; | |
| std::vector<doris::Dwarf::SymbolizedFrame> inline_frames; | |
| if (dwarf_it->second.findAddress(uintptr_t(current_frame.physical_addr), location, | |
| doris::Dwarf::LocationInfoMode::FAST, | |
| inline_frames)) { | |
| current_frame.file = location.file.toString(); | |
| current_frame.line = location.line; | |
| } | |
| } | |
| } else { | |
| current_frame.object = "?"; | |
| } | |
| if (const auto* symbol = symbol_index.findSymbol(current_frame.virtual_addr)) { | |
| current_frame.symbol = demangle(symbol->name); | |
| } else { | |
| current_frame.symbol = "?"; | |
| } | |
| } | |
| #else | |
| for (size_t i = 0; i < size; ++i) frames[i].virtual_addr = frame_pointers[i]; | |
| #endif | |
| } | |
| StackTrace::StackTrace(const ucontext_t& signal_context) { | |
| tryCapture(); | |
| /// This variable from signal handler is not instrumented by Memory Sanitizer. | |
| __msan_unpoison(&signal_context, sizeof(signal_context)); | |
| void* caller_address = getCallerAddress(signal_context); | |
| if (size == 0 && caller_address) { | |
| frame_pointers[0] = caller_address; | |
| size = 1; | |
| } else { | |
| /// Skip excessive stack frames that we have created while finding stack trace. | |
| for (size_t i = 0; i < size; ++i) { | |
| const auto frame_address = reinterpret_cast<uintptr_t>(frame_pointers[i]); | |
| const auto caller_address_value = reinterpret_cast<uintptr_t>(caller_address); | |
| if (caller_address_value != 0 && | |
| (frame_address == caller_address_value || | |
| (caller_address_value < std::numeric_limits<uintptr_t>::max() && | |
| frame_address == caller_address_value + 1))) { | |
| offset = i; | |
| break; | |
| } | |
| } | |
| } | |
| } | |
| void StackTrace::tryCapture() { | |
| int frame_count = 0; | |
| #if defined(USE_UNWIND) && USE_UNWIND && defined(__x86_64__) | |
| frame_count = unw_backtrace(frame_pointers.data(), capacity); | |
| #else | |
| frame_count = backtrace(frame_pointers.data(), capacity); | |
| #endif | |
| size = frame_count > 0 ? static_cast<size_t>(frame_count) : 0; | |
| if (doris::config::enable_debug_points && | |
| doris::DebugPoints::instance()->is_enable("StackTrace::tryCapture.empty_trace")) { | |
| size = 0; | |
| } | |
| __msan_unpoison(frame_pointers.data(), size * sizeof(frame_pointers[0])); | |
| } | |
| /// ClickHouse uses bundled libc++ so type names will be the same on every system thus it's safe to hardcode them | |
| constexpr std::pair<std::string_view, std::string_view> replacements[] = { | |
| {"::__1", ""}, | |
| {"std::basic_string<char, std::char_traits<char>, std::allocator<char>>", "std::string"}}; | |
| std::string collapseNames(std::string&& haystack) { | |
| // TODO: surely there is a written version already for better in place search&replace | |
| for (auto [needle, to] : replacements) { | |
| size_t pos = 0; | |
| while ((pos = haystack.find(needle, pos)) != std::string::npos) { | |
| haystack.replace(pos, needle.length(), to); | |
| pos += to.length(); | |
| } | |
| } | |
| return haystack; | |
| } | |
| struct StackTraceRefTriple { | |
| const StackTrace::FramePointers& pointers; | |
| size_t offset; | |
| size_t size; | |
| std::string_view dwarf_location_info_mode; | |
| }; | |
| struct StackTraceTriple { | |
| StackTrace::FramePointers pointers; | |
| size_t offset; | |
| size_t size; | |
| std::string dwarf_location_info_mode; | |
| }; | |
| template <class T> | |
| concept MaybeRef = std::is_same_v<T, StackTraceTriple> || std::is_same_v<T, StackTraceRefTriple>; | |
| constexpr bool operator<(const MaybeRef auto& left, const MaybeRef auto& right) { | |
| // The same PCs can be rendered with different DWARF detail levels. Keeping the mode in the | |
| // key prevents a cheap disabled rendering from poisoning a later full rendering, and prevents | |
| // full file/line detail from leaking into a disabled request. | |
| return std::tuple {left.pointers, left.size, left.offset, | |
| std::string_view(left.dwarf_location_info_mode)} < | |
| std::tuple {right.pointers, right.size, right.offset, | |
| std::string_view(right.dwarf_location_info_mode)}; | |
| } | |
| static void toStringEveryLineImpl([[maybe_unused]] const std::string dwarf_location_info_mode, | |
| const StackTraceRefTriple& stack_trace, | |
| std::function<void(std::string_view)> callback) { | |
| if (stack_trace.size == 0) { | |
| return callback("<Empty trace>"); | |
| } | |
| #if defined(__ELF__) && !defined(__FreeBSD__) | |
| using enum doris::Dwarf::LocationInfoMode; | |
| doris::Dwarf::LocationInfoMode mode; | |
| auto dwarf_location_info_mode_lower = doris::to_lower(dwarf_location_info_mode); | |
| if (dwarf_location_info_mode_lower == "disabled") { | |
| mode = DISABLED; | |
| } else if (dwarf_location_info_mode_lower == "fast") { | |
| mode = FAST; | |
| } else if (dwarf_location_info_mode_lower == "full") { | |
| mode = FULL; | |
| } else if (dwarf_location_info_mode_lower == "full_with_inline") { | |
| mode = FULL_WITH_INLINE; | |
| } else { | |
| LOG(INFO) << "invalid LocationInfoMode: " << dwarf_location_info_mode; | |
| mode = DISABLED; | |
| } | |
| auto symbol_index_ptr = doris::SymbolIndex::instance(); | |
| const doris::SymbolIndex& symbol_index = *symbol_index_ptr; | |
| std::unordered_map<std::string, doris::Dwarf> dwarfs; | |
| for (size_t i = stack_trace.offset; i < stack_trace.size; ++i) { | |
| std::vector<doris::Dwarf::SymbolizedFrame> inline_frames; | |
| const void* virtual_addr = stack_trace.pointers[i]; | |
| const auto* object = symbol_index.findObject(virtual_addr); | |
| uintptr_t virtual_offset = object ? uintptr_t(object->address_begin) : 0; | |
| const void* physical_addr = | |
| reinterpret_cast<const void*>(uintptr_t(virtual_addr) - virtual_offset); | |
| std::stringstream out; | |
| out << "\t" << i << "# "; | |
| if (i < 10) { // for alignment | |
| out << " "; | |
| } | |
| if (const auto* const symbol = symbol_index.findSymbol(virtual_addr)) { | |
| out << collapseNames(demangle(symbol->name)); | |
| } else { | |
| out << "?"; | |
| } | |
| if (std::error_code ec; object && std::filesystem::exists(object->name, ec) && !ec) { | |
| auto dwarf_it = dwarfs.try_emplace(object->name, object->elf).first; | |
| doris::Dwarf::LocationInfo location; | |
| if (dwarf_it->second.findAddress(uintptr_t(physical_addr), location, mode, | |
| inline_frames)) { | |
| out << " at " << location.file.toString() << ":" << location.line; | |
| } | |
| } | |
| // Do not display the stack address and file name, it is not important. | |
| // if (shouldShowAddress(physical_addr)) { | |
| // out << " @ "; | |
| // writePointerHex(physical_addr, out); | |
| // } | |
| // out << " in " << (object ? object->name : "?"); | |
| callback(out.str()); | |
| for (size_t j = 0; j < inline_frames.size(); ++j) { | |
| const auto& frame = inline_frames[j]; | |
| callback(fmt::format("\t{}.{}. inlined from {}: {}:{}", i, j + 1, | |
| collapseNames(demangle(frame.name)), | |
| frame.location.file.toString(), frame.location.line)); | |
| } | |
| } | |
| #else | |
| for (size_t i = stack_trace.offset; i < stack_trace.size; ++i) | |
| if (const void* const addr = stack_trace.pointers[i]; shouldShowAddress(addr)) | |
| callback(fmt::format("{}. {}", i, addr)); | |
| #endif | |
| } | |
| void StackTrace::toStringEveryLine(std::function<void(std::string_view)> callback) const { | |
| toStringEveryLineImpl("FULL_WITH_INLINE", {frame_pointers, offset, size, "FULL_WITH_INLINE"}, | |
| std::move(callback)); | |
| } | |
| using StackTraceCache = std::map<StackTraceTriple, std::string, std::less<>>; | |
| static StackTraceCache& cacheInstance() { | |
| static StackTraceCache cache; | |
| return cache; | |
| } | |
| static std::mutex stacktrace_cache_mutex; | |
| std::string toStringCached(const StackTrace::FramePointers& pointers, size_t offset, size_t size, | |
| const std::string& dwarf_location_info_mode) { | |
| /// Calculation of stack trace text is extremely slow. | |
| /// We use simple cache because otherwise the server could be overloaded by trash queries. | |
| /// Note that this cache can grow unconditionally, but practically it should be small. | |
| std::lock_guard lock {stacktrace_cache_mutex}; | |
| StackTraceCache& cache = cacheInstance(); | |
| const StackTraceRefTriple key {pointers, offset, size, dwarf_location_info_mode}; | |
| if (auto it = cache.find(key); it != cache.end()) { | |
| return it->second; | |
| } else { | |
| std::stringstream out; | |
| toStringEveryLineImpl(dwarf_location_info_mode, key, | |
| [&](std::string_view str) { out << str << '\n'; }); | |
| return cache | |
| .emplace(StackTraceTriple {pointers, offset, size, dwarf_location_info_mode}, | |
| out.str()) | |
| .first->second; | |
| } | |
| } | |
| std::string StackTrace::toString(int start_pointers_index, | |
| const std::string& dwarf_location_info_mode) const { | |
| // Default delete the first three frame pointers, which are inside the stack_trace.cpp. | |
| start_pointers_index += 3; | |
| if (start_pointers_index < 0 || static_cast<size_t>(start_pointers_index) >= size) { | |
| // Unwind can legitimately return fewer frames than the internal frames we normally hide. | |
| return toStringCached({}, 0, 0, dwarf_location_info_mode); | |
| } | |
| const auto first_frame = static_cast<size_t>(start_pointers_index); | |
| StackTrace::FramePointers frame_pointers_raw {}; | |
| std::copy(frame_pointers.begin() + first_frame, frame_pointers.end(), | |
| frame_pointers_raw.begin()); | |
| return toStringCached(frame_pointers_raw, offset > first_frame ? offset - first_frame : 0, | |
| size - first_frame, dwarf_location_info_mode); | |
| } | |
| std::string StackTrace::toString(void** frame_pointers_raw, size_t offset, size_t size, | |
| const std::string& dwarf_location_info_mode) { | |
| __msan_unpoison(frame_pointers_raw, size * sizeof(*frame_pointers_raw)); | |
| StackTrace::FramePointers frame_pointers {}; | |
| std::copy_n(frame_pointers_raw, size, frame_pointers.begin()); | |
| return toStringCached(frame_pointers, offset, size, dwarf_location_info_mode); | |
| } | |
| void StackTrace::createCache() { | |
| std::lock_guard lock {stacktrace_cache_mutex}; | |
| cacheInstance(); | |
| } | |
| void StackTrace::dropCache() { | |
| std::lock_guard lock {stacktrace_cache_mutex}; | |
| cacheInstance().clear(); | |
| } |