From 07a49cd564b7cc953ce720446c98bcb391413a39 Mon Sep 17 00:00:00 2001 From: Juan Cruz Viotti Date: Tue, 26 May 2026 13:34:41 -0400 Subject: [PATCH 01/12] [WIP] Implement a cross-platform stack trace printer Signed-off-by: Juan Cruz Viotti --- .github/workflows/website-build.yml | 1 + .github/workflows/website-deploy.yml | 1 + CMakeLists.txt | 9 + config.cmake.in | 3 + src/lang/stacktrace/CMakeLists.txt | 16 ++ .../include/sourcemeta/core/stacktrace.h | 50 ++++ src/lang/stacktrace/stacktrace.cc | 9 + src/lang/stacktrace/stacktrace_posix.h | 224 ++++++++++++++++++ src/lang/stacktrace/stacktrace_windows.h | 126 ++++++++++ test/stacktrace/CMakeLists.txt | 62 +++++ test/stacktrace/stacktrace_abort_main.cc | 25 ++ .../stacktrace_abort_no_debug_test.ps1 | 34 +++ test/stacktrace/stacktrace_abort_test.ps1 | 48 ++++ test/stacktrace/stacktrace_abort_test.sh | 43 ++++ test/stacktrace/stacktrace_on_demand_main.cc | 24 ++ .../stacktrace_on_demand_no_debug_test.ps1 | 31 +++ test/stacktrace/stacktrace_on_demand_test.ps1 | 50 ++++ test/stacktrace/stacktrace_on_demand_test.sh | 41 ++++ test/stacktrace/stacktrace_segfault_main.cc | 26 ++ .../stacktrace_segfault_no_debug_test.ps1 | 34 +++ test/stacktrace/stacktrace_segfault_test.ps1 | 50 ++++ test/stacktrace/stacktrace_segfault_test.sh | 43 ++++ 22 files changed, 950 insertions(+) create mode 100644 src/lang/stacktrace/CMakeLists.txt create mode 100644 src/lang/stacktrace/include/sourcemeta/core/stacktrace.h create mode 100644 src/lang/stacktrace/stacktrace.cc create mode 100644 src/lang/stacktrace/stacktrace_posix.h create mode 100644 src/lang/stacktrace/stacktrace_windows.h create mode 100644 test/stacktrace/CMakeLists.txt create mode 100644 test/stacktrace/stacktrace_abort_main.cc create mode 100644 test/stacktrace/stacktrace_abort_no_debug_test.ps1 create mode 100644 test/stacktrace/stacktrace_abort_test.ps1 create mode 100755 test/stacktrace/stacktrace_abort_test.sh create mode 100644 test/stacktrace/stacktrace_on_demand_main.cc create mode 100644 test/stacktrace/stacktrace_on_demand_no_debug_test.ps1 create mode 100644 test/stacktrace/stacktrace_on_demand_test.ps1 create mode 100755 test/stacktrace/stacktrace_on_demand_test.sh create mode 100644 test/stacktrace/stacktrace_segfault_main.cc create mode 100644 test/stacktrace/stacktrace_segfault_no_debug_test.ps1 create mode 100644 test/stacktrace/stacktrace_segfault_test.ps1 create mode 100755 test/stacktrace/stacktrace_segfault_test.sh 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..94a3302ab 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,10 @@ if(SOURCEMETA_CORE_TESTS) add_subdirectory(test/text) endif() + if(SOURCEMETA_CORE_LANG_STACKTRACE) + 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..9131ae67c --- /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::install_crash_handler(); +/// // ... rest of the program +/// } +/// ``` +SOURCEMETA_CORE_STACKTRACE_EXPORT +auto install_crash_handler() -> 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..5d4153fdd --- /dev/null +++ b/src/lang/stacktrace/stacktrace_posix.h @@ -0,0 +1,224 @@ +#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"); +} + +auto write_backtrace(int file_descriptor, int frames_to_skip, + void *crash_pc = nullptr) -> void { + int frame_index{0}; + if (crash_pc != nullptr) { + write_frame(file_descriptor, frame_index, crash_pc); + frame_index = frame_index + 1; + } + std::array frames{}; + const int captured{::backtrace(frames.data(), maximum_frames)}; + for (int index{frames_to_skip}; index < captured; ++index) { + write_frame(file_descriptor, frame_index, + frames[static_cast(index)]); + frame_index = frame_index + 1; + } +} + +auto extract_crash_pc(void *context) -> void * { + if (context == nullptr) { + return nullptr; + } + 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]); +#else + (void)user_context; +#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" 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) +auto install_crash_handler() -> 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) +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..f23fc0798 --- /dev/null +++ b/src/lang/stacktrace/stacktrace_windows.h @@ -0,0 +1,126 @@ +#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))); +} + +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)); + } + } +} + +auto WINAPI crash_filter(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 + +namespace sourcemeta::core { + +// NOLINTNEXTLINE(misc-definitions-in-headers) +auto install_crash_handler() -> void { + bool expected{false}; + if (!crash_handler_installed.compare_exchange_strong(expected, true)) { + return; + } + ::SetUnhandledExceptionFilter(&crash_filter); +} + +// NOLINTNEXTLINE(misc-definitions-in-headers) +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..6dcccdb4f --- /dev/null +++ b/test/stacktrace/CMakeLists.txt @@ -0,0 +1,62 @@ +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) + else() + target_compile_options(${target} PRIVATE -${debug_flag}) + 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(abort) +add_stacktrace_test_windows(on_demand) + +add_stacktrace_test_windows_no_debug(segfault) +add_stacktrace_test_windows_no_debug(abort) +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..6ea228206 --- /dev/null +++ b/test/stacktrace/stacktrace_abort_main.cc @@ -0,0 +1,25 @@ +#include + +#include // std::abort + +namespace sourcemeta_core_stacktrace_test { + +volatile int sink{0}; + +auto crash_deepest() -> void; +auto crash_middle() -> void; + +__attribute__((noinline)) auto crash_deepest() -> void { std::abort(); } + +__attribute__((noinline)) auto crash_middle() -> void { + crash_deepest(); + sink = sink + 1; +} + +} // namespace sourcemeta_core_stacktrace_test + +auto main() -> int { + sourcemeta::core::install_crash_handler(); + sourcemeta_core_stacktrace_test::crash_middle(); + return 0; +} diff --git a/test/stacktrace/stacktrace_abort_no_debug_test.ps1 b/test/stacktrace/stacktrace_abort_no_debug_test.ps1 new file mode 100644 index 000000000..2a70eba98 --- /dev/null +++ b/test/stacktrace/stacktrace_abort_no_debug_test.ps1 @@ -0,0 +1,34 @@ +param( + [Parameter(Mandatory=$true)] + [string]$StacktraceAbortMain, + [Parameter(Mandatory=$true)] + [string]$WorkDir +) + +$ErrorActionPreference = "Stop" +$Self = [IO.Path]::GetFileName($StacktraceAbortMain) +$Actual = Join-Path $WorkDir "$Self.actual.txt" + +& $StacktraceAbortMain *> $Actual +$ExitCode = $LASTEXITCODE +# 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_abort_test.ps1 b/test/stacktrace/stacktrace_abort_test.ps1 new file mode 100644 index 000000000..c241a3773 --- /dev/null +++ b/test/stacktrace/stacktrace_abort_test.ps1 @@ -0,0 +1,48 @@ +param( + [Parameter(Mandatory=$true)] + [string]$StacktraceAbortMain, + [Parameter(Mandatory=$true)] + [string]$WorkDir +) + +$ErrorActionPreference = "Stop" +$Self = [IO.Path]::GetFileName($StacktraceAbortMain) +$Actual = Join-Path $WorkDir "$Self.actual.txt" +$Normalized = Join-Path $WorkDir "$Self.normalized.txt" +$Expected = Join-Path $WorkDir "$Self.expected.txt" + +& $StacktraceAbortMain *> $Actual +$ExitCode = $LASTEXITCODE +# Crashed by a fatal exception +if ($ExitCode -eq 0) { + throw "Expected non-zero exit code, got $ExitCode" +} + +Get-Content $Actual + +(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 \+)[^\r\n]*\r?\n in [^\r\n]*\r?\n', '' ` + -replace '(?m)^# [^\r\n]*\r?\n in [^\r\n]*(libsystem_|libdyld|/dyld)[^\r\n]*\r?\n', '' ` + | Set-Content -NoNewline $Normalized + +@" + +================================================================================ +signal: 0x80000003 (SEH) +pid: + +# 0xADDR sourcemeta_core_stacktrace_test::crash_middle +0xOFFSET + in $StacktraceAbortMain +# 0xADDR main +0xOFFSET + in $StacktraceAbortMain +================================================================================ +"@ | Set-Content -NoNewline $Expected + +if ((Get-Content $Normalized -Raw) -ne (Get-Content $Expected -Raw)) { + Compare-Object (Get-Content $Expected) (Get-Content $Normalized) + throw "Output did not match expected" +} diff --git a/test/stacktrace/stacktrace_abort_test.sh b/test/stacktrace/stacktrace_abort_test.sh new file mode 100755 index 000000000..56cd9c322 --- /dev/null +++ b/test/stacktrace/stacktrace_abort_test.sh @@ -0,0 +1,43 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +STACKTRACE_ABORT_MAIN="$1" +WORKDIR="$2" +SELF="$(basename "$STACKTRACE_ABORT_MAIN")" + +"$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|libsystem_|libdyld|\/dyld| _?start \+/d;}' \ + "$WORKDIR/$SELF.actual.txt" \ + > "$WORKDIR/$SELF.normalized.txt" + +cat << EOF > "$WORKDIR/$SELF.expected.txt" + +================================================================================ +signal: 6 (SIGABRT) +pid: + +# 0xADDR crash_handler +0xOFFSET + in $STACKTRACE_ABORT_MAIN +# 0xADDR _ZN31sourcemeta_core_stacktrace_test12crash_middleEv +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..cb0f5437f --- /dev/null +++ b/test/stacktrace/stacktrace_on_demand_main.cc @@ -0,0 +1,24 @@ +#include + +namespace sourcemeta_core_stacktrace_test { + +volatile int sink{0}; + +auto print_deepest() -> void; +auto print_middle() -> void; + +__attribute__((noinline)) auto print_deepest() -> void { + sourcemeta::core::stacktrace(); +} + +__attribute__((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..9ceeebca5 --- /dev/null +++ b/test/stacktrace/stacktrace_on_demand_no_debug_test.ps1 @@ -0,0 +1,31 @@ +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" + +& $StacktraceOnDemandMain *> $Actual +$ExitCode = $LASTEXITCODE +# 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..dd0f1fc88 --- /dev/null +++ b/test/stacktrace/stacktrace_on_demand_test.ps1 @@ -0,0 +1,50 @@ +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" +$Normalized = Join-Path $WorkDir "$Self.normalized.txt" +$Expected = Join-Path $WorkDir "$Self.expected.txt" + +& $StacktraceOnDemandMain *> $Actual +$ExitCode = $LASTEXITCODE +# Exited cleanly after printing the on-demand trace +if ($ExitCode -ne 0) { + throw "Expected exit code 0, got $ExitCode" +} + +Get-Content $Actual + +(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 \+)[^\r\n]*\r?\n in [^\r\n]*\r?\n', '' ` + -replace '(?m)^# [^\r\n]*\r?\n in [^\r\n]*(libsystem_|libdyld|/dyld)[^\r\n]*\r?\n', '' ` + | Set-Content -NoNewline $Normalized + +@" +================================================================================ +pid: + +# 0xADDR sourcemeta::core::stacktrace +0xOFFSET + in $StacktraceOnDemandMain +# 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 +================================================================================ +"@ | Set-Content -NoNewline $Expected + +if ((Get-Content $Normalized -Raw) -ne (Get-Content $Expected -Raw)) { + Compare-Object (Get-Content $Expected) (Get-Content $Normalized) + 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..4510aba0e --- /dev/null +++ b/test/stacktrace/stacktrace_on_demand_test.sh @@ -0,0 +1,41 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +STACKTRACE_ON_DEMAND_MAIN="$1" +WORKDIR="$2" +SELF="$(basename "$STACKTRACE_ON_DEMAND_MAIN")" + +"$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|libsystem_|libdyld|\/dyld| _?start \+/d;}' \ + "$WORKDIR/$SELF.actual.txt" \ + > "$WORKDIR/$SELF.normalized.txt" + +cat << EOF > "$WORKDIR/$SELF.expected.txt" +================================================================================ +pid: + +# 0xADDR _ZN10sourcemeta4core10stacktraceEv +0xOFFSET + in $STACKTRACE_ON_DEMAND_MAIN +# 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..5afed0b32 --- /dev/null +++ b/test/stacktrace/stacktrace_segfault_main.cc @@ -0,0 +1,26 @@ +#include + +namespace sourcemeta_core_stacktrace_test { + +volatile int sink{0}; + +auto crash_deepest() -> void; +auto crash_middle() -> void; + +__attribute__((noinline)) auto crash_deepest() -> void { + volatile int *null_pointer{nullptr}; + *null_pointer = 42; +} + +__attribute__((noinline)) auto crash_middle() -> void { + crash_deepest(); + sink = sink + 1; +} + +} // namespace sourcemeta_core_stacktrace_test + +auto main() -> int { + sourcemeta::core::install_crash_handler(); + 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..312138aeb --- /dev/null +++ b/test/stacktrace/stacktrace_segfault_no_debug_test.ps1 @@ -0,0 +1,34 @@ +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" + +& $StacktraceSegfaultMain *> $Actual +$ExitCode = $LASTEXITCODE +# 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..454e95c03 --- /dev/null +++ b/test/stacktrace/stacktrace_segfault_test.ps1 @@ -0,0 +1,50 @@ +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" +$Normalized = Join-Path $WorkDir "$Self.normalized.txt" +$Expected = Join-Path $WorkDir "$Self.expected.txt" + +& $StacktraceSegfaultMain *> $Actual +$ExitCode = $LASTEXITCODE +# Crashed by a fatal exception +if ($ExitCode -eq 0) { + throw "Expected non-zero exit code, got $ExitCode" +} + +Get-Content $Actual + +(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 \+)[^\r\n]*\r?\n in [^\r\n]*\r?\n', '' ` + -replace '(?m)^# [^\r\n]*\r?\n in [^\r\n]*(libsystem_|libdyld|/dyld)[^\r\n]*\r?\n', '' ` + | Set-Content -NoNewline $Normalized + +@" + +================================================================================ +signal: 0xc0000005 (SEH) +pid: + +# 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 +================================================================================ +"@ | Set-Content -NoNewline $Expected + +if ((Get-Content $Normalized -Raw) -ne (Get-Content $Expected -Raw)) { + Compare-Object (Get-Content $Expected) (Get-Content $Normalized) + 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..35c3ea3d6 --- /dev/null +++ b/test/stacktrace/stacktrace_segfault_test.sh @@ -0,0 +1,43 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +STACKTRACE_SEGFAULT_MAIN="$1" +WORKDIR="$2" +SELF="$(basename "$STACKTRACE_SEGFAULT_MAIN")" + +"$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|libsystem_|libdyld|\/dyld| _?start \+/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 $STACKTRACE_SEGFAULT_MAIN +# 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" From 276d7c138df9ad7d694f3e988ef7a6605233b464 Mon Sep 17 00:00:00 2001 From: Juan Cruz Viotti Date: Tue, 26 May 2026 15:39:42 -0400 Subject: [PATCH 02/12] Fix Signed-off-by: Juan Cruz Viotti --- src/lang/stacktrace/stacktrace_posix.h | 8 +++++--- src/lang/stacktrace/stacktrace_windows.h | 5 +++-- test/stacktrace/stacktrace_abort_main.cc | 7 +++++-- test/stacktrace/stacktrace_abort_test.ps1 | 2 ++ test/stacktrace/stacktrace_abort_test.sh | 2 +- test/stacktrace/stacktrace_on_demand_main.cc | 1 + 6 files changed, 17 insertions(+), 8 deletions(-) diff --git a/src/lang/stacktrace/stacktrace_posix.h b/src/lang/stacktrace/stacktrace_posix.h index 5d4153fdd..e24b81934 100644 --- a/src/lang/stacktrace/stacktrace_posix.h +++ b/src/lang/stacktrace/stacktrace_posix.h @@ -100,8 +100,10 @@ auto write_frame(int file_descriptor, int frame_index, void *address) -> void { write_text(file_descriptor, "\n"); } -auto write_backtrace(int file_descriptor, int frames_to_skip, - void *crash_pc = nullptr) -> void { +__attribute__((noinline)) auto write_backtrace(int file_descriptor, + int frames_to_skip, + void *crash_pc = nullptr) + -> void { int frame_index{0}; if (crash_pc != nullptr) { write_frame(file_descriptor, frame_index, crash_pc); @@ -209,7 +211,7 @@ auto install_crash_handler() -> void { } // NOLINTNEXTLINE(misc-definitions-in-headers) -auto stacktrace() -> void { +__attribute__((noinline)) auto stacktrace() -> void { const int file_descriptor{STDERR_FILENO}; write_text(file_descriptor, separator); write_text(file_descriptor, "pid: "); diff --git a/src/lang/stacktrace/stacktrace_windows.h b/src/lang/stacktrace/stacktrace_windows.h index f23fc0798..3c0eb1670 100644 --- a/src/lang/stacktrace/stacktrace_windows.h +++ b/src/lang/stacktrace/stacktrace_windows.h @@ -37,7 +37,8 @@ auto write_text(int file_descriptor, const char *text) -> void { ::_write(file_descriptor, text, static_cast(std::strlen(text))); } -auto write_frames(int file_descriptor, USHORT frames_to_skip) -> void { +__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)}; @@ -108,7 +109,7 @@ auto install_crash_handler() -> void { } // NOLINTNEXTLINE(misc-definitions-in-headers) -auto stacktrace() -> void { +__declspec(noinline) auto stacktrace() -> void { const int file_descriptor{2}; write_text(file_descriptor, separator); char header[256]; diff --git a/test/stacktrace/stacktrace_abort_main.cc b/test/stacktrace/stacktrace_abort_main.cc index 6ea228206..8fa1bb0a2 100644 --- a/test/stacktrace/stacktrace_abort_main.cc +++ b/test/stacktrace/stacktrace_abort_main.cc @@ -1,6 +1,6 @@ #include -#include // std::abort +#include // raise, SIGABRT namespace sourcemeta_core_stacktrace_test { @@ -9,7 +9,10 @@ volatile int sink{0}; auto crash_deepest() -> void; auto crash_middle() -> void; -__attribute__((noinline)) auto crash_deepest() -> void { std::abort(); } +__attribute__((noinline)) auto crash_deepest() -> void { + ::raise(SIGABRT); + sink = sink + 1; +} __attribute__((noinline)) auto crash_middle() -> void { crash_deepest(); diff --git a/test/stacktrace/stacktrace_abort_test.ps1 b/test/stacktrace/stacktrace_abort_test.ps1 index c241a3773..2022313da 100644 --- a/test/stacktrace/stacktrace_abort_test.ps1 +++ b/test/stacktrace/stacktrace_abort_test.ps1 @@ -35,6 +35,8 @@ Get-Content $Actual signal: 0x80000003 (SEH) pid: +# 0xADDR sourcemeta_core_stacktrace_test::crash_deepest +0xOFFSET + in $StacktraceAbortMain # 0xADDR sourcemeta_core_stacktrace_test::crash_middle +0xOFFSET in $StacktraceAbortMain # 0xADDR main +0xOFFSET diff --git a/test/stacktrace/stacktrace_abort_test.sh b/test/stacktrace/stacktrace_abort_test.sh index 56cd9c322..e8d128f28 100755 --- a/test/stacktrace/stacktrace_abort_test.sh +++ b/test/stacktrace/stacktrace_abort_test.sh @@ -31,7 +31,7 @@ pid: # 0xADDR crash_handler +0xOFFSET in $STACKTRACE_ABORT_MAIN -# 0xADDR _ZN31sourcemeta_core_stacktrace_test12crash_middleEv +0xOFFSET +# 0xADDR _ZN31sourcemeta_core_stacktrace_test13crash_deepestEv +0xOFFSET in $STACKTRACE_ABORT_MAIN # 0xADDR _ZN31sourcemeta_core_stacktrace_test12crash_middleEv +0xOFFSET in $STACKTRACE_ABORT_MAIN diff --git a/test/stacktrace/stacktrace_on_demand_main.cc b/test/stacktrace/stacktrace_on_demand_main.cc index cb0f5437f..99b6d4391 100644 --- a/test/stacktrace/stacktrace_on_demand_main.cc +++ b/test/stacktrace/stacktrace_on_demand_main.cc @@ -9,6 +9,7 @@ auto print_middle() -> void; __attribute__((noinline)) auto print_deepest() -> void { sourcemeta::core::stacktrace(); + sink = sink + 1; } __attribute__((noinline)) auto print_middle() -> void { From a114a3ca6ae11e317efe90d89ac1cff04c486300 Mon Sep 17 00:00:00 2001 From: Juan Cruz Viotti Date: Tue, 26 May 2026 16:07:16 -0400 Subject: [PATCH 03/12] Fix Signed-off-by: Juan Cruz Viotti --- test/stacktrace/CMakeLists.txt | 6 ++++-- test/stacktrace/stacktrace_abort_test.ps1 | 11 ++++++++++- test/stacktrace/stacktrace_abort_test.sh | 8 +++++++- test/stacktrace/stacktrace_on_demand_test.ps1 | 11 +++++++++-- test/stacktrace/stacktrace_on_demand_test.sh | 8 +++++++- test/stacktrace/stacktrace_segfault_test.ps1 | 11 ++++++++++- test/stacktrace/stacktrace_segfault_test.sh | 8 +++++++- 7 files changed, 54 insertions(+), 9 deletions(-) diff --git a/test/stacktrace/CMakeLists.txt b/test/stacktrace/CMakeLists.txt index 6dcccdb4f..65ecafd4a 100644 --- a/test/stacktrace/CMakeLists.txt +++ b/test/stacktrace/CMakeLists.txt @@ -11,7 +11,8 @@ macro(add_stacktrace_test_unix scenario debug_flag) add_test(NAME core.stacktrace.${scenario}.${debug_flag}.e2e COMMAND "${CMAKE_CURRENT_SOURCE_DIR}/stacktrace_${scenario}_test.sh" "$" - "${CMAKE_CURRENT_BINARY_DIR}") + "${CMAKE_CURRENT_BINARY_DIR}" + "$") endif() endmacro() @@ -26,7 +27,8 @@ macro(add_stacktrace_test_windows scenario) COMMAND powershell -ExecutionPolicy Bypass -File "${CMAKE_CURRENT_SOURCE_DIR}/stacktrace_${scenario}_test.ps1" "$" - "${CMAKE_CURRENT_BINARY_DIR}") + "${CMAKE_CURRENT_BINARY_DIR}" + "$") endif() endmacro() diff --git a/test/stacktrace/stacktrace_abort_test.ps1 b/test/stacktrace/stacktrace_abort_test.ps1 index 2022313da..e7b9fe821 100644 --- a/test/stacktrace/stacktrace_abort_test.ps1 +++ b/test/stacktrace/stacktrace_abort_test.ps1 @@ -2,11 +2,18 @@ param( [Parameter(Mandatory=$true)] [string]$StacktraceAbortMain, [Parameter(Mandatory=$true)] - [string]$WorkDir + [string]$WorkDir, + [Parameter(Mandatory=$true)] + [string]$StacktraceLibrary ) $ErrorActionPreference = "Stop" $Self = [IO.Path]::GetFileName($StacktraceAbortMain) +$LibraryPath = if ($StacktraceLibrary -match '\.(lib|a)$') { + $StacktraceAbortMain +} else { + $StacktraceLibrary +} $Actual = Join-Path $WorkDir "$Self.actual.txt" $Normalized = Join-Path $WorkDir "$Self.normalized.txt" $Expected = Join-Path $WorkDir "$Self.expected.txt" @@ -35,6 +42,8 @@ Get-Content $Actual signal: 0x80000003 (SEH) pid: +# 0xADDR crash_handler +0xOFFSET + in $LibraryPath # 0xADDR sourcemeta_core_stacktrace_test::crash_deepest +0xOFFSET in $StacktraceAbortMain # 0xADDR sourcemeta_core_stacktrace_test::crash_middle +0xOFFSET diff --git a/test/stacktrace/stacktrace_abort_test.sh b/test/stacktrace/stacktrace_abort_test.sh index e8d128f28..731929866 100755 --- a/test/stacktrace/stacktrace_abort_test.sh +++ b/test/stacktrace/stacktrace_abort_test.sh @@ -5,8 +5,14 @@ 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="$STACKTRACE_LIBRARY" ;; +esac + "$STACKTRACE_ABORT_MAIN" > "$WORKDIR/$SELF.actual.txt" 2>&1 \ && EXIT_CODE="$?" || EXIT_CODE="$?" # Crashed by a fatal signal @@ -30,7 +36,7 @@ signal: 6 (SIGABRT) pid: # 0xADDR crash_handler +0xOFFSET - in $STACKTRACE_ABORT_MAIN + in $LIBRARY_PATH # 0xADDR _ZN31sourcemeta_core_stacktrace_test13crash_deepestEv +0xOFFSET in $STACKTRACE_ABORT_MAIN # 0xADDR _ZN31sourcemeta_core_stacktrace_test12crash_middleEv +0xOFFSET diff --git a/test/stacktrace/stacktrace_on_demand_test.ps1 b/test/stacktrace/stacktrace_on_demand_test.ps1 index dd0f1fc88..018f310ce 100644 --- a/test/stacktrace/stacktrace_on_demand_test.ps1 +++ b/test/stacktrace/stacktrace_on_demand_test.ps1 @@ -2,11 +2,18 @@ param( [Parameter(Mandatory=$true)] [string]$StacktraceOnDemandMain, [Parameter(Mandatory=$true)] - [string]$WorkDir + [string]$WorkDir, + [Parameter(Mandatory=$true)] + [string]$StacktraceLibrary ) $ErrorActionPreference = "Stop" $Self = [IO.Path]::GetFileName($StacktraceOnDemandMain) +$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" @@ -34,7 +41,7 @@ Get-Content $Actual pid: # 0xADDR sourcemeta::core::stacktrace +0xOFFSET - in $StacktraceOnDemandMain + in $LibraryPath # 0xADDR sourcemeta_core_stacktrace_test::print_deepest +0xOFFSET in $StacktraceOnDemandMain # 0xADDR sourcemeta_core_stacktrace_test::print_middle +0xOFFSET diff --git a/test/stacktrace/stacktrace_on_demand_test.sh b/test/stacktrace/stacktrace_on_demand_test.sh index 4510aba0e..e05b053a4 100755 --- a/test/stacktrace/stacktrace_on_demand_test.sh +++ b/test/stacktrace/stacktrace_on_demand_test.sh @@ -5,8 +5,14 @@ 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="$STACKTRACE_LIBRARY" ;; +esac + "$STACKTRACE_ON_DEMAND_MAIN" > "$WORKDIR/$SELF.actual.txt" 2>&1 \ && EXIT_CODE="$?" || EXIT_CODE="$?" # Exited cleanly after printing the on-demand trace @@ -28,7 +34,7 @@ cat << EOF > "$WORKDIR/$SELF.expected.txt" pid: # 0xADDR _ZN10sourcemeta4core10stacktraceEv +0xOFFSET - in $STACKTRACE_ON_DEMAND_MAIN + in $LIBRARY_PATH # 0xADDR _ZN31sourcemeta_core_stacktrace_test13print_deepestEv +0xOFFSET in $STACKTRACE_ON_DEMAND_MAIN # 0xADDR _ZN31sourcemeta_core_stacktrace_test12print_middleEv +0xOFFSET diff --git a/test/stacktrace/stacktrace_segfault_test.ps1 b/test/stacktrace/stacktrace_segfault_test.ps1 index 454e95c03..25252578a 100644 --- a/test/stacktrace/stacktrace_segfault_test.ps1 +++ b/test/stacktrace/stacktrace_segfault_test.ps1 @@ -2,11 +2,18 @@ param( [Parameter(Mandatory=$true)] [string]$StacktraceSegfaultMain, [Parameter(Mandatory=$true)] - [string]$WorkDir + [string]$WorkDir, + [Parameter(Mandatory=$true)] + [string]$StacktraceLibrary ) $ErrorActionPreference = "Stop" $Self = [IO.Path]::GetFileName($StacktraceSegfaultMain) +$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" @@ -37,6 +44,8 @@ pid: # 0xADDR sourcemeta_core_stacktrace_test::crash_deepest +0xOFFSET in $StacktraceSegfaultMain +# 0xADDR crash_handler +0xOFFSET + in $LibraryPath # 0xADDR sourcemeta_core_stacktrace_test::crash_middle +0xOFFSET in $StacktraceSegfaultMain # 0xADDR main +0xOFFSET diff --git a/test/stacktrace/stacktrace_segfault_test.sh b/test/stacktrace/stacktrace_segfault_test.sh index 35c3ea3d6..1c2a12dbc 100755 --- a/test/stacktrace/stacktrace_segfault_test.sh +++ b/test/stacktrace/stacktrace_segfault_test.sh @@ -5,8 +5,14 @@ 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="$STACKTRACE_LIBRARY" ;; +esac + "$STACKTRACE_SEGFAULT_MAIN" > "$WORKDIR/$SELF.actual.txt" 2>&1 \ && EXIT_CODE="$?" || EXIT_CODE="$?" # Crashed by a fatal signal @@ -32,7 +38,7 @@ pid: # 0xADDR _ZN31sourcemeta_core_stacktrace_test13crash_deepestEv +0xOFFSET in $STACKTRACE_SEGFAULT_MAIN # 0xADDR crash_handler +0xOFFSET - in $STACKTRACE_SEGFAULT_MAIN + in $LIBRARY_PATH # 0xADDR _ZN31sourcemeta_core_stacktrace_test12crash_middleEv +0xOFFSET in $STACKTRACE_SEGFAULT_MAIN # 0xADDR main +0xOFFSET From 81cab0b31f5f3ed10d06e26a9e7b4c36a0e78617 Mon Sep 17 00:00:00 2001 From: Juan Cruz Viotti Date: Tue, 26 May 2026 16:09:24 -0400 Subject: [PATCH 04/12] MSVC Signed-off-by: Juan Cruz Viotti --- test/stacktrace/stacktrace_abort_main.cc | 10 ++++++++-- test/stacktrace/stacktrace_on_demand_main.cc | 10 ++++++++-- test/stacktrace/stacktrace_segfault_main.cc | 10 ++++++++-- 3 files changed, 24 insertions(+), 6 deletions(-) diff --git a/test/stacktrace/stacktrace_abort_main.cc b/test/stacktrace/stacktrace_abort_main.cc index 8fa1bb0a2..0dd3f0a82 100644 --- a/test/stacktrace/stacktrace_abort_main.cc +++ b/test/stacktrace/stacktrace_abort_main.cc @@ -2,6 +2,12 @@ #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}; @@ -9,12 +15,12 @@ volatile int sink{0}; auto crash_deepest() -> void; auto crash_middle() -> void; -__attribute__((noinline)) auto crash_deepest() -> void { +STACKTRACE_TEST_NOINLINE auto crash_deepest() -> void { ::raise(SIGABRT); sink = sink + 1; } -__attribute__((noinline)) auto crash_middle() -> void { +STACKTRACE_TEST_NOINLINE auto crash_middle() -> void { crash_deepest(); sink = sink + 1; } diff --git a/test/stacktrace/stacktrace_on_demand_main.cc b/test/stacktrace/stacktrace_on_demand_main.cc index 99b6d4391..ad23f73d6 100644 --- a/test/stacktrace/stacktrace_on_demand_main.cc +++ b/test/stacktrace/stacktrace_on_demand_main.cc @@ -1,5 +1,11 @@ #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}; @@ -7,12 +13,12 @@ volatile int sink{0}; auto print_deepest() -> void; auto print_middle() -> void; -__attribute__((noinline)) auto print_deepest() -> void { +STACKTRACE_TEST_NOINLINE auto print_deepest() -> void { sourcemeta::core::stacktrace(); sink = sink + 1; } -__attribute__((noinline)) auto print_middle() -> void { +STACKTRACE_TEST_NOINLINE auto print_middle() -> void { print_deepest(); sink = sink + 1; } diff --git a/test/stacktrace/stacktrace_segfault_main.cc b/test/stacktrace/stacktrace_segfault_main.cc index 5afed0b32..67f96f835 100644 --- a/test/stacktrace/stacktrace_segfault_main.cc +++ b/test/stacktrace/stacktrace_segfault_main.cc @@ -1,5 +1,11 @@ #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}; @@ -7,12 +13,12 @@ volatile int sink{0}; auto crash_deepest() -> void; auto crash_middle() -> void; -__attribute__((noinline)) auto crash_deepest() -> void { +STACKTRACE_TEST_NOINLINE auto crash_deepest() -> void { volatile int *null_pointer{nullptr}; *null_pointer = 42; } -__attribute__((noinline)) auto crash_middle() -> void { +STACKTRACE_TEST_NOINLINE auto crash_middle() -> void { crash_deepest(); sink = sink + 1; } From 7231f9310bd29e782b0d38f2cf7ece7831cccd59 Mon Sep 17 00:00:00 2001 From: Juan Cruz Viotti Date: Tue, 26 May 2026 17:01:19 -0400 Subject: [PATCH 05/12] Fix Linux Signed-off-by: Juan Cruz Viotti --- src/lang/stacktrace/stacktrace_posix.h | 32 +++++++++++++++----- test/stacktrace/CMakeLists.txt | 4 +-- test/stacktrace/stacktrace_abort_test.sh | 8 +++-- test/stacktrace/stacktrace_on_demand_test.sh | 8 +++-- test/stacktrace/stacktrace_segfault_main.cc | 2 +- test/stacktrace/stacktrace_segfault_test.sh | 8 +++-- 6 files changed, 45 insertions(+), 17 deletions(-) diff --git a/src/lang/stacktrace/stacktrace_posix.h b/src/lang/stacktrace/stacktrace_posix.h index e24b81934..6116863f9 100644 --- a/src/lang/stacktrace/stacktrace_posix.h +++ b/src/lang/stacktrace/stacktrace_posix.h @@ -104,16 +104,32 @@ __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; + } } - std::array frames{}; - const int captured{::backtrace(frames.data(), maximum_frames)}; + for (int index{frames_to_skip}; index < captured; ++index) { - write_frame(file_descriptor, frame_index, - frames[static_cast(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; } } @@ -145,8 +161,8 @@ constexpr const char *separator{"========================================" std::atomic crash_handler_installed{false}; -extern "C" auto crash_handler(int signal_number, siginfo_t * /*info*/, - void *context) -> void { +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); @@ -194,7 +210,7 @@ extern "C" auto crash_handler(int signal_number, siginfo_t * /*info*/, namespace sourcemeta::core { // NOLINTNEXTLINE(misc-definitions-in-headers) -auto install_crash_handler() -> void { +__attribute__((visibility("default"))) auto install_crash_handler() -> void { bool expected{false}; if (!crash_handler_installed.compare_exchange_strong(expected, true)) { return; @@ -211,7 +227,7 @@ auto install_crash_handler() -> void { } // NOLINTNEXTLINE(misc-definitions-in-headers) -__attribute__((noinline)) auto stacktrace() -> void { +__attribute__((noinline, visibility("default"))) auto stacktrace() -> void { const int file_descriptor{STDERR_FILENO}; write_text(file_descriptor, separator); write_text(file_descriptor, "pid: "); diff --git a/test/stacktrace/CMakeLists.txt b/test/stacktrace/CMakeLists.txt index 65ecafd4a..037ca085c 100644 --- a/test/stacktrace/CMakeLists.txt +++ b/test/stacktrace/CMakeLists.txt @@ -4,9 +4,9 @@ macro(add_stacktrace_test_unix scenario 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) + target_compile_options(${target} PRIVATE -g0 -fvisibility=default) else() - target_compile_options(${target} PRIVATE -${debug_flag}) + 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" diff --git a/test/stacktrace/stacktrace_abort_test.sh b/test/stacktrace/stacktrace_abort_test.sh index 731929866..42d4cf0a8 100755 --- a/test/stacktrace/stacktrace_abort_test.sh +++ b/test/stacktrace/stacktrace_abort_test.sh @@ -10,7 +10,7 @@ SELF="$(basename "$STACKTRACE_ABORT_MAIN")" case "$STACKTRACE_LIBRARY" in *.a|*.lib) LIBRARY_PATH="$STACKTRACE_ABORT_MAIN" ;; - *) LIBRARY_PATH="$STACKTRACE_LIBRARY" ;; + *) 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 \ @@ -25,7 +25,11 @@ sed -E \ -e 's/\+0xADDR/+0xOFFSET/g' \ -e 's/^pid:[[:space:]]+[0-9]+/pid: /' \ -e 's/^#[0-9]+ /# /' \ - -e '/^# /{N;/_sigtramp|__restore_rt|libsystem_|libdyld|\/dyld| _?start \+/d;}' \ + -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" diff --git a/test/stacktrace/stacktrace_on_demand_test.sh b/test/stacktrace/stacktrace_on_demand_test.sh index e05b053a4..e078f7e94 100755 --- a/test/stacktrace/stacktrace_on_demand_test.sh +++ b/test/stacktrace/stacktrace_on_demand_test.sh @@ -10,7 +10,7 @@ SELF="$(basename "$STACKTRACE_ON_DEMAND_MAIN")" case "$STACKTRACE_LIBRARY" in *.a|*.lib) LIBRARY_PATH="$STACKTRACE_ON_DEMAND_MAIN" ;; - *) LIBRARY_PATH="$STACKTRACE_LIBRARY" ;; + *) 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 \ @@ -25,7 +25,11 @@ sed -E \ -e 's/\+0xADDR/+0xOFFSET/g' \ -e 's/^pid:[[:space:]]+[0-9]+/pid: /' \ -e 's/^#[0-9]+ /# /' \ - -e '/^# /{N;/_sigtramp|__restore_rt|libsystem_|libdyld|\/dyld| _?start \+/d;}' \ + -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" diff --git a/test/stacktrace/stacktrace_segfault_main.cc b/test/stacktrace/stacktrace_segfault_main.cc index 67f96f835..68143b26b 100644 --- a/test/stacktrace/stacktrace_segfault_main.cc +++ b/test/stacktrace/stacktrace_segfault_main.cc @@ -14,7 +14,7 @@ auto crash_deepest() -> void; auto crash_middle() -> void; STACKTRACE_TEST_NOINLINE auto crash_deepest() -> void { - volatile int *null_pointer{nullptr}; + volatile int *volatile null_pointer{nullptr}; *null_pointer = 42; } diff --git a/test/stacktrace/stacktrace_segfault_test.sh b/test/stacktrace/stacktrace_segfault_test.sh index 1c2a12dbc..ecc1c10fb 100755 --- a/test/stacktrace/stacktrace_segfault_test.sh +++ b/test/stacktrace/stacktrace_segfault_test.sh @@ -10,7 +10,7 @@ SELF="$(basename "$STACKTRACE_SEGFAULT_MAIN")" case "$STACKTRACE_LIBRARY" in *.a|*.lib) LIBRARY_PATH="$STACKTRACE_SEGFAULT_MAIN" ;; - *) LIBRARY_PATH="$STACKTRACE_LIBRARY" ;; + *) 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 \ @@ -25,7 +25,11 @@ sed -E \ -e 's/\+0xADDR/+0xOFFSET/g' \ -e 's/^pid:[[:space:]]+[0-9]+/pid: /' \ -e 's/^#[0-9]+ /# /' \ - -e '/^# /{N;/_sigtramp|__restore_rt|libsystem_|libdyld|\/dyld| _?start \+/d;}' \ + -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" From a01d39d817cb1d9af1d2ec0b621cf48a07780e2d Mon Sep 17 00:00:00 2001 From: Juan Cruz Viotti Date: Tue, 26 May 2026 17:13:03 -0400 Subject: [PATCH 06/12] No sanitizer Signed-off-by: Juan Cruz Viotti --- CMakeLists.txt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 94a3302ab..a06d37497 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -250,7 +250,9 @@ if(SOURCEMETA_CORE_TESTS) add_subdirectory(test/text) endif() - if(SOURCEMETA_CORE_LANG_STACKTRACE) + if(SOURCEMETA_CORE_LANG_STACKTRACE + AND NOT SOURCEMETA_CORE_ADDRESS_SANITIZER + AND NOT SOURCEMETA_CORE_UNDEFINED_SANITIZER) add_subdirectory(test/stacktrace) endif() From 90cfaec2d654d2693e538ee9f588aae5095cf163 Mon Sep 17 00:00:00 2001 From: Juan Cruz Viotti Date: Tue, 26 May 2026 17:15:34 -0400 Subject: [PATCH 07/12] Fix Signed-off-by: Juan Cruz Viotti --- test/stacktrace/stacktrace_abort_no_debug_test.ps1 | 4 +++- test/stacktrace/stacktrace_abort_test.ps1 | 4 +++- test/stacktrace/stacktrace_on_demand_no_debug_test.ps1 | 4 +++- test/stacktrace/stacktrace_on_demand_test.ps1 | 4 +++- test/stacktrace/stacktrace_segfault_no_debug_test.ps1 | 4 +++- test/stacktrace/stacktrace_segfault_test.ps1 | 7 ++++++- 6 files changed, 21 insertions(+), 6 deletions(-) diff --git a/test/stacktrace/stacktrace_abort_no_debug_test.ps1 b/test/stacktrace/stacktrace_abort_no_debug_test.ps1 index 2a70eba98..39c2dd564 100644 --- a/test/stacktrace/stacktrace_abort_no_debug_test.ps1 +++ b/test/stacktrace/stacktrace_abort_no_debug_test.ps1 @@ -9,8 +9,10 @@ $ErrorActionPreference = "Stop" $Self = [IO.Path]::GetFileName($StacktraceAbortMain) $Actual = Join-Path $WorkDir "$Self.actual.txt" -& $StacktraceAbortMain *> $Actual +$ErrorActionPreference = "Continue" +& $StacktraceAbortMain > $Actual 2>&1 $ExitCode = $LASTEXITCODE +$ErrorActionPreference = "Stop" # Crashed by a fatal exception if ($ExitCode -eq 0) { throw "Expected non-zero exit code, got $ExitCode" diff --git a/test/stacktrace/stacktrace_abort_test.ps1 b/test/stacktrace/stacktrace_abort_test.ps1 index e7b9fe821..55363712d 100644 --- a/test/stacktrace/stacktrace_abort_test.ps1 +++ b/test/stacktrace/stacktrace_abort_test.ps1 @@ -18,8 +18,10 @@ $Actual = Join-Path $WorkDir "$Self.actual.txt" $Normalized = Join-Path $WorkDir "$Self.normalized.txt" $Expected = Join-Path $WorkDir "$Self.expected.txt" -& $StacktraceAbortMain *> $Actual +$ErrorActionPreference = "Continue" +& $StacktraceAbortMain > $Actual 2>&1 $ExitCode = $LASTEXITCODE +$ErrorActionPreference = "Stop" # Crashed by a fatal exception if ($ExitCode -eq 0) { throw "Expected non-zero exit code, got $ExitCode" diff --git a/test/stacktrace/stacktrace_on_demand_no_debug_test.ps1 b/test/stacktrace/stacktrace_on_demand_no_debug_test.ps1 index 9ceeebca5..432582d21 100644 --- a/test/stacktrace/stacktrace_on_demand_no_debug_test.ps1 +++ b/test/stacktrace/stacktrace_on_demand_no_debug_test.ps1 @@ -9,8 +9,10 @@ $ErrorActionPreference = "Stop" $Self = [IO.Path]::GetFileName($StacktraceOnDemandMain) $Actual = Join-Path $WorkDir "$Self.actual.txt" -& $StacktraceOnDemandMain *> $Actual +$ErrorActionPreference = "Continue" +& $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" diff --git a/test/stacktrace/stacktrace_on_demand_test.ps1 b/test/stacktrace/stacktrace_on_demand_test.ps1 index 018f310ce..ea948d843 100644 --- a/test/stacktrace/stacktrace_on_demand_test.ps1 +++ b/test/stacktrace/stacktrace_on_demand_test.ps1 @@ -18,8 +18,10 @@ $Actual = Join-Path $WorkDir "$Self.actual.txt" $Normalized = Join-Path $WorkDir "$Self.normalized.txt" $Expected = Join-Path $WorkDir "$Self.expected.txt" -& $StacktraceOnDemandMain *> $Actual +$ErrorActionPreference = "Continue" +& $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" diff --git a/test/stacktrace/stacktrace_segfault_no_debug_test.ps1 b/test/stacktrace/stacktrace_segfault_no_debug_test.ps1 index 312138aeb..c2670447f 100644 --- a/test/stacktrace/stacktrace_segfault_no_debug_test.ps1 +++ b/test/stacktrace/stacktrace_segfault_no_debug_test.ps1 @@ -9,8 +9,10 @@ $ErrorActionPreference = "Stop" $Self = [IO.Path]::GetFileName($StacktraceSegfaultMain) $Actual = Join-Path $WorkDir "$Self.actual.txt" -& $StacktraceSegfaultMain *> $Actual +$ErrorActionPreference = "Continue" +& $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" diff --git a/test/stacktrace/stacktrace_segfault_test.ps1 b/test/stacktrace/stacktrace_segfault_test.ps1 index 25252578a..a13d43cf3 100644 --- a/test/stacktrace/stacktrace_segfault_test.ps1 +++ b/test/stacktrace/stacktrace_segfault_test.ps1 @@ -18,8 +18,13 @@ $Actual = Join-Path $WorkDir "$Self.actual.txt" $Normalized = Join-Path $WorkDir "$Self.normalized.txt" $Expected = Join-Path $WorkDir "$Self.expected.txt" -& $StacktraceSegfaultMain *> $Actual +# `$ErrorActionPreference = "Stop"` would treat the crashing binary's stderr +# output as a NativeCommandError and abort the script; soften it just for the +# native invocation. +$ErrorActionPreference = "Continue" +& $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" From b5cfad04e7c71be5630dd059a26f61bc39bc6523 Mon Sep 17 00:00:00 2001 From: Juan Cruz Viotti Date: Tue, 26 May 2026 17:18:46 -0400 Subject: [PATCH 08/12] More Signed-off-by: Juan Cruz Viotti --- src/lang/stacktrace/include/sourcemeta/core/stacktrace.h | 4 ++-- src/lang/stacktrace/stacktrace_posix.h | 7 +++---- src/lang/stacktrace/stacktrace_windows.h | 2 +- test/stacktrace/stacktrace_abort_main.cc | 2 +- test/stacktrace/stacktrace_segfault_main.cc | 2 +- 5 files changed, 8 insertions(+), 9 deletions(-) diff --git a/src/lang/stacktrace/include/sourcemeta/core/stacktrace.h b/src/lang/stacktrace/include/sourcemeta/core/stacktrace.h index 9131ae67c..52be2935d 100644 --- a/src/lang/stacktrace/include/sourcemeta/core/stacktrace.h +++ b/src/lang/stacktrace/include/sourcemeta/core/stacktrace.h @@ -25,12 +25,12 @@ namespace sourcemeta::core { /// #include /// /// auto main() -> int { -/// sourcemeta::core::install_crash_handler(); +/// sourcemeta::core::stacktrace_on_crash(); /// // ... rest of the program /// } /// ``` SOURCEMETA_CORE_STACKTRACE_EXPORT -auto install_crash_handler() -> void; +auto stacktrace_on_crash() -> void; /// @ingroup stacktrace /// diff --git a/src/lang/stacktrace/stacktrace_posix.h b/src/lang/stacktrace/stacktrace_posix.h index 6116863f9..07bb250c9 100644 --- a/src/lang/stacktrace/stacktrace_posix.h +++ b/src/lang/stacktrace/stacktrace_posix.h @@ -138,7 +138,8 @@ auto extract_crash_pc(void *context) -> void * { if (context == nullptr) { return nullptr; } - const auto *user_context{static_cast(context)}; + [[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; @@ -149,8 +150,6 @@ auto extract_crash_pc(void *context) -> void * { #elif defined(__linux__) && defined(__x86_64__) program_counter = static_cast(user_context->uc_mcontext.gregs[16]); -#else - (void)user_context; #endif // NOLINTNEXTLINE(performance-no-int-to-ptr) return reinterpret_cast(program_counter); @@ -210,7 +209,7 @@ crash_handler(int signal_number, siginfo_t * /*info*/, void *context) -> void { namespace sourcemeta::core { // NOLINTNEXTLINE(misc-definitions-in-headers) -__attribute__((visibility("default"))) auto install_crash_handler() -> void { +__attribute__((visibility("default"))) auto stacktrace_on_crash() -> void { bool expected{false}; if (!crash_handler_installed.compare_exchange_strong(expected, true)) { return; diff --git a/src/lang/stacktrace/stacktrace_windows.h b/src/lang/stacktrace/stacktrace_windows.h index 3c0eb1670..bc0625f08 100644 --- a/src/lang/stacktrace/stacktrace_windows.h +++ b/src/lang/stacktrace/stacktrace_windows.h @@ -100,7 +100,7 @@ auto WINAPI crash_filter(EXCEPTION_POINTERS *information) -> LONG { namespace sourcemeta::core { // NOLINTNEXTLINE(misc-definitions-in-headers) -auto install_crash_handler() -> void { +auto stacktrace_on_crash() -> void { bool expected{false}; if (!crash_handler_installed.compare_exchange_strong(expected, true)) { return; diff --git a/test/stacktrace/stacktrace_abort_main.cc b/test/stacktrace/stacktrace_abort_main.cc index 0dd3f0a82..a6644274d 100644 --- a/test/stacktrace/stacktrace_abort_main.cc +++ b/test/stacktrace/stacktrace_abort_main.cc @@ -28,7 +28,7 @@ STACKTRACE_TEST_NOINLINE auto crash_middle() -> void { } // namespace sourcemeta_core_stacktrace_test auto main() -> int { - sourcemeta::core::install_crash_handler(); + sourcemeta::core::stacktrace_on_crash(); sourcemeta_core_stacktrace_test::crash_middle(); return 0; } diff --git a/test/stacktrace/stacktrace_segfault_main.cc b/test/stacktrace/stacktrace_segfault_main.cc index 68143b26b..053c98b94 100644 --- a/test/stacktrace/stacktrace_segfault_main.cc +++ b/test/stacktrace/stacktrace_segfault_main.cc @@ -26,7 +26,7 @@ STACKTRACE_TEST_NOINLINE auto crash_middle() -> void { } // namespace sourcemeta_core_stacktrace_test auto main() -> int { - sourcemeta::core::install_crash_handler(); + sourcemeta::core::stacktrace_on_crash(); sourcemeta_core_stacktrace_test::crash_middle(); return 0; } From 8bcd1d7a6183c115e6168dacc12db73b47957ad7 Mon Sep 17 00:00:00 2001 From: Juan Cruz Viotti Date: Tue, 26 May 2026 17:37:28 -0400 Subject: [PATCH 09/12] MSVC Signed-off-by: Juan Cruz Viotti --- src/lang/stacktrace/stacktrace_windows.h | 8 +-- test/stacktrace/CMakeLists.txt | 3 - .../stacktrace_abort_no_debug_test.ps1 | 36 ----------- test/stacktrace/stacktrace_abort_test.ps1 | 61 ------------------- test/stacktrace/stacktrace_on_demand_test.ps1 | 4 +- test/stacktrace/stacktrace_segfault_test.ps1 | 8 +-- 6 files changed, 10 insertions(+), 110 deletions(-) delete mode 100644 test/stacktrace/stacktrace_abort_no_debug_test.ps1 delete mode 100644 test/stacktrace/stacktrace_abort_test.ps1 diff --git a/src/lang/stacktrace/stacktrace_windows.h b/src/lang/stacktrace/stacktrace_windows.h index bc0625f08..0b089326e 100644 --- a/src/lang/stacktrace/stacktrace_windows.h +++ b/src/lang/stacktrace/stacktrace_windows.h @@ -76,7 +76,9 @@ __declspec(noinline) auto write_frames(int file_descriptor, } } -auto WINAPI crash_filter(EXCEPTION_POINTERS *information) -> LONG { +} // namespace + +extern "C" auto WINAPI crash_handler(EXCEPTION_POINTERS *information) -> LONG { const int file_descriptor{2}; write_text(file_descriptor, "\n"); write_text(file_descriptor, separator); @@ -95,8 +97,6 @@ auto WINAPI crash_filter(EXCEPTION_POINTERS *information) -> LONG { return EXCEPTION_CONTINUE_SEARCH; } -} // namespace - namespace sourcemeta::core { // NOLINTNEXTLINE(misc-definitions-in-headers) @@ -105,7 +105,7 @@ auto stacktrace_on_crash() -> void { if (!crash_handler_installed.compare_exchange_strong(expected, true)) { return; } - ::SetUnhandledExceptionFilter(&crash_filter); + ::SetUnhandledExceptionFilter(&crash_handler); } // NOLINTNEXTLINE(misc-definitions-in-headers) diff --git a/test/stacktrace/CMakeLists.txt b/test/stacktrace/CMakeLists.txt index 037ca085c..c30cb513a 100644 --- a/test/stacktrace/CMakeLists.txt +++ b/test/stacktrace/CMakeLists.txt @@ -56,9 +56,6 @@ add_stacktrace_test_unix(on_demand g1) add_stacktrace_test_unix(on_demand none) add_stacktrace_test_windows(segfault) -add_stacktrace_test_windows(abort) add_stacktrace_test_windows(on_demand) - add_stacktrace_test_windows_no_debug(segfault) -add_stacktrace_test_windows_no_debug(abort) add_stacktrace_test_windows_no_debug(on_demand) diff --git a/test/stacktrace/stacktrace_abort_no_debug_test.ps1 b/test/stacktrace/stacktrace_abort_no_debug_test.ps1 deleted file mode 100644 index 39c2dd564..000000000 --- a/test/stacktrace/stacktrace_abort_no_debug_test.ps1 +++ /dev/null @@ -1,36 +0,0 @@ -param( - [Parameter(Mandatory=$true)] - [string]$StacktraceAbortMain, - [Parameter(Mandatory=$true)] - [string]$WorkDir -) - -$ErrorActionPreference = "Stop" -$Self = [IO.Path]::GetFileName($StacktraceAbortMain) -$Actual = Join-Path $WorkDir "$Self.actual.txt" - -$ErrorActionPreference = "Continue" -& $StacktraceAbortMain > $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_abort_test.ps1 b/test/stacktrace/stacktrace_abort_test.ps1 deleted file mode 100644 index 55363712d..000000000 --- a/test/stacktrace/stacktrace_abort_test.ps1 +++ /dev/null @@ -1,61 +0,0 @@ -param( - [Parameter(Mandatory=$true)] - [string]$StacktraceAbortMain, - [Parameter(Mandatory=$true)] - [string]$WorkDir, - [Parameter(Mandatory=$true)] - [string]$StacktraceLibrary -) - -$ErrorActionPreference = "Stop" -$Self = [IO.Path]::GetFileName($StacktraceAbortMain) -$LibraryPath = if ($StacktraceLibrary -match '\.(lib|a)$') { - $StacktraceAbortMain -} 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" -& $StacktraceAbortMain > $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 - -(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 \+)[^\r\n]*\r?\n in [^\r\n]*\r?\n', '' ` - -replace '(?m)^# [^\r\n]*\r?\n in [^\r\n]*(libsystem_|libdyld|/dyld)[^\r\n]*\r?\n', '' ` - | Set-Content -NoNewline $Normalized - -@" - -================================================================================ -signal: 0x80000003 (SEH) -pid: - -# 0xADDR crash_handler +0xOFFSET - in $LibraryPath -# 0xADDR sourcemeta_core_stacktrace_test::crash_deepest +0xOFFSET - in $StacktraceAbortMain -# 0xADDR sourcemeta_core_stacktrace_test::crash_middle +0xOFFSET - in $StacktraceAbortMain -# 0xADDR main +0xOFFSET - in $StacktraceAbortMain -================================================================================ -"@ | Set-Content -NoNewline $Expected - -if ((Get-Content $Normalized -Raw) -ne (Get-Content $Expected -Raw)) { - Compare-Object (Get-Content $Expected) (Get-Content $Normalized) - throw "Output did not match expected" -} diff --git a/test/stacktrace/stacktrace_on_demand_test.ps1 b/test/stacktrace/stacktrace_on_demand_test.ps1 index ea948d843..fe1a3f45f 100644 --- a/test/stacktrace/stacktrace_on_demand_test.ps1 +++ b/test/stacktrace/stacktrace_on_demand_test.ps1 @@ -34,8 +34,8 @@ Get-Content $Actual -replace '\+0xADDR', '+0xOFFSET' ` -replace '(?m)^pid:\s+\d+', 'pid: ' ` -replace '(?m)^#\d+ ', '# ' ` - -replace '(?m)^# [^\r\n]*(_sigtramp|__restore_rt| _?start \+)[^\r\n]*\r?\n in [^\r\n]*\r?\n', '' ` - -replace '(?m)^# [^\r\n]*\r?\n in [^\r\n]*(libsystem_|libdyld|/dyld)[^\r\n]*\r?\n', '' ` + -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', '' ` | Set-Content -NoNewline $Normalized @" diff --git a/test/stacktrace/stacktrace_segfault_test.ps1 b/test/stacktrace/stacktrace_segfault_test.ps1 index a13d43cf3..c413cc776 100644 --- a/test/stacktrace/stacktrace_segfault_test.ps1 +++ b/test/stacktrace/stacktrace_segfault_test.ps1 @@ -37,8 +37,8 @@ Get-Content $Actual -replace '\+0xADDR', '+0xOFFSET' ` -replace '(?m)^pid:\s+\d+', 'pid: ' ` -replace '(?m)^#\d+ ', '# ' ` - -replace '(?m)^# [^\r\n]*(_sigtramp|__restore_rt| _?start \+)[^\r\n]*\r?\n in [^\r\n]*\r?\n', '' ` - -replace '(?m)^# [^\r\n]*\r?\n in [^\r\n]*(libsystem_|libdyld|/dyld)[^\r\n]*\r?\n', '' ` + -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', '' ` | Set-Content -NoNewline $Normalized @" @@ -47,10 +47,10 @@ Get-Content $Actual signal: 0xc0000005 (SEH) pid: -# 0xADDR sourcemeta_core_stacktrace_test::crash_deepest +0xOFFSET - in $StacktraceSegfaultMain # 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 From 98a923b52c215fb48ae62f02f179f7f869ffc30c Mon Sep 17 00:00:00 2001 From: Juan Cruz Viotti Date: Tue, 26 May 2026 17:55:37 -0400 Subject: [PATCH 10/12] MSVC Signed-off-by: Juan Cruz Viotti --- test/stacktrace/stacktrace_on_demand_no_debug_test.ps1 | 2 +- test/stacktrace/stacktrace_on_demand_test.ps1 | 2 +- test/stacktrace/stacktrace_segfault_no_debug_test.ps1 | 2 +- test/stacktrace/stacktrace_segfault_test.ps1 | 9 +++++---- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/test/stacktrace/stacktrace_on_demand_no_debug_test.ps1 b/test/stacktrace/stacktrace_on_demand_no_debug_test.ps1 index 432582d21..a7f468498 100644 --- a/test/stacktrace/stacktrace_on_demand_no_debug_test.ps1 +++ b/test/stacktrace/stacktrace_on_demand_no_debug_test.ps1 @@ -10,7 +10,7 @@ $Self = [IO.Path]::GetFileName($StacktraceOnDemandMain) $Actual = Join-Path $WorkDir "$Self.actual.txt" $ErrorActionPreference = "Continue" -& $StacktraceOnDemandMain > $Actual 2>&1 +& cmd.exe /c "`"$StacktraceOnDemandMain`" > `"$Actual`" 2>&1" $ExitCode = $LASTEXITCODE $ErrorActionPreference = "Stop" # Exited cleanly after printing the on-demand trace diff --git a/test/stacktrace/stacktrace_on_demand_test.ps1 b/test/stacktrace/stacktrace_on_demand_test.ps1 index fe1a3f45f..48835e844 100644 --- a/test/stacktrace/stacktrace_on_demand_test.ps1 +++ b/test/stacktrace/stacktrace_on_demand_test.ps1 @@ -19,7 +19,7 @@ $Normalized = Join-Path $WorkDir "$Self.normalized.txt" $Expected = Join-Path $WorkDir "$Self.expected.txt" $ErrorActionPreference = "Continue" -& $StacktraceOnDemandMain > $Actual 2>&1 +& cmd.exe /c "`"$StacktraceOnDemandMain`" > `"$Actual`" 2>&1" $ExitCode = $LASTEXITCODE $ErrorActionPreference = "Stop" # Exited cleanly after printing the on-demand trace diff --git a/test/stacktrace/stacktrace_segfault_no_debug_test.ps1 b/test/stacktrace/stacktrace_segfault_no_debug_test.ps1 index c2670447f..4fdcf9f97 100644 --- a/test/stacktrace/stacktrace_segfault_no_debug_test.ps1 +++ b/test/stacktrace/stacktrace_segfault_no_debug_test.ps1 @@ -10,7 +10,7 @@ $Self = [IO.Path]::GetFileName($StacktraceSegfaultMain) $Actual = Join-Path $WorkDir "$Self.actual.txt" $ErrorActionPreference = "Continue" -& $StacktraceSegfaultMain > $Actual 2>&1 +& cmd.exe /c "`"$StacktraceSegfaultMain`" > `"$Actual`" 2>&1" $ExitCode = $LASTEXITCODE $ErrorActionPreference = "Stop" # Crashed by a fatal exception diff --git a/test/stacktrace/stacktrace_segfault_test.ps1 b/test/stacktrace/stacktrace_segfault_test.ps1 index c413cc776..0aedb80bc 100644 --- a/test/stacktrace/stacktrace_segfault_test.ps1 +++ b/test/stacktrace/stacktrace_segfault_test.ps1 @@ -18,11 +18,12 @@ $Actual = Join-Path $WorkDir "$Self.actual.txt" $Normalized = Join-Path $WorkDir "$Self.normalized.txt" $Expected = Join-Path $WorkDir "$Self.expected.txt" -# `$ErrorActionPreference = "Stop"` would treat the crashing binary's stderr -# output as a NativeCommandError and abort the script; soften it just for the -# native invocation. +# 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" -& $StacktraceSegfaultMain > $Actual 2>&1 +& cmd.exe /c "`"$StacktraceSegfaultMain`" > `"$Actual`" 2>&1" $ExitCode = $LASTEXITCODE $ErrorActionPreference = "Stop" # Crashed by a fatal exception From f6d429c97faf5c748f0daf60e81b7e03b8c4d4a6 Mon Sep 17 00:00:00 2001 From: Juan Cruz Viotti Date: Tue, 26 May 2026 18:11:25 -0400 Subject: [PATCH 11/12] Fix Signed-off-by: Juan Cruz Viotti --- src/lang/stacktrace/stacktrace_windows.h | 3 ++- test/stacktrace/stacktrace_on_demand_test.ps1 | 20 ++++++++++++----- test/stacktrace/stacktrace_segfault_test.ps1 | 22 ++++++++++++++----- 3 files changed, 32 insertions(+), 13 deletions(-) diff --git a/src/lang/stacktrace/stacktrace_windows.h b/src/lang/stacktrace/stacktrace_windows.h index 0b089326e..9dc86b95f 100644 --- a/src/lang/stacktrace/stacktrace_windows.h +++ b/src/lang/stacktrace/stacktrace_windows.h @@ -78,7 +78,8 @@ __declspec(noinline) auto write_frames(int file_descriptor, } // namespace -extern "C" auto WINAPI crash_handler(EXCEPTION_POINTERS *information) -> LONG { +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); diff --git a/test/stacktrace/stacktrace_on_demand_test.ps1 b/test/stacktrace/stacktrace_on_demand_test.ps1 index 48835e844..6e6bc450e 100644 --- a/test/stacktrace/stacktrace_on_demand_test.ps1 +++ b/test/stacktrace/stacktrace_on_demand_test.ps1 @@ -9,6 +9,8 @@ param( $ErrorActionPreference = "Stop" $Self = [IO.Path]::GetFileName($StacktraceOnDemandMain) +$StacktraceOnDemandMain = $StacktraceOnDemandMain -replace '/', '\' +$StacktraceLibrary = $StacktraceLibrary -replace '/', '\' $LibraryPath = if ($StacktraceLibrary -match '\.(lib|a)$') { $StacktraceOnDemandMain } else { @@ -29,16 +31,16 @@ if ($ExitCode -ne 0) { Get-Content $Actual -(Get-Content $Actual -Raw) ` +$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', '' ` - | Set-Content -NoNewline $Normalized + -replace "`r`n", "`n" -@" +$ExpectedContent = (@" ================================================================================ pid: @@ -51,9 +53,15 @@ pid: # 0xADDR main +0xOFFSET in $StacktraceOnDemandMain ================================================================================ -"@ | Set-Content -NoNewline $Expected +"@) -replace "`r`n", "`n" -if ((Get-Content $Normalized -Raw) -ne (Get-Content $Expected -Raw)) { - Compare-Object (Get-Content $Expected) (Get-Content $Normalized) +[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.ps1 b/test/stacktrace/stacktrace_segfault_test.ps1 index 0aedb80bc..f7662b05a 100644 --- a/test/stacktrace/stacktrace_segfault_test.ps1 +++ b/test/stacktrace/stacktrace_segfault_test.ps1 @@ -9,6 +9,10 @@ param( $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 { @@ -33,16 +37,16 @@ if ($ExitCode -eq 0) { Get-Content $Actual -(Get-Content $Actual -Raw) ` +$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', '' ` - | Set-Content -NoNewline $Normalized + -replace "`r`n", "`n" -@" +$ExpectedContent = (@" ================================================================================ signal: 0xc0000005 (SEH) @@ -57,9 +61,15 @@ pid: # 0xADDR main +0xOFFSET in $StacktraceSegfaultMain ================================================================================ -"@ | Set-Content -NoNewline $Expected +"@) -replace "`r`n", "`n" -if ((Get-Content $Normalized -Raw) -ne (Get-Content $Expected -Raw)) { - Compare-Object (Get-Content $Expected) (Get-Content $Normalized) +[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" } From 6cc703ff7f1839582c1cfa4092eeb82053735285 Mon Sep 17 00:00:00 2001 From: Juan Cruz Viotti Date: Tue, 26 May 2026 18:32:09 -0400 Subject: [PATCH 12/12] Fix Signed-off-by: Juan Cruz Viotti --- test/stacktrace/stacktrace_on_demand_test.ps1 | 2 ++ test/stacktrace/stacktrace_segfault_test.ps1 | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/test/stacktrace/stacktrace_on_demand_test.ps1 b/test/stacktrace/stacktrace_on_demand_test.ps1 index 6e6bc450e..7d867865f 100644 --- a/test/stacktrace/stacktrace_on_demand_test.ps1 +++ b/test/stacktrace/stacktrace_on_demand_test.ps1 @@ -55,6 +55,8 @@ pid: ================================================================================ "@) -replace "`r`n", "`n" +$NormalizedContent = $NormalizedContent.TrimEnd() +$ExpectedContent = $ExpectedContent.TrimEnd() [System.IO.File]::WriteAllText($Normalized, $NormalizedContent) [System.IO.File]::WriteAllText($Expected, $ExpectedContent) diff --git a/test/stacktrace/stacktrace_segfault_test.ps1 b/test/stacktrace/stacktrace_segfault_test.ps1 index f7662b05a..55f9dab7f 100644 --- a/test/stacktrace/stacktrace_segfault_test.ps1 +++ b/test/stacktrace/stacktrace_segfault_test.ps1 @@ -49,7 +49,7 @@ $NormalizedContent = (Get-Content $Actual -Raw) ` $ExpectedContent = (@" ================================================================================ -signal: 0xc0000005 (SEH) +signal: 0xADDR (SEH) pid: # 0xADDR crash_handler +0xOFFSET @@ -63,6 +63,8 @@ pid: ================================================================================ "@) -replace "`r`n", "`n" +$NormalizedContent = $NormalizedContent.TrimEnd() +$ExpectedContent = $ExpectedContent.TrimEnd() [System.IO.File]::WriteAllText($Normalized, $NormalizedContent) [System.IO.File]::WriteAllText($Expected, $ExpectedContent)