diff --git a/.github/workflows/website-build.yml b/.github/workflows/website-build.yml index f25435563..914a8bf62 100644 --- a/.github/workflows/website-build.yml +++ b/.github/workflows/website-build.yml @@ -24,6 +24,7 @@ jobs: -DSOURCEMETA_CORE_LANG_ERROR:BOOL=OFF -DSOURCEMETA_CORE_LANG_OPTIONS:BOOL=OFF -DSOURCEMETA_CORE_LANG_TEXT:BOOL=OFF + -DSOURCEMETA_CORE_LANG_STACKTRACE:BOOL=OFF -DSOURCEMETA_CORE_UNICODE:BOOL=OFF -DSOURCEMETA_CORE_PUNYCODE:BOOL=OFF -DSOURCEMETA_CORE_TIME:BOOL=OFF diff --git a/.github/workflows/website-deploy.yml b/.github/workflows/website-deploy.yml index eee6b3702..38d092c6e 100644 --- a/.github/workflows/website-deploy.yml +++ b/.github/workflows/website-deploy.yml @@ -34,6 +34,7 @@ jobs: -DSOURCEMETA_CORE_LANG_ERROR:BOOL=OFF -DSOURCEMETA_CORE_LANG_OPTIONS:BOOL=OFF -DSOURCEMETA_CORE_LANG_TEXT:BOOL=OFF + -DSOURCEMETA_CORE_LANG_STACKTRACE:BOOL=OFF -DSOURCEMETA_CORE_UNICODE:BOOL=OFF -DSOURCEMETA_CORE_PUNYCODE:BOOL=OFF -DSOURCEMETA_CORE_TIME:BOOL=OFF diff --git a/CMakeLists.txt b/CMakeLists.txt index 79a2302ea..a06d37497 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -11,6 +11,7 @@ option(SOURCEMETA_CORE_LANG_NUMERIC "Build the Sourcemeta Core language numeric option(SOURCEMETA_CORE_LANG_ERROR "Build the Sourcemeta Core language error library" ON) option(SOURCEMETA_CORE_LANG_OPTIONS "Build the Sourcemeta Core Options library" ON) option(SOURCEMETA_CORE_LANG_TEXT "Build the Sourcemeta Core language text library" ON) +option(SOURCEMETA_CORE_LANG_STACKTRACE "Build the Sourcemeta Core language stacktrace library" ON) option(SOURCEMETA_CORE_UNICODE "Build the Sourcemeta Core Unicode library" ON) option(SOURCEMETA_CORE_PUNYCODE "Build the Sourcemeta Core Punycode library" ON) option(SOURCEMETA_CORE_TIME "Build the Sourcemeta Core time library" ON) @@ -99,6 +100,10 @@ if(SOURCEMETA_CORE_LANG_TEXT) add_subdirectory(src/lang/text) endif() +if(SOURCEMETA_CORE_LANG_STACKTRACE) + add_subdirectory(src/lang/stacktrace) +endif() + if(SOURCEMETA_CORE_UNICODE) add_subdirectory(src/core/unicode) endif() @@ -245,6 +250,12 @@ if(SOURCEMETA_CORE_TESTS) add_subdirectory(test/text) endif() + if(SOURCEMETA_CORE_LANG_STACKTRACE + AND NOT SOURCEMETA_CORE_ADDRESS_SANITIZER + AND NOT SOURCEMETA_CORE_UNDEFINED_SANITIZER) + add_subdirectory(test/stacktrace) + endif() + if(SOURCEMETA_CORE_UNICODE) add_subdirectory(test/unicode) endif() diff --git a/config.cmake.in b/config.cmake.in index 5245c135e..09e97cca4 100644 --- a/config.cmake.in +++ b/config.cmake.in @@ -32,6 +32,7 @@ if(NOT SOURCEMETA_CORE_COMPONENTS) list(APPEND SOURCEMETA_CORE_COMPONENTS error) list(APPEND SOURCEMETA_CORE_COMPONENTS options) list(APPEND SOURCEMETA_CORE_COMPONENTS text) + list(APPEND SOURCEMETA_CORE_COMPONENTS stacktrace) endif() include(CMakeFindDependencyMacro) @@ -158,6 +159,8 @@ foreach(component ${SOURCEMETA_CORE_COMPONENTS}) include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_options.cmake") elseif(component STREQUAL "text") include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_text.cmake") + elseif(component STREQUAL "stacktrace") + include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_stacktrace.cmake") else() message(FATAL_ERROR "Unknown Sourcemeta Core component: ${component}") endif() diff --git a/src/lang/stacktrace/CMakeLists.txt b/src/lang/stacktrace/CMakeLists.txt new file mode 100644 index 000000000..b807d7542 --- /dev/null +++ b/src/lang/stacktrace/CMakeLists.txt @@ -0,0 +1,16 @@ +sourcemeta_library(NAMESPACE sourcemeta PROJECT core NAME stacktrace + PRIVATE_HEADERS + SOURCES stacktrace.cc stacktrace_posix.h stacktrace_windows.h) + +if(UNIX) + target_link_libraries(sourcemeta_core_stacktrace PRIVATE ${CMAKE_DL_LIBS}) + target_link_options(sourcemeta_core_stacktrace INTERFACE + "$<$:-rdynamic>" + "$<$:-rdynamic>") +elseif(WIN32) + target_link_libraries(sourcemeta_core_stacktrace PRIVATE dbghelp) +endif() + +if(SOURCEMETA_CORE_INSTALL) + sourcemeta_library_install(NAMESPACE sourcemeta PROJECT core NAME stacktrace) +endif() diff --git a/src/lang/stacktrace/include/sourcemeta/core/stacktrace.h b/src/lang/stacktrace/include/sourcemeta/core/stacktrace.h new file mode 100644 index 000000000..52be2935d --- /dev/null +++ b/src/lang/stacktrace/include/sourcemeta/core/stacktrace.h @@ -0,0 +1,50 @@ +#ifndef SOURCEMETA_CORE_STACKTRACE_H_ +#define SOURCEMETA_CORE_STACKTRACE_H_ + +#ifndef SOURCEMETA_CORE_STACKTRACE_EXPORT +#include +#endif + +/// @defgroup stacktrace Stacktrace +/// @brief A collection of utilities for interacting with stack traces. +/// +/// This functionality is included as follows: +/// +/// ```cpp +/// #include +/// ``` + +namespace sourcemeta::core { + +/// @ingroup stacktrace +/// +/// Install a process-wide handler that prints a stack trace on fatal signals. +/// For example: +/// +/// ```cpp +/// #include +/// +/// auto main() -> int { +/// sourcemeta::core::stacktrace_on_crash(); +/// // ... rest of the program +/// } +/// ``` +SOURCEMETA_CORE_STACKTRACE_EXPORT +auto stacktrace_on_crash() -> void; + +/// @ingroup stacktrace +/// +/// Print the current backtrace. Safe to call from any thread, but not from a +/// signal handler. For example: +/// +/// ```cpp +/// #include +/// +/// sourcemeta::core::stacktrace(); +/// ``` +SOURCEMETA_CORE_STACKTRACE_EXPORT +auto stacktrace() -> void; + +} // namespace sourcemeta::core + +#endif diff --git a/src/lang/stacktrace/stacktrace.cc b/src/lang/stacktrace/stacktrace.cc new file mode 100644 index 000000000..a3535da1f --- /dev/null +++ b/src/lang/stacktrace/stacktrace.cc @@ -0,0 +1,9 @@ +#include + +#if defined(_WIN32) +#include "stacktrace_windows.h" +#elif defined(__unix__) || defined(__APPLE__) +#include "stacktrace_posix.h" +#else +#error "sourcemeta::core::stacktrace has no implementation for this platform" +#endif diff --git a/src/lang/stacktrace/stacktrace_posix.h b/src/lang/stacktrace/stacktrace_posix.h new file mode 100644 index 000000000..07bb250c9 --- /dev/null +++ b/src/lang/stacktrace/stacktrace_posix.h @@ -0,0 +1,241 @@ +#ifndef SOURCEMETA_CORE_STACKTRACE_POSIX_H_ +#define SOURCEMETA_CORE_STACKTRACE_POSIX_H_ + +#include + +#include // std::array +#include // std::atomic +#include // sigaction, struct sigaction, SIG*, raise +#include // std::size_t +#include // std::uintptr_t +#include // std::strlen +#include // std::initializer_list + +#include // dladdr, Dl_info +#include // backtrace +#include // ucontext_t +#include // write, getpid, STDERR_FILENO + +namespace { + +constexpr int maximum_frames{128}; +constexpr std::size_t hex_buffer_size{2 + (sizeof(std::uintptr_t) * 2)}; +constexpr std::size_t decimal_buffer_size{24}; + +auto raw_write(int file_descriptor, const char *data, std::size_t size) + -> void { + [[maybe_unused]] const auto result{::write(file_descriptor, data, size)}; +} + +auto write_text(int file_descriptor, const char *text) -> void { + raw_write(file_descriptor, text, std::strlen(text)); +} + +auto write_hex(int file_descriptor, std::uintptr_t value) -> void { + constexpr const char *digits{"0123456789abcdef"}; + std::array buffer{{'0', 'x'}}; + std::size_t index{2}; + if (value == 0) { + buffer[index++] = '0'; + } else { + std::array temporary{}; + std::size_t length{0}; + while (value != 0) { + temporary[length++] = digits[value & 0xF]; + value >>= 4; + } + while (length > 0) { + buffer[index++] = temporary[--length]; + } + } + raw_write(file_descriptor, buffer.data(), index); +} + +auto write_decimal(int file_descriptor, unsigned long value) -> void { + std::array buffer{}; + std::size_t length{0}; + if (value == 0) { + buffer[length++] = '0'; + } else { + std::array temporary{}; + std::size_t temporary_length{0}; + while (value != 0) { + temporary[temporary_length++] = static_cast('0' + (value % 10)); + value /= 10; + } + while (temporary_length > 0) { + buffer[length++] = temporary[--temporary_length]; + } + } + raw_write(file_descriptor, buffer.data(), length); +} + +auto write_frame(int file_descriptor, int frame_index, void *address) -> void { + Dl_info information{}; + const int resolved{::dladdr(address, &information)}; + + write_text(file_descriptor, "#"); + write_decimal(file_descriptor, static_cast(frame_index)); + write_text(file_descriptor, " "); + write_hex(file_descriptor, reinterpret_cast(address)); + write_text(file_descriptor, " "); + + const char *symbol_name{(resolved != 0 && information.dli_sname != nullptr) + ? information.dli_sname + : ""}; + write_text(file_descriptor, symbol_name); + + if (resolved != 0 && information.dli_saddr != nullptr) { + const auto offset{static_cast( + reinterpret_cast(address) - + reinterpret_cast(information.dli_saddr))}; + write_text(file_descriptor, " +"); + write_hex(file_descriptor, offset); + } + + if (resolved != 0 && information.dli_fname != nullptr) { + write_text(file_descriptor, "\n in "); + write_text(file_descriptor, information.dli_fname); + } + write_text(file_descriptor, "\n"); +} + +__attribute__((noinline)) auto write_backtrace(int file_descriptor, + int frames_to_skip, + void *crash_pc = nullptr) + -> void { + std::array frames{}; + const int captured{::backtrace(frames.data(), maximum_frames)}; + + int frame_index{0}; + void *suppress_saddr{nullptr}; + if (crash_pc != nullptr) { + write_frame(file_descriptor, frame_index, crash_pc); + frame_index = frame_index + 1; + Dl_info crash_information{}; + if (::dladdr(crash_pc, &crash_information) != 0 && + crash_information.dli_saddr != nullptr) { + suppress_saddr = crash_information.dli_saddr; + } + } + + for (int index{frames_to_skip}; index < captured; ++index) { + void *address{frames[static_cast(index)]}; + if (suppress_saddr != nullptr) { + Dl_info frame_information{}; + if (::dladdr(address, &frame_information) != 0 && + frame_information.dli_saddr == suppress_saddr) { + suppress_saddr = nullptr; + continue; + } + } + write_frame(file_descriptor, frame_index, address); + frame_index = frame_index + 1; + } +} + +auto extract_crash_pc(void *context) -> void * { + if (context == nullptr) { + return nullptr; + } + [[maybe_unused]] const auto *user_context{ + static_cast(context)}; + std::uintptr_t program_counter{0}; +#if defined(__APPLE__) && defined(__aarch64__) + program_counter = user_context->uc_mcontext->__ss.__pc; +#elif defined(__APPLE__) && defined(__x86_64__) + program_counter = user_context->uc_mcontext->__ss.__rip; +#elif defined(__linux__) && defined(__aarch64__) + program_counter = user_context->uc_mcontext.pc; +#elif defined(__linux__) && defined(__x86_64__) + program_counter = + static_cast(user_context->uc_mcontext.gregs[16]); +#endif + // NOLINTNEXTLINE(performance-no-int-to-ptr) + return reinterpret_cast(program_counter); +} + +constexpr const char *separator{"========================================" + "========================================\n"}; + +std::atomic crash_handler_installed{false}; + +extern "C" __attribute__((visibility("default"))) auto +crash_handler(int signal_number, siginfo_t * /*info*/, void *context) -> void { + const int file_descriptor{STDERR_FILENO}; + write_text(file_descriptor, "\n"); + write_text(file_descriptor, separator); + write_text(file_descriptor, "signal: "); + write_decimal(file_descriptor, static_cast(signal_number)); + write_text(file_descriptor, " ("); + const char *signal_name{"UNKNOWN"}; + switch (signal_number) { + case SIGSEGV: + signal_name = "SIGSEGV"; + break; + case SIGABRT: + signal_name = "SIGABRT"; + break; + case SIGFPE: + signal_name = "SIGFPE"; + break; + case SIGBUS: + signal_name = "SIGBUS"; + break; + case SIGILL: + signal_name = "SIGILL"; + break; + default: + break; + } + write_text(file_descriptor, signal_name); + write_text(file_descriptor, ")\n"); + write_text(file_descriptor, "pid: "); + write_decimal(file_descriptor, static_cast(::getpid())); + write_text(file_descriptor, "\n\n"); + write_backtrace(file_descriptor, /*frames_to_skip=*/1, + extract_crash_pc(context)); + write_text(file_descriptor, separator); + + struct sigaction default_action{}; + default_action.sa_handler = SIG_DFL; + sigemptyset(&default_action.sa_mask); + ::sigaction(signal_number, &default_action, nullptr); + ::raise(signal_number); +} + +} // namespace + +namespace sourcemeta::core { + +// NOLINTNEXTLINE(misc-definitions-in-headers) +__attribute__((visibility("default"))) auto stacktrace_on_crash() -> void { + bool expected{false}; + if (!crash_handler_installed.compare_exchange_strong(expected, true)) { + return; + } + + struct sigaction action{}; + action.sa_sigaction = &crash_handler; + action.sa_flags = static_cast(SA_SIGINFO | SA_RESETHAND | SA_NODEFER); + sigemptyset(&action.sa_mask); + + for (const int signal_number : {SIGSEGV, SIGABRT, SIGFPE, SIGBUS, SIGILL}) { + ::sigaction(signal_number, &action, nullptr); + } +} + +// NOLINTNEXTLINE(misc-definitions-in-headers) +__attribute__((noinline, visibility("default"))) auto stacktrace() -> void { + const int file_descriptor{STDERR_FILENO}; + write_text(file_descriptor, separator); + write_text(file_descriptor, "pid: "); + write_decimal(file_descriptor, static_cast(::getpid())); + write_text(file_descriptor, "\n\n"); + write_backtrace(file_descriptor, /*frames_to_skip=*/1); + write_text(file_descriptor, separator); +} + +} // namespace sourcemeta::core + +#endif diff --git a/src/lang/stacktrace/stacktrace_windows.h b/src/lang/stacktrace/stacktrace_windows.h new file mode 100644 index 000000000..9dc86b95f --- /dev/null +++ b/src/lang/stacktrace/stacktrace_windows.h @@ -0,0 +1,128 @@ +#ifndef SOURCEMETA_CORE_STACKTRACE_WINDOWS_H_ +#define SOURCEMETA_CORE_STACKTRACE_WINDOWS_H_ + +#include + +// clang-format off +#include +#include +// clang-format on + +#include // std::atomic +#include // std::snprintf +#include // std::strlen +#include // _write +#include // _getpid + +#pragma comment(lib, "dbghelp.lib") + +namespace { + +constexpr USHORT maximum_frames{128}; +constexpr const char *separator{"========================================" + "========================================\n"}; + +std::atomic crash_handler_installed{false}; +std::atomic symbols_initialized{false}; + +auto ensure_symbols_initialized() -> void { + bool expected{false}; + if (symbols_initialized.compare_exchange_strong(expected, true)) { + ::SymSetOptions(SYMOPT_UNDNAME | SYMOPT_DEFERRED_LOADS); + ::SymInitialize(::GetCurrentProcess(), nullptr, TRUE); + } +} + +auto write_text(int file_descriptor, const char *text) -> void { + ::_write(file_descriptor, text, static_cast(std::strlen(text))); +} + +__declspec(noinline) auto write_frames(int file_descriptor, + USHORT frames_to_skip) -> void { + void *frames[maximum_frames]; + const USHORT captured{ + ::CaptureStackBackTrace(frames_to_skip, maximum_frames, frames, nullptr)}; + ensure_symbols_initialized(); + const HANDLE process{::GetCurrentProcess()}; + + alignas(SYMBOL_INFO) char symbol_buffer[sizeof(SYMBOL_INFO) + 512]{}; + auto *symbol{reinterpret_cast(symbol_buffer)}; + symbol->SizeOfStruct = sizeof(SYMBOL_INFO); + symbol->MaxNameLen = 511; + + for (USHORT index{0}; index < captured; ++index) { + DWORD64 displacement{0}; + const char *symbol_name{""}; + if (::SymFromAddr(process, reinterpret_cast(frames[index]), + &displacement, symbol)) { + symbol_name = symbol->Name; + } + HMODULE module_handle{nullptr}; + char module_path[MAX_PATH]{""}; + if (::GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | + GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, + static_cast(frames[index]), + &module_handle) != 0) { + ::GetModuleFileNameA(module_handle, module_path, MAX_PATH); + } + char line[1024]; + const int length{std::snprintf( + line, sizeof(line), "#%u 0x%p %s +0x%llx\n in %s\n", + static_cast(index), frames[index], symbol_name, + static_cast(displacement), module_path)}; + if (length > 0) { + ::_write(file_descriptor, line, static_cast(length)); + } + } +} + +} // namespace + +extern "C" SOURCEMETA_CORE_STACKTRACE_EXPORT auto WINAPI +crash_handler(EXCEPTION_POINTERS *information) -> LONG { + const int file_descriptor{2}; + write_text(file_descriptor, "\n"); + write_text(file_descriptor, separator); + char header[256]; + const int length{std::snprintf( + header, sizeof(header), + "signal: 0x%08lx (SEH)\n" + "pid: %d\n\n", + static_cast(information->ExceptionRecord->ExceptionCode), + _getpid())}; + if (length > 0) { + ::_write(file_descriptor, header, static_cast(length)); + } + write_frames(file_descriptor, /*frames_to_skip=*/1); + write_text(file_descriptor, separator); + return EXCEPTION_CONTINUE_SEARCH; +} + +namespace sourcemeta::core { + +// NOLINTNEXTLINE(misc-definitions-in-headers) +auto stacktrace_on_crash() -> void { + bool expected{false}; + if (!crash_handler_installed.compare_exchange_strong(expected, true)) { + return; + } + ::SetUnhandledExceptionFilter(&crash_handler); +} + +// NOLINTNEXTLINE(misc-definitions-in-headers) +__declspec(noinline) auto stacktrace() -> void { + const int file_descriptor{2}; + write_text(file_descriptor, separator); + char header[256]; + const int length{ + std::snprintf(header, sizeof(header), "pid: %d\n\n", _getpid())}; + if (length > 0) { + ::_write(file_descriptor, header, static_cast(length)); + } + write_frames(file_descriptor, /*frames_to_skip=*/1); + write_text(file_descriptor, separator); +} + +} // namespace sourcemeta::core + +#endif diff --git a/test/stacktrace/CMakeLists.txt b/test/stacktrace/CMakeLists.txt new file mode 100644 index 000000000..c30cb513a --- /dev/null +++ b/test/stacktrace/CMakeLists.txt @@ -0,0 +1,61 @@ +macro(add_stacktrace_test_unix scenario debug_flag) + if(UNIX) + set(target sourcemeta_core_stacktrace_unit_${scenario}_main_${debug_flag}) + add_executable(${target} stacktrace_${scenario}_main.cc) + target_link_libraries(${target} PRIVATE sourcemeta::core::stacktrace) + if("${debug_flag}" STREQUAL "none") + target_compile_options(${target} PRIVATE -g0 -fvisibility=default) + else() + target_compile_options(${target} PRIVATE -${debug_flag} -fvisibility=default) + endif() + add_test(NAME core.stacktrace.${scenario}.${debug_flag}.e2e + COMMAND "${CMAKE_CURRENT_SOURCE_DIR}/stacktrace_${scenario}_test.sh" + "$" + "${CMAKE_CURRENT_BINARY_DIR}" + "$") + endif() +endmacro() + +macro(add_stacktrace_test_windows scenario) + if(WIN32) + set(target sourcemeta_core_stacktrace_unit_${scenario}_main) + add_executable(${target} stacktrace_${scenario}_main.cc) + target_link_libraries(${target} PRIVATE sourcemeta::core::stacktrace) + target_compile_options(${target} PRIVATE /Zi) + target_link_options(${target} PRIVATE /DEBUG) + add_test(NAME core.stacktrace.${scenario}.e2e + COMMAND powershell -ExecutionPolicy Bypass -File + "${CMAKE_CURRENT_SOURCE_DIR}/stacktrace_${scenario}_test.ps1" + "$" + "${CMAKE_CURRENT_BINARY_DIR}" + "$") + endif() +endmacro() + +macro(add_stacktrace_test_windows_no_debug scenario) + if(WIN32) + set(target sourcemeta_core_stacktrace_unit_${scenario}_main_no_debug) + add_executable(${target} stacktrace_${scenario}_main.cc) + target_link_libraries(${target} PRIVATE sourcemeta::core::stacktrace) + add_test(NAME core.stacktrace.${scenario}.no_debug.e2e + COMMAND powershell -ExecutionPolicy Bypass -File + "${CMAKE_CURRENT_SOURCE_DIR}/stacktrace_${scenario}_no_debug_test.ps1" + "$" + "${CMAKE_CURRENT_BINARY_DIR}") + endif() +endmacro() + +add_stacktrace_test_unix(segfault g) +add_stacktrace_test_unix(segfault g1) +add_stacktrace_test_unix(segfault none) +add_stacktrace_test_unix(abort g) +add_stacktrace_test_unix(abort g1) +add_stacktrace_test_unix(abort none) +add_stacktrace_test_unix(on_demand g) +add_stacktrace_test_unix(on_demand g1) +add_stacktrace_test_unix(on_demand none) + +add_stacktrace_test_windows(segfault) +add_stacktrace_test_windows(on_demand) +add_stacktrace_test_windows_no_debug(segfault) +add_stacktrace_test_windows_no_debug(on_demand) diff --git a/test/stacktrace/stacktrace_abort_main.cc b/test/stacktrace/stacktrace_abort_main.cc new file mode 100644 index 000000000..a6644274d --- /dev/null +++ b/test/stacktrace/stacktrace_abort_main.cc @@ -0,0 +1,34 @@ +#include + +#include // raise, SIGABRT + +#if defined(_MSC_VER) +#define STACKTRACE_TEST_NOINLINE __declspec(noinline) +#else +#define STACKTRACE_TEST_NOINLINE __attribute__((noinline)) +#endif + +namespace sourcemeta_core_stacktrace_test { + +volatile int sink{0}; + +auto crash_deepest() -> void; +auto crash_middle() -> void; + +STACKTRACE_TEST_NOINLINE auto crash_deepest() -> void { + ::raise(SIGABRT); + sink = sink + 1; +} + +STACKTRACE_TEST_NOINLINE auto crash_middle() -> void { + crash_deepest(); + sink = sink + 1; +} + +} // namespace sourcemeta_core_stacktrace_test + +auto main() -> int { + sourcemeta::core::stacktrace_on_crash(); + sourcemeta_core_stacktrace_test::crash_middle(); + return 0; +} diff --git a/test/stacktrace/stacktrace_abort_test.sh b/test/stacktrace/stacktrace_abort_test.sh new file mode 100755 index 000000000..42d4cf0a8 --- /dev/null +++ b/test/stacktrace/stacktrace_abort_test.sh @@ -0,0 +1,53 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +STACKTRACE_ABORT_MAIN="$1" +WORKDIR="$2" +STACKTRACE_LIBRARY="$3" +SELF="$(basename "$STACKTRACE_ABORT_MAIN")" + +case "$STACKTRACE_LIBRARY" in + *.a|*.lib) LIBRARY_PATH="$STACKTRACE_ABORT_MAIN" ;; + *) LIBRARY_PATH="$(echo "$STACKTRACE_LIBRARY" | sed -E -e 's|\.so\.[0-9.]+|.so|g' -e 's|\.[0-9.]+\.dylib|.dylib|g')" ;; +esac + +"$STACKTRACE_ABORT_MAIN" > "$WORKDIR/$SELF.actual.txt" 2>&1 \ + && EXIT_CODE="$?" || EXIT_CODE="$?" +# Crashed by a fatal signal +test "$EXIT_CODE" -ne 0 + +cat "$WORKDIR/$SELF.actual.txt" + +sed -E \ + -e 's/0x[0-9a-fA-F]+/0xADDR/g' \ + -e 's/\+0xADDR/+0xOFFSET/g' \ + -e 's/^pid:[[:space:]]+[0-9]+/pid: /' \ + -e 's/^#[0-9]+ /# /' \ + -e '/^# /{N;/_sigtramp|__restore_rt|__kernel_rt_sigreturn|__libc_start_main|libsystem_|libdyld|\/dyld|linux-vdso|libc\.so| pthread_kill | gsignal | raise | _?start \+/d;}' \ + -e 's|\.so\.[0-9.]+|.so|g' \ + -e 's|\.[0-9.]+\.dylib|.dylib|g' \ + -e '/^Aborted/d' \ + -e '/^Segmentation fault/d' \ + "$WORKDIR/$SELF.actual.txt" \ + > "$WORKDIR/$SELF.normalized.txt" + +cat << EOF > "$WORKDIR/$SELF.expected.txt" + +================================================================================ +signal: 6 (SIGABRT) +pid: + +# 0xADDR crash_handler +0xOFFSET + in $LIBRARY_PATH +# 0xADDR _ZN31sourcemeta_core_stacktrace_test13crash_deepestEv +0xOFFSET + in $STACKTRACE_ABORT_MAIN +# 0xADDR _ZN31sourcemeta_core_stacktrace_test12crash_middleEv +0xOFFSET + in $STACKTRACE_ABORT_MAIN +# 0xADDR main +0xOFFSET + in $STACKTRACE_ABORT_MAIN +================================================================================ +EOF + +diff "$WORKDIR/$SELF.normalized.txt" "$WORKDIR/$SELF.expected.txt" diff --git a/test/stacktrace/stacktrace_on_demand_main.cc b/test/stacktrace/stacktrace_on_demand_main.cc new file mode 100644 index 000000000..ad23f73d6 --- /dev/null +++ b/test/stacktrace/stacktrace_on_demand_main.cc @@ -0,0 +1,31 @@ +#include + +#if defined(_MSC_VER) +#define STACKTRACE_TEST_NOINLINE __declspec(noinline) +#else +#define STACKTRACE_TEST_NOINLINE __attribute__((noinline)) +#endif + +namespace sourcemeta_core_stacktrace_test { + +volatile int sink{0}; + +auto print_deepest() -> void; +auto print_middle() -> void; + +STACKTRACE_TEST_NOINLINE auto print_deepest() -> void { + sourcemeta::core::stacktrace(); + sink = sink + 1; +} + +STACKTRACE_TEST_NOINLINE auto print_middle() -> void { + print_deepest(); + sink = sink + 1; +} + +} // namespace sourcemeta_core_stacktrace_test + +auto main() -> int { + sourcemeta_core_stacktrace_test::print_middle(); + return 0; +} diff --git a/test/stacktrace/stacktrace_on_demand_no_debug_test.ps1 b/test/stacktrace/stacktrace_on_demand_no_debug_test.ps1 new file mode 100644 index 000000000..a7f468498 --- /dev/null +++ b/test/stacktrace/stacktrace_on_demand_no_debug_test.ps1 @@ -0,0 +1,33 @@ +param( + [Parameter(Mandatory=$true)] + [string]$StacktraceOnDemandMain, + [Parameter(Mandatory=$true)] + [string]$WorkDir +) + +$ErrorActionPreference = "Stop" +$Self = [IO.Path]::GetFileName($StacktraceOnDemandMain) +$Actual = Join-Path $WorkDir "$Self.actual.txt" + +$ErrorActionPreference = "Continue" +& cmd.exe /c "`"$StacktraceOnDemandMain`" > `"$Actual`" 2>&1" +$ExitCode = $LASTEXITCODE +$ErrorActionPreference = "Stop" +# Exited cleanly after printing the on-demand trace +if ($ExitCode -ne 0) { + throw "Expected exit code 0, got $ExitCode" +} + +Get-Content $Actual + +$Output = Get-Content $Actual -Raw + +# Without /Zi + /DEBUG, dbghelp cannot resolve internal application symbols. +# Verify the on-demand call still produced a structurally-correct trace where +# the application frames show as . +if ($Output -notmatch '={80}') { + throw "Missing separator line" +} +if ($Output -notmatch '') { + throw "Expected at least one frame (dbghelp without PDB)" +} diff --git a/test/stacktrace/stacktrace_on_demand_test.ps1 b/test/stacktrace/stacktrace_on_demand_test.ps1 new file mode 100644 index 000000000..7d867865f --- /dev/null +++ b/test/stacktrace/stacktrace_on_demand_test.ps1 @@ -0,0 +1,69 @@ +param( + [Parameter(Mandatory=$true)] + [string]$StacktraceOnDemandMain, + [Parameter(Mandatory=$true)] + [string]$WorkDir, + [Parameter(Mandatory=$true)] + [string]$StacktraceLibrary +) + +$ErrorActionPreference = "Stop" +$Self = [IO.Path]::GetFileName($StacktraceOnDemandMain) +$StacktraceOnDemandMain = $StacktraceOnDemandMain -replace '/', '\' +$StacktraceLibrary = $StacktraceLibrary -replace '/', '\' +$LibraryPath = if ($StacktraceLibrary -match '\.(lib|a)$') { + $StacktraceOnDemandMain +} else { + $StacktraceLibrary +} +$Actual = Join-Path $WorkDir "$Self.actual.txt" +$Normalized = Join-Path $WorkDir "$Self.normalized.txt" +$Expected = Join-Path $WorkDir "$Self.expected.txt" + +$ErrorActionPreference = "Continue" +& cmd.exe /c "`"$StacktraceOnDemandMain`" > `"$Actual`" 2>&1" +$ExitCode = $LASTEXITCODE +$ErrorActionPreference = "Stop" +# Exited cleanly after printing the on-demand trace +if ($ExitCode -ne 0) { + throw "Expected exit code 0, got $ExitCode" +} + +Get-Content $Actual + +$NormalizedContent = (Get-Content $Actual -Raw) ` + -replace '0x[0-9a-fA-F]+', '0xADDR' ` + -replace '\+0xADDR', '+0xOFFSET' ` + -replace '(?m)^pid:\s+\d+', 'pid: ' ` + -replace '(?m)^#\d+ ', '# ' ` + -replace '(?m)^# [^\r\n]*(_sigtramp|__restore_rt| _?start \+|__scrt_|BaseThreadInitThunk|RtlUserThreadStart|UnhandledExceptionFilter|KiUserExceptionDispatcher)[^\r\n]*\r?\n in [^\r\n]*\r?\n', '' ` + -replace '(?m)^# [^\r\n]*\r?\n in [^\r\n]*(libsystem_|libdyld|/dyld|\\Windows\\)[^\r\n]*\r?\n', '' ` + -replace "`r`n", "`n" + +$ExpectedContent = (@" +================================================================================ +pid: + +# 0xADDR sourcemeta::core::stacktrace +0xOFFSET + in $LibraryPath +# 0xADDR sourcemeta_core_stacktrace_test::print_deepest +0xOFFSET + in $StacktraceOnDemandMain +# 0xADDR sourcemeta_core_stacktrace_test::print_middle +0xOFFSET + in $StacktraceOnDemandMain +# 0xADDR main +0xOFFSET + in $StacktraceOnDemandMain +================================================================================ +"@) -replace "`r`n", "`n" + +$NormalizedContent = $NormalizedContent.TrimEnd() +$ExpectedContent = $ExpectedContent.TrimEnd() +[System.IO.File]::WriteAllText($Normalized, $NormalizedContent) +[System.IO.File]::WriteAllText($Expected, $ExpectedContent) + +if ($NormalizedContent -ne $ExpectedContent) { + Write-Host "=== EXPECTED ===" + Write-Host $ExpectedContent + Write-Host "=== ACTUAL (normalized) ===" + Write-Host $NormalizedContent + throw "Output did not match expected" +} diff --git a/test/stacktrace/stacktrace_on_demand_test.sh b/test/stacktrace/stacktrace_on_demand_test.sh new file mode 100755 index 000000000..e078f7e94 --- /dev/null +++ b/test/stacktrace/stacktrace_on_demand_test.sh @@ -0,0 +1,51 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +STACKTRACE_ON_DEMAND_MAIN="$1" +WORKDIR="$2" +STACKTRACE_LIBRARY="$3" +SELF="$(basename "$STACKTRACE_ON_DEMAND_MAIN")" + +case "$STACKTRACE_LIBRARY" in + *.a|*.lib) LIBRARY_PATH="$STACKTRACE_ON_DEMAND_MAIN" ;; + *) LIBRARY_PATH="$(echo "$STACKTRACE_LIBRARY" | sed -E -e 's|\.so\.[0-9.]+|.so|g' -e 's|\.[0-9.]+\.dylib|.dylib|g')" ;; +esac + +"$STACKTRACE_ON_DEMAND_MAIN" > "$WORKDIR/$SELF.actual.txt" 2>&1 \ + && EXIT_CODE="$?" || EXIT_CODE="$?" +# Exited cleanly after printing the on-demand trace +test "$EXIT_CODE" -eq 0 + +cat "$WORKDIR/$SELF.actual.txt" + +sed -E \ + -e 's/0x[0-9a-fA-F]+/0xADDR/g' \ + -e 's/\+0xADDR/+0xOFFSET/g' \ + -e 's/^pid:[[:space:]]+[0-9]+/pid: /' \ + -e 's/^#[0-9]+ /# /' \ + -e '/^# /{N;/_sigtramp|__restore_rt|__kernel_rt_sigreturn|__libc_start_main|libsystem_|libdyld|\/dyld|linux-vdso|libc\.so| pthread_kill | gsignal | raise | _?start \+/d;}' \ + -e 's|\.so\.[0-9.]+|.so|g' \ + -e 's|\.[0-9.]+\.dylib|.dylib|g' \ + -e '/^Aborted/d' \ + -e '/^Segmentation fault/d' \ + "$WORKDIR/$SELF.actual.txt" \ + > "$WORKDIR/$SELF.normalized.txt" + +cat << EOF > "$WORKDIR/$SELF.expected.txt" +================================================================================ +pid: + +# 0xADDR _ZN10sourcemeta4core10stacktraceEv +0xOFFSET + in $LIBRARY_PATH +# 0xADDR _ZN31sourcemeta_core_stacktrace_test13print_deepestEv +0xOFFSET + in $STACKTRACE_ON_DEMAND_MAIN +# 0xADDR _ZN31sourcemeta_core_stacktrace_test12print_middleEv +0xOFFSET + in $STACKTRACE_ON_DEMAND_MAIN +# 0xADDR main +0xOFFSET + in $STACKTRACE_ON_DEMAND_MAIN +================================================================================ +EOF + +diff "$WORKDIR/$SELF.normalized.txt" "$WORKDIR/$SELF.expected.txt" diff --git a/test/stacktrace/stacktrace_segfault_main.cc b/test/stacktrace/stacktrace_segfault_main.cc new file mode 100644 index 000000000..053c98b94 --- /dev/null +++ b/test/stacktrace/stacktrace_segfault_main.cc @@ -0,0 +1,32 @@ +#include + +#if defined(_MSC_VER) +#define STACKTRACE_TEST_NOINLINE __declspec(noinline) +#else +#define STACKTRACE_TEST_NOINLINE __attribute__((noinline)) +#endif + +namespace sourcemeta_core_stacktrace_test { + +volatile int sink{0}; + +auto crash_deepest() -> void; +auto crash_middle() -> void; + +STACKTRACE_TEST_NOINLINE auto crash_deepest() -> void { + volatile int *volatile null_pointer{nullptr}; + *null_pointer = 42; +} + +STACKTRACE_TEST_NOINLINE auto crash_middle() -> void { + crash_deepest(); + sink = sink + 1; +} + +} // namespace sourcemeta_core_stacktrace_test + +auto main() -> int { + sourcemeta::core::stacktrace_on_crash(); + sourcemeta_core_stacktrace_test::crash_middle(); + return 0; +} diff --git a/test/stacktrace/stacktrace_segfault_no_debug_test.ps1 b/test/stacktrace/stacktrace_segfault_no_debug_test.ps1 new file mode 100644 index 000000000..4fdcf9f97 --- /dev/null +++ b/test/stacktrace/stacktrace_segfault_no_debug_test.ps1 @@ -0,0 +1,36 @@ +param( + [Parameter(Mandatory=$true)] + [string]$StacktraceSegfaultMain, + [Parameter(Mandatory=$true)] + [string]$WorkDir +) + +$ErrorActionPreference = "Stop" +$Self = [IO.Path]::GetFileName($StacktraceSegfaultMain) +$Actual = Join-Path $WorkDir "$Self.actual.txt" + +$ErrorActionPreference = "Continue" +& cmd.exe /c "`"$StacktraceSegfaultMain`" > `"$Actual`" 2>&1" +$ExitCode = $LASTEXITCODE +$ErrorActionPreference = "Stop" +# Crashed by a fatal exception +if ($ExitCode -eq 0) { + throw "Expected non-zero exit code, got $ExitCode" +} + +Get-Content $Actual + +$Output = Get-Content $Actual -Raw + +# Without /Zi + /DEBUG, dbghelp cannot resolve internal application symbols. +# Verify the handler still ran and produced a structurally-correct trace where +# the application frames show as . +if ($Output -notmatch '={80}') { + throw "Missing separator line" +} +if ($Output -notmatch '(?m)^signal:\s+0x[0-9a-fA-F]+ \(SEH\)') { + throw "Missing signal line" +} +if ($Output -notmatch '') { + throw "Expected at least one frame (dbghelp without PDB)" +} diff --git a/test/stacktrace/stacktrace_segfault_test.ps1 b/test/stacktrace/stacktrace_segfault_test.ps1 new file mode 100644 index 000000000..55f9dab7f --- /dev/null +++ b/test/stacktrace/stacktrace_segfault_test.ps1 @@ -0,0 +1,77 @@ +param( + [Parameter(Mandatory=$true)] + [string]$StacktraceSegfaultMain, + [Parameter(Mandatory=$true)] + [string]$WorkDir, + [Parameter(Mandatory=$true)] + [string]$StacktraceLibrary +) + +$ErrorActionPreference = "Stop" +$Self = [IO.Path]::GetFileName($StacktraceSegfaultMain) +# CMake's $ can emit forward slashes; dbghelp returns +# backslashes via GetModuleFileNameA. Normalize so the expected matches. +$StacktraceSegfaultMain = $StacktraceSegfaultMain -replace '/', '\' +$StacktraceLibrary = $StacktraceLibrary -replace '/', '\' +$LibraryPath = if ($StacktraceLibrary -match '\.(lib|a)$') { + $StacktraceSegfaultMain +} else { + $StacktraceLibrary +} +$Actual = Join-Path $WorkDir "$Self.actual.txt" +$Normalized = Join-Path $WorkDir "$Self.normalized.txt" +$Expected = Join-Path $WorkDir "$Self.expected.txt" + +# Delegate the redirection to cmd.exe: PowerShell wraps native-command stderr +# as ErrorRecord objects whose stringified form (with file/line/category +# metadata) ends up in the captured file. cmd.exe does plain fd-level +# redirection without that wrapping. +$ErrorActionPreference = "Continue" +& cmd.exe /c "`"$StacktraceSegfaultMain`" > `"$Actual`" 2>&1" +$ExitCode = $LASTEXITCODE +$ErrorActionPreference = "Stop" +# Crashed by a fatal exception +if ($ExitCode -eq 0) { + throw "Expected non-zero exit code, got $ExitCode" +} + +Get-Content $Actual + +$NormalizedContent = (Get-Content $Actual -Raw) ` + -replace '0x[0-9a-fA-F]+', '0xADDR' ` + -replace '\+0xADDR', '+0xOFFSET' ` + -replace '(?m)^pid:\s+\d+', 'pid: ' ` + -replace '(?m)^#\d+ ', '# ' ` + -replace '(?m)^# [^\r\n]*(_sigtramp|__restore_rt| _?start \+|__scrt_|BaseThreadInitThunk|RtlUserThreadStart|UnhandledExceptionFilter|KiUserExceptionDispatcher)[^\r\n]*\r?\n in [^\r\n]*\r?\n', '' ` + -replace '(?m)^# [^\r\n]*\r?\n in [^\r\n]*(libsystem_|libdyld|/dyld|\\Windows\\)[^\r\n]*\r?\n', '' ` + -replace "`r`n", "`n" + +$ExpectedContent = (@" + +================================================================================ +signal: 0xADDR (SEH) +pid: + +# 0xADDR crash_handler +0xOFFSET + in $LibraryPath +# 0xADDR sourcemeta_core_stacktrace_test::crash_deepest +0xOFFSET + in $StacktraceSegfaultMain +# 0xADDR sourcemeta_core_stacktrace_test::crash_middle +0xOFFSET + in $StacktraceSegfaultMain +# 0xADDR main +0xOFFSET + in $StacktraceSegfaultMain +================================================================================ +"@) -replace "`r`n", "`n" + +$NormalizedContent = $NormalizedContent.TrimEnd() +$ExpectedContent = $ExpectedContent.TrimEnd() +[System.IO.File]::WriteAllText($Normalized, $NormalizedContent) +[System.IO.File]::WriteAllText($Expected, $ExpectedContent) + +if ($NormalizedContent -ne $ExpectedContent) { + Write-Host "=== EXPECTED ===" + Write-Host $ExpectedContent + Write-Host "=== ACTUAL (normalized) ===" + Write-Host $NormalizedContent + throw "Output did not match expected" +} diff --git a/test/stacktrace/stacktrace_segfault_test.sh b/test/stacktrace/stacktrace_segfault_test.sh new file mode 100755 index 000000000..ecc1c10fb --- /dev/null +++ b/test/stacktrace/stacktrace_segfault_test.sh @@ -0,0 +1,53 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +STACKTRACE_SEGFAULT_MAIN="$1" +WORKDIR="$2" +STACKTRACE_LIBRARY="$3" +SELF="$(basename "$STACKTRACE_SEGFAULT_MAIN")" + +case "$STACKTRACE_LIBRARY" in + *.a|*.lib) LIBRARY_PATH="$STACKTRACE_SEGFAULT_MAIN" ;; + *) LIBRARY_PATH="$(echo "$STACKTRACE_LIBRARY" | sed -E -e 's|\.so\.[0-9.]+|.so|g' -e 's|\.[0-9.]+\.dylib|.dylib|g')" ;; +esac + +"$STACKTRACE_SEGFAULT_MAIN" > "$WORKDIR/$SELF.actual.txt" 2>&1 \ + && EXIT_CODE="$?" || EXIT_CODE="$?" +# Crashed by a fatal signal +test "$EXIT_CODE" -ne 0 + +cat "$WORKDIR/$SELF.actual.txt" + +sed -E \ + -e 's/0x[0-9a-fA-F]+/0xADDR/g' \ + -e 's/\+0xADDR/+0xOFFSET/g' \ + -e 's/^pid:[[:space:]]+[0-9]+/pid: /' \ + -e 's/^#[0-9]+ /# /' \ + -e '/^# /{N;/_sigtramp|__restore_rt|__kernel_rt_sigreturn|__libc_start_main|libsystem_|libdyld|\/dyld|linux-vdso|libc\.so| pthread_kill | gsignal | raise | _?start \+/d;}' \ + -e 's|\.so\.[0-9.]+|.so|g' \ + -e 's|\.[0-9.]+\.dylib|.dylib|g' \ + -e '/^Aborted/d' \ + -e '/^Segmentation fault/d' \ + "$WORKDIR/$SELF.actual.txt" \ + > "$WORKDIR/$SELF.normalized.txt" + +cat << EOF > "$WORKDIR/$SELF.expected.txt" + +================================================================================ +signal: 11 (SIGSEGV) +pid: + +# 0xADDR _ZN31sourcemeta_core_stacktrace_test13crash_deepestEv +0xOFFSET + in $STACKTRACE_SEGFAULT_MAIN +# 0xADDR crash_handler +0xOFFSET + in $LIBRARY_PATH +# 0xADDR _ZN31sourcemeta_core_stacktrace_test12crash_middleEv +0xOFFSET + in $STACKTRACE_SEGFAULT_MAIN +# 0xADDR main +0xOFFSET + in $STACKTRACE_SEGFAULT_MAIN +================================================================================ +EOF + +diff "$WORKDIR/$SELF.normalized.txt" "$WORKDIR/$SELF.expected.txt"