From 01eb6e146c929b559de2709dcb1138b2bc914e17 Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Thu, 6 Nov 2025 11:10:44 +0100 Subject: [PATCH 001/130] fix: split inproc with a handler thread --- CMakeLists.txt | 14 +- src/CMakeLists.txt | 7 + src/backends/sentry_backend_inproc.c | 510 +++++++++++++++---- src/sentry_sync.c | 103 +++- src/sentry_sync.h | 1 + src/unwinder/sentry_unwinder.c | 4 + src/unwinder/sentry_unwinder_libunwind_mac.c | 92 ++++ tests/assertions.py | 12 +- 8 files changed, 633 insertions(+), 110 deletions(-) create mode 100644 src/unwinder/sentry_unwinder_libunwind_mac.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 50beda333..b7639b7e5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -144,15 +144,6 @@ if(LINUX) set(CMAKE_ASM_FLAGS "${CMAKE_ASM_FLAGS} -m32 -D_FILE_OFFSET_BITS=64 -D_LARGEFILE64_SOURCE") set_property(GLOBAL PROPERTY FIND_LIBRARY_USE_LIB64_PATHS OFF) endif() - - execute_process( - COMMAND ${CMAKE_C_COMPILER} -dumpmachine - OUTPUT_VARIABLE TARGET_TRIPLET - OUTPUT_STRIP_TRAILING_WHITESPACE - ) - if(TARGET_TRIPLET MATCHES "musl") - set(MUSL TRUE) - endif() endif() # CMAKE_POSITION_INDEPENDENT_CODE must be set BEFORE adding any libraries (including subprojects) @@ -266,9 +257,12 @@ endif() if(ANDROID) set(SENTRY_WITH_LIBUNWINDSTACK TRUE) -elseif(MUSL) +elseif(LINUX) set(SENTRY_WITH_LIBUNWIND TRUE) +elseif(APPLE) + set(SENTRY_WITH_LIBUNWIND_MAC TRUE) elseif(NOT WIN32 AND NOT PROSPERO) + # this should never be true, but we keep it as fallback set(SENTRY_WITH_LIBBACKTRACE TRUE) endif() diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 738d1c110..e212c702c 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -167,6 +167,13 @@ if(SENTRY_WITH_LIBUNWIND) ) endif() +if(SENTRY_WITH_LIBUNWIND_MAC) + target_compile_definitions(sentry PRIVATE SENTRY_WITH_UNWINDER_LIBUNWIND_MAC) + sentry_target_sources_cwd(sentry + unwinder/sentry_unwinder_libunwind_mac.c + ) +endif() + if(SENTRY_WITH_LIBUNWINDSTACK) target_compile_definitions(sentry PRIVATE SENTRY_WITH_UNWINDER_LIBUNWINDSTACK) sentry_target_sources_cwd(sentry diff --git a/src/backends/sentry_backend_inproc.c b/src/backends/sentry_backend_inproc.c index 8a8755a75..89ef37628 100644 --- a/src/backends/sentry_backend_inproc.c +++ b/src/backends/sentry_backend_inproc.c @@ -4,6 +4,7 @@ #include "sentry_alloc.h" #include "sentry_backend.h" #include "sentry_core.h" +#include "sentry_cpu_relax.h" #include "sentry_database.h" #include "sentry_envelope.h" #include "sentry_logger.h" @@ -18,13 +19,56 @@ #include "sentry_transport.h" #include "sentry_unix_pageallocator.h" #include "transports/sentry_disk_transport.h" +#include +#include #include #define SIGNAL_DEF(Sig, Desc) { Sig, #Sig, Desc } #define MAX_FRAMES 128 +// the data exchange between the signal handler and the handler thread +typedef struct sentry_inproc_handler_state_s { + sentry_ucontext_t uctx; +#ifdef SENTRY_PLATFORM_UNIX + siginfo_t siginfo_storage; + ucontext_t user_context_storage; +#endif + const struct signal_slot *sig_slot; + sentry_handler_strategy_t strategy; +} sentry_inproc_handler_state_t; + +// "data" struct containing options to prevent mutex access in signal handler +typedef struct sentry_inproc_backend_config_s { + bool enable_logging_when_crashed; + sentry_handler_strategy_t handler_strategy; +} sentry_inproc_backend_config_t; + +// global instance for data-exchange between signal handler and handler thread +static sentry_inproc_handler_state_t g_handler_state; +// global instance for backend configuration state +static sentry_inproc_backend_config_t g_backend_config; + +// handler thread state and synchronization variables +static sentry_threadid_t g_handler_thread; +// true once the handler thread starts waiting +static volatile long g_handler_thread_ready = 0; +// shutdown loop invariant +static volatile long g_handler_should_exit = 0; +// signal handler tells handler thread to start working +static volatile long g_handler_has_work = 0; +// handler thread wakes signal handler from suspension after finishing the work +static volatile long g_handler_work_done = 0; + +// trigger/schedule primitives that block the handler thread until we need it +#ifdef SENTRY_PLATFORM_UNIX +static int g_handler_pipe[2] = { -1, -1 }; +#elif defined(SENTRY_PLATFORM_WINDOWS) +static HANDLE g_handler_semaphore = NULL; +#endif + #ifdef SENTRY_PLATFORM_UNIX +# include struct signal_slot { int signum; const char *signame; @@ -47,6 +91,8 @@ static const struct signal_slot SIGNAL_DEFINITIONS[SIGNAL_COUNT] = { }; static void handle_signal(int signum, siginfo_t *info, void *user_context); +static int start_handler_thread(void); +static void stop_handler_thread(void); static void reset_signal_handlers(void) @@ -77,8 +123,22 @@ invoke_signal_handler(int signum, siginfo_t *info, void *user_context) static int startup_inproc_backend( - sentry_backend_t *UNUSED(backend), const sentry_options_t *UNUSED(options)) + sentry_backend_t *backend, const sentry_options_t *options) { + // get option state so we don't need to sync read during signal handling + g_backend_config.enable_logging_when_crashed + = options ? options->enable_logging_when_crashed : true; + g_backend_config.handler_strategy = +# if defined(SENTRY_PLATFORM_LINUX) + options ? sentry_options_get_handler_strategy(options) : +# endif + SENTRY_HANDLER_STRATEGY_DEFAULT; + if (backend) { + backend->data = &g_backend_config; + } + + start_handler_thread(); + // save the old signal handlers memset(g_previous_handlers, 0, sizeof(g_previous_handlers)); for (size_t i = 0; i < SIGNAL_COUNT; ++i) { @@ -119,8 +179,10 @@ startup_inproc_backend( } static void -shutdown_inproc_backend(sentry_backend_t *UNUSED(backend)) +shutdown_inproc_backend(sentry_backend_t *backend) { + stop_handler_thread(); + if (g_signal_stack.ss_sp) { g_signal_stack.ss_flags = SS_DISABLE; sigaltstack(&g_signal_stack, 0); @@ -128,6 +190,9 @@ shutdown_inproc_backend(sentry_backend_t *UNUSED(backend)) g_signal_stack.ss_sp = NULL; } reset_signal_handlers(); + if (backend) { + backend->data = NULL; + } } #elif defined(SENTRY_PLATFORM_WINDOWS) @@ -169,25 +234,40 @@ static LONG WINAPI handle_exception(EXCEPTION_POINTERS *); static int startup_inproc_backend( - sentry_backend_t *UNUSED(backend), const sentry_options_t *UNUSED(options)) + sentry_backend_t *backend, const sentry_options_t *options) { + g_backend_config.enable_logging_when_crashed + = options ? options->enable_logging_when_crashed : true; + g_backend_config.handler_strategy = options + ? sentry_options_get_handler_strategy(options) + : SENTRY_HANDLER_STRATEGY_DEFAULT; + if (backend) { + backend->data = &g_backend_config; + } + # if !defined(SENTRY_BUILD_SHARED) \ && defined(SENTRY_THREAD_STACK_GUARANTEE_AUTO_INIT) sentry__set_default_thread_stack_guarantee(); # endif + start_handler_thread(); g_previous_handler = SetUnhandledExceptionFilter(&handle_exception); SetErrorMode(SEM_FAILCRITICALERRORS); return 0; } static void -shutdown_inproc_backend(sentry_backend_t *UNUSED(backend)) +shutdown_inproc_backend(sentry_backend_t *backend) { + stop_handler_thread(); + LPTOP_LEVEL_EXCEPTION_FILTER current_handler = SetUnhandledExceptionFilter(g_previous_handler); if (current_handler != &handle_exception) { SetUnhandledExceptionFilter(current_handler); } + if (backend) { + backend->data = NULL; + } } #endif @@ -517,6 +597,7 @@ make_signal_event(const struct signal_slot *sig_slot, signal_meta, "name", sentry_value_new_string(sig_slot->signame)); // at least on windows, the signum is a true u32 which we can't // otherwise represent. + // TODO: does that still match reality? sentry_value_set_by_key(signal_meta, "number", sentry_value_new_double((double)sig_slot->signum)); } @@ -563,97 +644,32 @@ make_signal_event(const struct signal_slot *sig_slot, return event; } +/** + * This is the signal-unsafe part of the inproc handler. Everything that + * requires stdio, time-formatting/-capture or serialization must happen here. + * + * Although we can use signal-unsafe functions here, this should still be + * written with care. Don't overly rely on thread synchronization since the + * program is in a crashed state. At least one thread no longer progresses and + * memory can be corrupted. + */ static void -handle_ucontext(const sentry_ucontext_t *uctx) +process_ucontext_deferred( + const sentry_ucontext_t *uctx, const struct signal_slot *sig_slot) { - // Disable logging during crash handling if the option is set - SENTRY_WITH_OPTIONS (options) { - if (!options->enable_logging_when_crashed) { - sentry__logger_disable(); - } - } - SENTRY_INFO("entering signal handler"); - sentry_handler_strategy_t strategy = SENTRY_HANDLER_STRATEGY_DEFAULT; -#ifdef SENTRY_PLATFORM_UNIX - // inform the sentry_sync system that we're in a signal handler. This will - // make mutexes spin on a spinlock instead as it's no longer safe to use a - // pthread mutex. - sentry__enter_signal_handler(); -#endif - SENTRY_WITH_OPTIONS (options) { -#ifdef SENTRY_PLATFORM_LINUX - // On Linux (and thus Android) CLR/Mono converts signals provoked by - // AOT/JIT-generated native code into managed code exceptions. In these - // cases, we shouldn't react to the signal at all and let their handler - // discontinue the signal chain by invoking the runtime handler before - // we process the signal. - strategy = sentry_options_get_handler_strategy(options); - if (strategy == SENTRY_HANDLER_STRATEGY_CHAIN_AT_START) { - SENTRY_DEBUG("defer to runtime signal handler at start"); - // there is a good chance that we won't return from the previous - // handler and that would mean we couldn't enter this handler with - // the next signal coming in if we didn't "leave" here. - sentry__leave_signal_handler(); - if (!options->enable_logging_when_crashed) { - sentry__logger_enable(); - } - - uintptr_t ip = get_instruction_pointer(uctx); - uintptr_t sp = get_stack_pointer(uctx); - - // invoke the previous handler (typically the CLR/Mono - // signal-to-managed-exception handler) - invoke_signal_handler( - uctx->signum, uctx->siginfo, (void *)uctx->user_context); - - // If the execution returns here in AOT mode, and the instruction - // or stack pointer were changed, it means CLR/Mono converted the - // signal into a managed exception and transferred execution to a - // managed exception handler. - // https://github.com/dotnet/runtime/blob/6d96e28597e7da0d790d495ba834cc4908e442cd/src/mono/mono/mini/exceptions-arm64.c#L538 - if (ip != get_instruction_pointer(uctx) - || sp != get_stack_pointer(uctx)) { - SENTRY_DEBUG("runtime converted the signal to a managed " - "exception, we do not handle the signal"); - return; - } - - // let's re-enter because it means this was an actual native crash - if (!options->enable_logging_when_crashed) { - sentry__logger_disable(); - } - sentry__enter_signal_handler(); - SENTRY_DEBUG( - "return from runtime signal handler, we handle the signal"); - } -#endif - - const struct signal_slot *sig_slot = NULL; - for (int i = 0; i < SIGNAL_COUNT; ++i) { -#ifdef SENTRY_PLATFORM_UNIX - if (SIGNAL_DEFINITIONS[i].signum == uctx->signum) { -#elif defined SENTRY_PLATFORM_WINDOWS - if (SIGNAL_DEFINITIONS[i].signum - == uctx->exception_ptrs.ExceptionRecord->ExceptionCode) { -#else -# error Unsupported platform -#endif - sig_slot = &SIGNAL_DEFINITIONS[i]; - } - } - -#ifdef SENTRY_PLATFORM_UNIX - // use a signal-safe allocator before we tear down. - sentry__page_allocator_enable(); -#endif // Flush logs in a crash-safe manner before crash handling if (options->enable_logs) { sentry__logs_flush_crash_safe(); } + sentry_handler_strategy_t strategy = +#if defined(SENTRY_PLATFORM_LINUX) + options ? sentry_options_get_handler_strategy(options) : +#endif + SENTRY_HANDLER_STRATEGY_DEFAULT; sentry_value_t event = make_signal_event(sig_slot, uctx, strategy); bool should_handle = true; sentry__write_crash_marker(options); @@ -699,9 +715,325 @@ handle_ucontext(const sentry_ucontext_t *uctx) // after capturing the crash event, dump all the envelopes to disk sentry__transport_dump_queue(options->transport, options->run); + + SENTRY_INFO("crash has been captured"); + } +} + +SENTRY_THREAD_FN +handler_thread_main(void *UNUSED(data)) +{ + sentry__atomic_store(&g_handler_thread_ready, 1); + + while (!sentry__atomic_fetch(&g_handler_should_exit)) { +#ifdef SENTRY_PLATFORM_UNIX + char command = 0; + ssize_t rv = read(g_handler_pipe[0], &command, 1); + if (rv == -1 && errno == EINTR) { + continue; + } + if (rv <= 0) { + if (sentry__atomic_fetch(&g_handler_should_exit)) { + break; + } + continue; + } +#elif defined(SENTRY_PLATFORM_WINDOWS) + DWORD wait_result = WaitForSingleObject(g_handler_semaphore, INFINITE); + if (wait_result != WAIT_OBJECT_0) { + continue; + } + if (sentry__atomic_fetch(&g_handler_should_exit)) { + break; + } +#endif + + if (!sentry__atomic_fetch(&g_handler_has_work)) { + continue; + } + +#ifdef SENTRY_PLATFORM_UNIX + sentry__switch_handler_thread(); +#endif + process_ucontext_deferred( + &g_handler_state.uctx, g_handler_state.sig_slot); + sentry__atomic_store(&g_handler_has_work, 0); + sentry__atomic_store(&g_handler_work_done, 1); + } + +#ifdef SENTRY_PLATFORM_WINDOWS + return 0; +#else + return NULL; +#endif +} + +static int +start_handler_thread(void) +{ + if (sentry__atomic_fetch(&g_handler_thread_ready)) { + return 0; + } + + sentry__thread_init(&g_handler_thread); + sentry__atomic_store(&g_handler_should_exit, 0); + sentry__atomic_store(&g_handler_has_work, 0); + sentry__atomic_store(&g_handler_work_done, 0); + +#ifdef SENTRY_PLATFORM_UNIX + if (pipe(g_handler_pipe) != 0) { + SENTRY_WARNF("failed to create handler pipe: %s", strerror(errno)); + return 1; + } +#elif defined(SENTRY_PLATFORM_WINDOWS) + g_handler_semaphore = CreateSemaphoreW(NULL, 0, LONG_MAX, NULL); + if (!g_handler_semaphore) { + SENTRY_WARN("failed to create handler semaphore"); + return 1; + } +#endif + + if (sentry__thread_spawn(&g_handler_thread, handler_thread_main, NULL) + != 0) { + SENTRY_WARN("failed to spawn handler thread"); +#ifdef SENTRY_PLATFORM_UNIX + close(g_handler_pipe[0]); + close(g_handler_pipe[1]); + g_handler_pipe[0] = -1; + g_handler_pipe[1] = -1; +#elif defined(SENTRY_PLATFORM_WINDOWS) + CloseHandle(g_handler_semaphore); + g_handler_semaphore = NULL; +#endif + return 1; } - SENTRY_INFO("crash has been captured"); + return 0; +} + +static void +stop_handler_thread(void) +{ + if (!sentry__atomic_fetch(&g_handler_thread_ready)) { + return; + } + + sentry__atomic_store(&g_handler_should_exit, 1); + +#ifdef SENTRY_PLATFORM_UNIX + if (g_handler_pipe[1] >= 0) { + char c = 0; + ssize_t rv; + do { + rv = write(g_handler_pipe[1], &c, 1); + } while (rv == -1 && errno == EINTR); + } +#elif defined(SENTRY_PLATFORM_WINDOWS) + if (g_handler_semaphore) { + ReleaseSemaphore(g_handler_semaphore, 1, NULL); + } +#endif + + sentry__thread_join(g_handler_thread); + sentry__thread_free(&g_handler_thread); + +#ifdef SENTRY_PLATFORM_UNIX + if (g_handler_pipe[0] >= 0) { + close(g_handler_pipe[0]); + g_handler_pipe[0] = -1; + } + if (g_handler_pipe[1] >= 0) { + close(g_handler_pipe[1]); + g_handler_pipe[1] = -1; + } +#elif defined(SENTRY_PLATFORM_WINDOWS) + if (g_handler_semaphore) { + CloseHandle(g_handler_semaphore); + g_handler_semaphore = NULL; + } +#endif + + sentry__atomic_store(&g_handler_thread_ready, 0); + sentry__atomic_store(&g_handler_should_exit, 0); +} + +static void +dispatch_ucontext(const sentry_ucontext_t *uctx, + const struct signal_slot *sig_slot, sentry_handler_strategy_t strategy) +{ + if (!sentry__atomic_fetch(&g_handler_thread_ready)) { + process_ucontext_deferred(uctx, sig_slot); + return; + } + + g_handler_state.uctx = *uctx; + g_handler_state.sig_slot = sig_slot; + +#ifdef SENTRY_PLATFORM_UNIX + if (uctx->siginfo) { + memcpy(&g_handler_state.siginfo_storage, uctx->siginfo, + sizeof(g_handler_state.siginfo_storage)); + g_handler_state.uctx.siginfo = &g_handler_state.siginfo_storage; + } else { + g_handler_state.uctx.siginfo = NULL; + } + + if (uctx->user_context) { + memcpy(&g_handler_state.user_context_storage, uctx->user_context, + sizeof(g_handler_state.user_context_storage)); + g_handler_state.uctx.user_context + = &g_handler_state.user_context_storage; + } else { + g_handler_state.uctx.user_context = NULL; + } +#endif + + sentry__atomic_store(&g_handler_work_done, 0); + sentry__atomic_store(&g_handler_has_work, 1); + + // signal the handler thread to start working +#ifdef SENTRY_PLATFORM_UNIX + if (g_handler_pipe[1] >= 0) { + char c = 1; + ssize_t rv; + do { + rv = write(g_handler_pipe[1], &c, 1); + } while (rv == -1 && errno == EINTR); + } +#elif defined(SENTRY_PLATFORM_WINDOWS) + if (g_handler_semaphore) { + ReleaseSemaphore(g_handler_semaphore, 1, NULL); + } +#endif + + // wait until the handler has done its work + while (!sentry__atomic_fetch(&g_handler_work_done)) { + sentry__cpu_relax(); + } + +#ifdef SENTRY_PLATFORM_UNIX + sentry__switch_handler_thread(); +#endif +} + +/** + * This is the signal-safe part of the inproc handler. Everything in here should + * not defer to more than the set of functions listed in: + * https://www.man7.org/linux/man-pages/man7/signal-safety.7.html + * + * That means: + * - no heap allocations except for sentry_malloc() (page allocator enabled!!!) + * - no stdio or any kind of libc string formatting + * - no logging (at least not with the printf-based default logger) + * - no pthread synchronization (SENTRY_WITH_OPTIONS will terminate with a log) + * - in particular, don't access sentry interfaces that could request + * access to options or the scope, those should go to the handler thread + * - sentry_value_* and sentry_malloc are generally fine, because we use a safe + * allocator, but keep in mind that some constructors create timestampy and + * similar stringy and thus formatted values (and those are forbidden here). + * + * If you are unsure about a particular function on a given target platform + * please consult the signal-safety man page. + * + * Another decision marker of whether code should go in here: do you must run + * on the preempted crashed thread? Do you need to run before anything else? + */ +static void +process_ucontext(const sentry_ucontext_t *uctx) +{ +#ifdef SENTRY_PLATFORM_UNIX + sentry__enter_signal_handler(); +#endif + + if (!g_backend_config.enable_logging_when_crashed) { + sentry__logger_disable(); + } + + sentry_handler_strategy_t strategy = g_backend_config.handler_strategy; + +#ifdef SENTRY_PLATFORM_LINUX + if (strategy == SENTRY_HANDLER_STRATEGY_CHAIN_AT_START) { + // On Linux (and thus Android) CLR/Mono converts signals provoked by + // AOT/JIT-generated native code into managed code exceptions. In these + // cases, we shouldn't react to the signal at all and let their handler + // discontinue the signal chain by invoking the runtime handler before + // we process the signal. + // there is a good chance that we won't return from the previous + // handler and that would mean we couldn't enter this handler with + // the next signal coming in if we didn't "leave" here. + sentry__leave_signal_handler(); + if (!g_backend_config.enable_logging_when_crashed) { + sentry__logger_enable(); + } + + uintptr_t ip = get_instruction_pointer(uctx); + uintptr_t sp = get_stack_pointer(uctx); + + // invoke the previous handler (typically the CLR/Mono + // signal-to-managed-exception handler) + invoke_signal_handler( + uctx->signum, uctx->siginfo, (void *)uctx->user_context); + + // If the execution returns here in AOT mode, and the instruction + // or stack pointer were changed, it means CLR/Mono converted the + // signal into a managed exception and transferred execution to a + // managed exception handler. + // https://github.com/dotnet/runtime/blob/6d96e28597e7da0d790d495ba834cc4908e442cd/src/mono/mono/mini/exceptions-arm64.c#L538 + if (ip != get_instruction_pointer(uctx) + || sp != get_stack_pointer(uctx)) { + return; + } + + // let's re-enter because it means this was an actual native crash + if (!g_backend_config.enable_logging_when_crashed) { + sentry__logger_disable(); + } + sentry__enter_signal_handler(); + // return from runtime handler; continue processing the crash on the + // signal thread until the worker takes over + } +#endif + + const struct signal_slot *sig_slot = NULL; + for (int i = 0; i < SIGNAL_COUNT; ++i) { +#ifdef SENTRY_PLATFORM_UNIX + if (SIGNAL_DEFINITIONS[i].signum == uctx->signum) { +#elif defined SENTRY_PLATFORM_WINDOWS + if (SIGNAL_DEFINITIONS[i].signum + == uctx->exception_ptrs.ExceptionRecord->ExceptionCode) { +#else +# error Unsupported platform +#endif + sig_slot = &SIGNAL_DEFINITIONS[i]; + } + } + +#ifdef SENTRY_PLATFORM_UNIX + // use a signal-safe allocator before we tear down. + sentry__page_allocator_enable(); +#endif + + const sentry_threadid_t current_thread = sentry__current_thread(); + if (g_handler_thread_ready + && sentry__threadid_equal(current_thread, g_handler_thread)) { + // This means our handler thread crashed, there is no safe way out: + // make an async-signal-safe log and defer to previous + static const char msg[] = "[sentry] FATAL crash in handler thread, " + "falling back to previous handler\n"; +#ifdef SENTRY_PLATFORM_UNIX + (void)write(STDERR_FILENO, msg, sizeof(msg) - 1); +#else + OutputDebugStringA(msg); + HANDLE stderr_handle = GetStdHandle(STD_ERROR_HANDLE); + if (stderr_handle && stderr_handle != INVALID_HANDLE_VALUE) { + DWORD written; + WriteFile(stderr_handle, msg, (DWORD)strlen(msg), &written, NULL); + } +#endif + } else { + // invoke the handler thread for signal unsafe actions + dispatch_ucontext(uctx, sig_slot, strategy); + } #ifdef SENTRY_PLATFORM_UNIX // reset signal handlers and invoke the original ones. This will then tear @@ -725,7 +1057,7 @@ handle_signal(int signum, siginfo_t *info, void *user_context) uctx.signum = signum; uctx.siginfo = info; uctx.user_context = (ucontext_t *)user_context; - handle_ucontext(&uctx); + process_ucontext(&uctx); } #elif defined SENTRY_PLATFORM_WINDOWS static LONG WINAPI @@ -740,7 +1072,7 @@ handle_exception(EXCEPTION_POINTERS *ExceptionInfo) sentry_ucontext_t uctx; memset(&uctx, 0, sizeof(uctx)); uctx.exception_ptrs = *ExceptionInfo; - handle_ucontext(&uctx); + process_ucontext(&uctx); return EXCEPTION_CONTINUE_SEARCH; } #endif @@ -748,7 +1080,7 @@ handle_exception(EXCEPTION_POINTERS *ExceptionInfo) static void handle_except(sentry_backend_t *UNUSED(backend), const sentry_ucontext_t *uctx) { - handle_ucontext(uctx); + process_ucontext(uctx); } sentry_backend_t * diff --git a/src/sentry_sync.c b/src/sentry_sync.c index 7766a6970..18d66c2a9 100644 --- a/src/sentry_sync.c +++ b/src/sentry_sync.c @@ -505,34 +505,117 @@ sentry__bgworker_get_thread_name(sentry_bgworker_t *bgw) #if defined(SENTRY_PLATFORM_UNIX) || defined(SENTRY_PLATFORM_NX) # include "sentry_cpu_relax.h" +# include -static sig_atomic_t g_in_signal_handler = 0; +static sig_atomic_t g_in_signal_handler __attribute__((aligned(64))) = 0; static sentry_threadid_t g_signal_handling_thread = { 0 }; +# ifdef SENTRY_BACKEND_INPROC +static sig_atomic_t g_signal_handler_can_lock __attribute__((aligned(64))) = 0; + +static void +fatal_signal_lock_violation(void) +{ + static const char msg[] + = "[sentry] FATAL attempted to acquire mutex inside signal handler\n"; + (void)write(STDERR_FILENO, msg, sizeof(msg) - 1); + _exit(1); +} +# endif + bool sentry__block_for_signal_handler(void) { - while (__sync_fetch_and_add(&g_in_signal_handler, 0)) { - if (sentry__threadid_equal( - sentry__current_thread(), g_signal_handling_thread)) { - return false; + for (;;) { + // if there is no signal handler active, we don't need to block + if (!__atomic_load_n(&g_in_signal_handler, __ATOMIC_RELAXED)) { + return true; } + + sentry_threadid_t current = sentry__current_thread(); + sentry_threadid_t handling + = __atomic_load_n(&g_signal_handling_thread, __ATOMIC_ACQUIRE); + + if (sentry__threadid_equal(current, handling)) { +# ifdef SENTRY_BACKEND_INPROC + if (!__atomic_load_n( + &g_signal_handler_can_lock, __ATOMIC_ACQUIRE)) { + fatal_signal_lock_violation(); + } +# endif + return true; + } + + // otherwise, spin sentry__cpu_relax(); } - return true; +} + +static bool +is_handling_thread(void) +{ + sentry_threadid_t handling + = __atomic_load_n(&g_signal_handling_thread, __ATOMIC_ACQUIRE); + return sentry__threadid_equal(handling, sentry__current_thread()); } void sentry__enter_signal_handler(void) { - sentry__block_for_signal_handler(); - g_signal_handling_thread = sentry__current_thread(); - __sync_fetch_and_or(&g_in_signal_handler, 1); + for (;;) { + // entering a signal handler while another runs, should block us + while (__atomic_load_n(&g_in_signal_handler, __ATOMIC_RELAXED)) { + // however, if we re-enter most likely a signal was raised from + // within the handler and then we should proceed. + // TODO: maybe pass in the signum and check for SIGABRT loops here + if (is_handling_thread()) { + return; + } + } + + // RMW that both tests AND sets atomically so we know we won the race + if (__sync_lock_test_and_set(&g_in_signal_handler, 1) == 0) { + sentry_threadid_t current = sentry__current_thread(); + // update the thread, now that no one else can and leave + __atomic_store_n( + &g_signal_handling_thread, current, __ATOMIC_RELEASE); +# ifdef SENTRY_BACKEND_INPROC + __atomic_store_n(&g_signal_handler_can_lock, 0, __ATOMIC_RELEASE); +# endif + return; + } + + // otherwise, spin + } +} + +bool +sentry__switch_handler_thread(void) +{ + if (!__atomic_load_n(&g_in_signal_handler, __ATOMIC_ACQUIRE)) { + return false; + } + + sentry_threadid_t current = sentry__current_thread(); + __atomic_store_n(&g_signal_handling_thread, current, __ATOMIC_RELEASE); + // TODO: this is still insufficient as a safe-guard when crashing in the + // handler thread +# ifdef SENTRY_BACKEND_INPROC + __atomic_store_n(&g_signal_handler_can_lock, 1, __ATOMIC_RELEASE); +# endif + + return true; } void sentry__leave_signal_handler(void) { - __sync_fetch_and_and(&g_in_signal_handler, 0); + // clean up the thread-id and drop the reentrancy guard + __atomic_store_n( + &g_signal_handling_thread, (sentry_threadid_t) { 0 }, __ATOMIC_RELAXED); +# ifdef SENTRY_BACKEND_INPROC + __atomic_store_n(&g_signal_handler_can_lock, 0, __ATOMIC_RELAXED); +# endif + __sync_lock_release(&g_in_signal_handler); } #endif diff --git a/src/sentry_sync.h b/src/sentry_sync.h index 5516443b9..8761cd6ab 100644 --- a/src/sentry_sync.h +++ b/src/sentry_sync.h @@ -227,6 +227,7 @@ typedef CONDITION_VARIABLE sentry_cond_t; bool sentry__block_for_signal_handler(void); void sentry__enter_signal_handler(void); void sentry__leave_signal_handler(void); +bool sentry__switch_handler_thread(void); typedef pthread_t sentry_threadid_t; typedef pthread_mutex_t sentry_mutex_t; diff --git a/src/unwinder/sentry_unwinder.c b/src/unwinder/sentry_unwinder.c index 19affe8fe..09664d015 100644 --- a/src/unwinder/sentry_unwinder.c +++ b/src/unwinder/sentry_unwinder.c @@ -16,6 +16,7 @@ DEFINE_UNWINDER(libunwindstack); DEFINE_UNWINDER(libbacktrace); DEFINE_UNWINDER(dbghelp); DEFINE_UNWINDER(libunwind); +DEFINE_UNWINDER(libunwind_mac); DEFINE_UNWINDER(psunwind); static size_t @@ -34,6 +35,9 @@ unwind_stack( #ifdef SENTRY_WITH_UNWINDER_LIBUNWIND TRY_UNWINDER(libunwind); #endif +#ifdef SENTRY_WITH_UNWINDER_LIBUNWIND_MAC + TRY_UNWINDER(libunwind_mac); +#endif #ifdef SENTRY_WITH_UNWINDER_PS TRY_UNWINDER(psunwind); #endif diff --git a/src/unwinder/sentry_unwinder_libunwind_mac.c b/src/unwinder/sentry_unwinder_libunwind_mac.c new file mode 100644 index 000000000..176fb287b --- /dev/null +++ b/src/unwinder/sentry_unwinder_libunwind_mac.c @@ -0,0 +1,92 @@ +#include "sentry_boot.h" +#include "sentry_logger.h" +#include + +// a very cheap pointer validation for starters +static bool +valid_ptr(uintptr_t p) +{ + return p && (p % sizeof(uintptr_t) == 0); +} + +size_t +sentry__unwind_stack_libunwind_mac( + void *addr, const sentry_ucontext_t *uctx, void **ptrs, size_t max_frames) +{ + if (addr) { + return 0; + } + size_t frame_idx = 0; + + if (uctx) { + // TODO: this is a working ARM64 from ucontext unwinder using FP that + // doesn't have the issues we see backtrace() (which isn't even + // signal-safe) but also the system provided libunwind() on macOS + // - clean this up + // - implement an x86_64 ucontext unwinder + + struct __darwin_mcontext64 *mctx = uctx->user_context->uc_mcontext; + uintptr_t pc = (uintptr_t)mctx->__ss.__pc; + uintptr_t fp = (uintptr_t)mctx->__ss.__fp; + uintptr_t lr = (uintptr_t)mctx->__ss.__lr; + + // top frame: adjust pc−1 so it symbolizes inside the function + if (pc) { + ptrs[frame_idx++] = (void *)(pc - 1); + } + + // next frame is from saved LR at current FP record + if (lr) { + ptrs[frame_idx++] = (void *)(lr - 1); + } + + for (size_t i = 0; i < max_frames; ++i) { + if (!valid_ptr(fp)) { + break; + } + + // arm64 frame record layout: [prev_fp, saved_lr] at fp and fp+8 + uintptr_t *record = (uintptr_t *)fp; + uintptr_t next_fp = record[0]; + uintptr_t retaddr = record[1]; + if (!valid_ptr(next_fp) || !retaddr) { + break; + } + + ptrs[frame_idx++] = (void *)(retaddr - 1); + if (next_fp <= fp) { + break; // prevent loops + } + fp = next_fp; + } + } else { + unw_context_t uc; + int ret = unw_getcontext(&uc); + if (ret != 0) { + SENTRY_WARN("Failed to retrieve context with libunwind"); + return 0; + } + + unw_cursor_t cursor; + ret = unw_init_local(&cursor, &uc); + if (ret != 0) { + SENTRY_WARN("Failed to initialize libunwind with local context"); + return 0; + } + while (unw_step(&cursor) > 0 && frame_idx < max_frames - 1) { + unw_word_t ip = 0; + SENTRY_INFOF("ip: %p", ip); + unw_get_reg(&cursor, UNW_REG_IP, &ip); +#if defined(__arm64__) + // Strip pointer authentication, for some reason ptrauth_strip() not + // working + // https://developer.apple.com/documentation/security/preparing_your_app_to_work_with_pointer_authentication + ip &= 0x7fffffffffffull; +#endif + ptrs[frame_idx] = (void *)ip; + frame_idx++; + } + } + + return frame_idx + 1; +} diff --git a/tests/assertions.py b/tests/assertions.py index 05d3ea320..62e5a2cb0 100644 --- a/tests/assertions.py +++ b/tests/assertions.py @@ -170,6 +170,16 @@ def assert_event_meta( ) +def is_valid_hex(s): + if not s.lower().startswith("0x"): + return False + try: + int(s, 0) + return True + except ValueError: + return False + + def assert_stacktrace( envelope, inside_exception=False, check_size=True, check_package=False ): @@ -181,7 +191,7 @@ def assert_stacktrace( if check_size: assert len(frames) > 0 - assert all(frame["instruction_addr"].startswith("0x") for frame in frames) + assert all(is_valid_hex(frame["instruction_addr"]) for frame in frames) assert any( frame.get("function") is not None and frame.get("package") is not None for frame in frames From 7eb3ca859a9e114568f6833e0ef09df2f30b3a53 Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Thu, 6 Nov 2025 14:15:25 +0100 Subject: [PATCH 002/130] prevent warning on write() return value --- src/sentry_sync.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/sentry_sync.c b/src/sentry_sync.c index 18d66c2a9..1af5979b7 100644 --- a/src/sentry_sync.c +++ b/src/sentry_sync.c @@ -518,7 +518,8 @@ fatal_signal_lock_violation(void) { static const char msg[] = "[sentry] FATAL attempted to acquire mutex inside signal handler\n"; - (void)write(STDERR_FILENO, msg, sizeof(msg) - 1); + const ssize_t rv = write(STDERR_FILENO, msg, sizeof(msg) - 1); + (void)rv; _exit(1); } # endif From 0cff127e525b422a04bf8eadc7dd80d999dfc0ff Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Thu, 6 Nov 2025 14:16:17 +0100 Subject: [PATCH 003/130] get rid of unsused dispatch parameter --- src/backends/sentry_backend_inproc.c | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/backends/sentry_backend_inproc.c b/src/backends/sentry_backend_inproc.c index 89ef37628..54681486a 100644 --- a/src/backends/sentry_backend_inproc.c +++ b/src/backends/sentry_backend_inproc.c @@ -35,7 +35,6 @@ typedef struct sentry_inproc_handler_state_s { ucontext_t user_context_storage; #endif const struct signal_slot *sig_slot; - sentry_handler_strategy_t strategy; } sentry_inproc_handler_state_t; // "data" struct containing options to prevent mutex access in signal handler @@ -858,8 +857,8 @@ stop_handler_thread(void) } static void -dispatch_ucontext(const sentry_ucontext_t *uctx, - const struct signal_slot *sig_slot, sentry_handler_strategy_t strategy) +dispatch_ucontext( + const sentry_ucontext_t *uctx, const struct signal_slot *sig_slot) { if (!sentry__atomic_fetch(&g_handler_thread_ready)) { process_ucontext_deferred(uctx, sig_slot); @@ -1032,7 +1031,7 @@ process_ucontext(const sentry_ucontext_t *uctx) #endif } else { // invoke the handler thread for signal unsafe actions - dispatch_ucontext(uctx, sig_slot, strategy); + dispatch_ucontext(uctx, sig_slot); } #ifdef SENTRY_PLATFORM_UNIX From 12650070f7f614c72039cb8952fbab48fad3e8c7 Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Thu, 6 Nov 2025 14:18:59 +0100 Subject: [PATCH 004/130] and another unused return value for write() --- src/backends/sentry_backend_inproc.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/backends/sentry_backend_inproc.c b/src/backends/sentry_backend_inproc.c index 54681486a..25fb3d03b 100644 --- a/src/backends/sentry_backend_inproc.c +++ b/src/backends/sentry_backend_inproc.c @@ -1020,7 +1020,8 @@ process_ucontext(const sentry_ucontext_t *uctx) static const char msg[] = "[sentry] FATAL crash in handler thread, " "falling back to previous handler\n"; #ifdef SENTRY_PLATFORM_UNIX - (void)write(STDERR_FILENO, msg, sizeof(msg) - 1); + const ssize_t rv = write(STDERR_FILENO, msg, sizeof(msg) - 1); + (void)rv; #else OutputDebugStringA(msg); HANDLE stderr_handle = GetStdHandle(STD_ERROR_HANDLE); From 5a11ea24c59a0d92a2e650659348736e5de66171 Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Fri, 7 Nov 2025 11:54:21 +0100 Subject: [PATCH 005/130] make tests a bit less brittle --- tests/assertions.py | 8 ++++---- tests/test_dotnet_signals.py | 6 ++++++ tests/unit/test_process.c | 17 +++++++++++++++-- 3 files changed, 25 insertions(+), 6 deletions(-) diff --git a/tests/assertions.py b/tests/assertions.py index 62e5a2cb0..d40311c18 100644 --- a/tests/assertions.py +++ b/tests/assertions.py @@ -127,11 +127,11 @@ def assert_event_meta( match = VERSION_RE.match(version) version = match.group(1) build = match.group(2) + expected_os_context = {"name": "Linux", "version": version} + if build: + expected_os_context["build"] = build - assert_matches( - event["contexts"]["os"], - {"name": "Linux", "version": version, "build": build}, - ) + assert_matches(event["contexts"]["os"], expected_os_context) assert "distribution_name" in event["contexts"]["os"] assert "distribution_version" in event["contexts"]["os"] elif sys.platform == "darwin": diff --git a/tests/test_dotnet_signals.py b/tests/test_dotnet_signals.py index 9d7fe07c9..7c4e2a70d 100644 --- a/tests/test_dotnet_signals.py +++ b/tests/test_dotnet_signals.py @@ -65,6 +65,9 @@ def run_dotnet_native_crash(tmp_path): reason="dotnet signal handling is currently only supported on 64-bit Linux without sanitizers", ) def test_dotnet_signals_inproc(cmake): + if shutil.which("dotnet") is None: + pytest.skip("dotnet is not installed") + try: # build native client library with inproc and the example for crash dumping tmp_path = cmake( @@ -166,6 +169,9 @@ def run_aot_native_crash(tmp_path): reason="dotnet AOT signal handling is currently only supported on 64-bit Linux without sanitizers", ) def test_aot_signals_inproc(cmake): + if shutil.which("dotnet") is None: + pytest.skip("dotnet is not installed") + try: # build native client library with inproc and the example for crash dumping tmp_path = cmake( diff --git a/tests/unit/test_process.c b/tests/unit/test_process.c index baeff7b20..71d6565b5 100644 --- a/tests/unit/test_process.c +++ b/tests/unit/test_process.c @@ -24,6 +24,17 @@ SENTRY_TEST(process_invalid) sentry__path_free(nul); } +void find_cp_path(char* buf, size_t buf_len) +{ + FILE *fp = popen("command -v cp 2>/dev/null", "r"); + if (fp && fgets(buf, buf_len, fp)) { + buf[strcspn(buf, "\n")] = 0; // strip newline + } + if (fp) { + pclose(fp); + } +} + SENTRY_TEST(process_spawn) { #if defined(SENTRY_PLATFORM_WINDOWS) || defined(SENTRY_PLATFORM_MACOS) \ @@ -46,8 +57,10 @@ SENTRY_TEST(process_spawn) sentry__process_spawn(cmd, "/C", "copy", exe->path, dst->path, NULL); sentry__path_free(cmd); # else - // /bin/cp - sentry_path_t *cp = sentry__path_from_str("/bin/cp"); + char cp_path[512] = "/bin/cp"; + find_cp_path(cp_path, sizeof(cp_path)); + // cp + sentry_path_t *cp = sentry__path_from_str(cp_path); TEST_ASSERT(!!cp); sentry__process_spawn(cp, exe->path, dst->path, NULL); sentry__path_free(cp); From b83fc370773ae14f78c18cfe78567f886ae1b011 Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Fri, 7 Nov 2025 11:27:53 +0100 Subject: [PATCH 006/130] ensure that find_cp_path isn't built on windows. --- tests/unit/test_process.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/unit/test_process.c b/tests/unit/test_process.c index 71d6565b5..8fed7546e 100644 --- a/tests/unit/test_process.c +++ b/tests/unit/test_process.c @@ -24,7 +24,9 @@ SENTRY_TEST(process_invalid) sentry__path_free(nul); } -void find_cp_path(char* buf, size_t buf_len) +#ifndef SENTRY_PLATFORM_WINDOWS +void +find_cp_path(char *buf, size_t buf_len) { FILE *fp = popen("command -v cp 2>/dev/null", "r"); if (fp && fgets(buf, buf_len, fp)) { @@ -34,6 +36,7 @@ void find_cp_path(char* buf, size_t buf_len) pclose(fp); } } +#endif SENTRY_TEST(process_spawn) { From b57995ca3637fcc10440eba6d8693212c01bbfed Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Fri, 7 Nov 2025 11:45:10 +0100 Subject: [PATCH 007/130] fix trivial windows compile def issues to run tests locally --- src/backends/sentry_backend_inproc.c | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/backends/sentry_backend_inproc.c b/src/backends/sentry_backend_inproc.c index 25fb3d03b..d12294ad1 100644 --- a/src/backends/sentry_backend_inproc.c +++ b/src/backends/sentry_backend_inproc.c @@ -66,6 +66,9 @@ static int g_handler_pipe[2] = { -1, -1 }; static HANDLE g_handler_semaphore = NULL; #endif +static int start_handler_thread(void); +static void stop_handler_thread(void); + #ifdef SENTRY_PLATFORM_UNIX # include struct signal_slot { @@ -90,8 +93,6 @@ static const struct signal_slot SIGNAL_DEFINITIONS[SIGNAL_COUNT] = { }; static void handle_signal(int signum, siginfo_t *info, void *user_context); -static int start_handler_thread(void); -static void stop_handler_thread(void); static void reset_signal_handlers(void) @@ -237,9 +238,7 @@ startup_inproc_backend( { g_backend_config.enable_logging_when_crashed = options ? options->enable_logging_when_crashed : true; - g_backend_config.handler_strategy = options - ? sentry_options_get_handler_strategy(options) - : SENTRY_HANDLER_STRATEGY_DEFAULT; + g_backend_config.handler_strategy = SENTRY_HANDLER_STRATEGY_DEFAULT; if (backend) { backend->data = &g_backend_config; } @@ -948,9 +947,9 @@ process_ucontext(const sentry_ucontext_t *uctx) sentry__logger_disable(); } - sentry_handler_strategy_t strategy = g_backend_config.handler_strategy; #ifdef SENTRY_PLATFORM_LINUX + sentry_handler_strategy_t strategy = g_backend_config.handler_strategy; if (strategy == SENTRY_HANDLER_STRATEGY_CHAIN_AT_START) { // On Linux (and thus Android) CLR/Mono converts signals provoked by // AOT/JIT-generated native code into managed code exceptions. In these From ea527caa8cf9d6cc5486ab7822f20ee6f96722f4 Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Fri, 7 Nov 2025 14:06:52 +0100 Subject: [PATCH 008/130] clean up backends * use `std::nothrow` `new` consistently to keep exception-free semantics for allocation * rename static crashpad_handler to have no module-public prefix * use `nullptr` for arguments where we previously used 0 to clarify that those are pointers * eliminate the `memset()` of the `crashpad_state_t` initialization since it now contains non-trivially constructable fields (`std::atomic`) and replace it with `new` and an empty value initializer. --- src/backends/sentry_backend_breakpad.cpp | 8 ++++---- src/backends/sentry_backend_crashpad.cpp | 22 ++++++++++------------ src/backends/sentry_backend_inproc.c | 1 - 3 files changed, 14 insertions(+), 17 deletions(-) diff --git a/src/backends/sentry_backend_breakpad.cpp b/src/backends/sentry_backend_breakpad.cpp index 1c737ce55..a312e69c9 100644 --- a/src/backends/sentry_backend_breakpad.cpp +++ b/src/backends/sentry_backend_breakpad.cpp @@ -261,22 +261,22 @@ breakpad_backend_startup( && defined(SENTRY_THREAD_STACK_GUARANTEE_AUTO_INIT) sentry__set_default_thread_stack_guarantee(); # endif - backend->data = new google_breakpad::ExceptionHandler( + backend->data = new (std::nothrow) google_breakpad::ExceptionHandler( current_run_folder->path_w, nullptr, breakpad_backend_callback, nullptr, google_breakpad::ExceptionHandler::HANDLER_EXCEPTION); #elif defined(SENTRY_PLATFORM_MACOS) // If process is being debugged and there are breakpoints set it will cause // task_set_exception_ports to crash the whole process and debugger - backend->data = new google_breakpad::ExceptionHandler( + backend->data = new (std::nothrow) google_breakpad::ExceptionHandler( current_run_folder->path, nullptr, breakpad_backend_callback, nullptr, !IsDebuggerActive(), nullptr); #elif defined(SENTRY_PLATFORM_IOS) backend->data - = new google_breakpad::ExceptionHandler(current_run_folder->path, + = new (std::nothrow) google_breakpad::ExceptionHandler(current_run_folder->path, nullptr, breakpad_backend_callback, nullptr, true, nullptr); #else google_breakpad::MinidumpDescriptor descriptor(current_run_folder->path); - backend->data = new google_breakpad::ExceptionHandler( + backend->data = new (std::nothrow) google_breakpad::ExceptionHandler( descriptor, nullptr, breakpad_backend_callback, nullptr, true, -1); #endif return backend->data == nullptr; diff --git a/src/backends/sentry_backend_crashpad.cpp b/src/backends/sentry_backend_crashpad.cpp index 181e3d6c4..9570170c7 100644 --- a/src/backends/sentry_backend_crashpad.cpp +++ b/src/backends/sentry_backend_crashpad.cpp @@ -310,11 +310,11 @@ flush_scope_from_handler( # ifdef SENTRY_PLATFORM_WINDOWS static bool -sentry__crashpad_handler(EXCEPTION_POINTERS *ExceptionInfo) +crashpad_handler(EXCEPTION_POINTERS *ExceptionInfo) { # else static bool -sentry__crashpad_handler(int signum, siginfo_t *info, ucontext_t *user_context) +crashpad_handler(int signum, siginfo_t *info, ucontext_t *user_context) { sentry__page_allocator_enable(); sentry__enter_signal_handler(); @@ -540,7 +540,7 @@ crashpad_backend_startup( // Initialize database first, flushing the consent later on as part of // `sentry_init` will persist the upload flag. data->db = crashpad::CrashReportDatabase::Initialize(database).release(); - data->client = new crashpad::CrashpadClient; + data->client = new (std::nothrow) crashpad::CrashpadClient; char *minidump_url = sentry__dsn_get_minidump_url(options->dsn, options->user_agent); if (minidump_url) { @@ -581,8 +581,7 @@ crashpad_backend_startup( } #if defined(SENTRY_PLATFORM_LINUX) || defined(SENTRY_PLATFORM_WINDOWS) - crashpad::CrashpadClient::SetFirstChanceExceptionHandler( - &sentry__crashpad_handler); + crashpad::CrashpadClient::SetFirstChanceExceptionHandler(&crashpad_handler); #endif #ifdef SENTRY_PLATFORM_LINUX // Crashpad was recently changed to register its own signal stack, which for @@ -593,7 +592,7 @@ crashpad_backend_startup( if (g_signal_stack.ss_sp) { g_signal_stack.ss_size = SIGNAL_STACK_SIZE; g_signal_stack.ss_flags = 0; - sigaltstack(&g_signal_stack, 0); + sigaltstack(&g_signal_stack, nullptr); } #endif @@ -632,9 +631,9 @@ crashpad_backend_shutdown(sentry_backend_t *backend) #ifdef SENTRY_PLATFORM_LINUX g_signal_stack.ss_flags = SS_DISABLE; - sigaltstack(&g_signal_stack, 0); + sigaltstack(&g_signal_stack, nullptr); sentry_free(g_signal_stack.ss_sp); - g_signal_stack.ss_sp = NULL; + g_signal_stack.ss_sp = nullptr; #endif } @@ -745,8 +744,8 @@ crashpad_backend_prune_database(sentry_backend_t *backend) // large. data->db->CleanDatabase(60 * 60 * 24 * 2); crashpad::BinaryPruneCondition condition(crashpad::BinaryPruneCondition::OR, - new crashpad::DatabaseSizePruneCondition(1024 * 8), - new crashpad::AgePruneCondition(2)); + new (std::nothrow) crashpad::DatabaseSizePruneCondition(1024 * 8), + new (std::nothrow) crashpad::AgePruneCondition(2)); crashpad::PruneCrashReportDatabase(data->db, &condition); } @@ -825,12 +824,11 @@ sentry__backend_new(void) } memset(backend, 0, sizeof(sentry_backend_t)); - auto *data = SENTRY_MAKE(crashpad_state_t); + auto *data = new (std::nothrow) crashpad_state_t {}; if (!data) { sentry_free(backend); return nullptr; } - memset(data, 0, sizeof(crashpad_state_t)); data->scope_flush = false; data->crashed = false; diff --git a/src/backends/sentry_backend_inproc.c b/src/backends/sentry_backend_inproc.c index d12294ad1..3f9ced4f4 100644 --- a/src/backends/sentry_backend_inproc.c +++ b/src/backends/sentry_backend_inproc.c @@ -947,7 +947,6 @@ process_ucontext(const sentry_ucontext_t *uctx) sentry__logger_disable(); } - #ifdef SENTRY_PLATFORM_LINUX sentry_handler_strategy_t strategy = g_backend_config.handler_strategy; if (strategy == SENTRY_HANDLER_STRATEGY_CHAIN_AT_START) { From c78f4250dc7b21632432240be793622846b52269 Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Mon, 10 Nov 2025 09:12:19 +0100 Subject: [PATCH 009/130] eliminate local handler strategy declaration in signal handler --- src/backends/sentry_backend_inproc.c | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/backends/sentry_backend_inproc.c b/src/backends/sentry_backend_inproc.c index 3f9ced4f4..db911a151 100644 --- a/src/backends/sentry_backend_inproc.c +++ b/src/backends/sentry_backend_inproc.c @@ -948,8 +948,8 @@ process_ucontext(const sentry_ucontext_t *uctx) } #ifdef SENTRY_PLATFORM_LINUX - sentry_handler_strategy_t strategy = g_backend_config.handler_strategy; - if (strategy == SENTRY_HANDLER_STRATEGY_CHAIN_AT_START) { + if (g_backend_config.handler_strategy + == SENTRY_HANDLER_STRATEGY_CHAIN_AT_START) { // On Linux (and thus Android) CLR/Mono converts signals provoked by // AOT/JIT-generated native code into managed code exceptions. In these // cases, we shouldn't react to the signal at all and let their handler @@ -1040,7 +1040,8 @@ process_ucontext(const sentry_ucontext_t *uctx) // forward as we're not restoring the page allocator. reset_signal_handlers(); sentry__leave_signal_handler(); - if (strategy != SENTRY_HANDLER_STRATEGY_CHAIN_AT_START) { + if (g_backend_config.handler_strategy + != SENTRY_HANDLER_STRATEGY_CHAIN_AT_START) { invoke_signal_handler( uctx->signum, uctx->siginfo, (void *)uctx->user_context); } From 116c4a52463e0f745a933fafef9e44230ee9589e Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Mon, 10 Nov 2025 09:52:59 +0100 Subject: [PATCH 010/130] turn off painful warning noise --- CMakeLists.txt | 9 +++++++++ src/backends/sentry_backend_crashpad.cpp | 2 -- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index b7639b7e5..236b11f62 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -679,6 +679,15 @@ if(SENTRY_BACKEND_CRASHPAD) $ $ ) + + # ignore #include_next warning being a GCC extension via pedantic + if (LINUX) + target_include_directories(sentry + SYSTEM PRIVATE + external/crashpad/compat/linux + ) + endif() + install(EXPORT crashpad_export NAMESPACE sentry_crashpad:: FILE sentry_crashpad-targets.cmake DESTINATION "${CMAKE_INSTALL_CMAKEDIR}" ) diff --git a/src/backends/sentry_backend_crashpad.cpp b/src/backends/sentry_backend_crashpad.cpp index 9570170c7..17ca48fa3 100644 --- a/src/backends/sentry_backend_crashpad.cpp +++ b/src/backends/sentry_backend_crashpad.cpp @@ -32,8 +32,6 @@ extern "C" { #if defined(__GNUC__) # pragma GCC diagnostic push # pragma GCC diagnostic ignored "-Wunused-parameter" -# pragma GCC diagnostic ignored "-Wgnu-zero-variadic-macro-arguments" -# pragma GCC diagnostic ignored "-Wfour-char-constants" #elif defined(_MSC_VER) # pragma warning(push) # pragma warning(disable : 4100) // unreferenced formal parameter From b4747c7b0ee739f5681b4a0bdb8e394943da778c Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Mon, 10 Nov 2025 10:13:40 +0100 Subject: [PATCH 011/130] add libunwind to the CI dependencies --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b2af65145..140384c1f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -200,7 +200,7 @@ jobs: if: ${{ runner.os == 'Linux' && matrix.os != 'ubuntu-22.04' && !env['TEST_X86'] && !matrix.container }} run: | sudo apt update - sudo apt install cmake clang-19 llvm g++-12 valgrind zlib1g-dev libcurl4-openssl-dev + sudo apt install cmake clang-19 llvm g++-12 valgrind zlib1g-dev libcurl4-openssl-dev libunwind-dev # Install kcov from source sudo apt-get install binutils-dev libssl-dev libelf-dev libstdc++-12-dev libdw-dev libiberty-dev git clone https://github.com/SimonKagstrom/kcov.git From d21c180f05c0ab2d484207c8c70018fbac7e3cf1 Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Mon, 10 Nov 2025 10:19:56 +0100 Subject: [PATCH 012/130] ensure we build the example with PIE disabled when doing a static build, since libraries like libunwind.a might be packaged without PIC. --- CMakeLists.txt | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 236b11f62..41f46f9d5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -816,6 +816,14 @@ if(SENTRY_BUILD_EXAMPLES) # Disable all optimizations for the `sentry_example` in gcc/clang. This allows us to keep crash triggers simple. # The effects besides reproducible code-gen across compiler versions, will be negligible for build- and runtime. target_compile_options(sentry_example PRIVATE $) + + # Most Linux distros will build executables with PIE. However, if we build a static library, not all static + # dependencies being packaged with the same distro, are built with PIC enabled. This will lead `ld` to error on + # relocation data, thus failing the build/tests. There is no need to build the example with PIE enabled. + if (LINUX AND NOT SENTRY_BUILD_SHARED_LIBS) + target_link_options(sentry_example PRIVATE -no-pie) + target_compile_options(sentry_example PRIVATE -fno-pie) + endif() endif() # set static runtime if enabled From b0822d3f4476b93af51691ddeef2324bcdd1d2ae Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Mon, 10 Nov 2025 10:53:29 +0100 Subject: [PATCH 013/130] extend handler crash to windows --- src/backends/sentry_backend_breakpad.cpp | 12 +++---- src/backends/sentry_backend_inproc.c | 46 ++++++++++++++---------- src/sentry_sync.h | 12 +++++++ 3 files changed, 45 insertions(+), 25 deletions(-) diff --git a/src/backends/sentry_backend_breakpad.cpp b/src/backends/sentry_backend_breakpad.cpp index a312e69c9..528a3c00a 100644 --- a/src/backends/sentry_backend_breakpad.cpp +++ b/src/backends/sentry_backend_breakpad.cpp @@ -267,13 +267,13 @@ breakpad_backend_startup( #elif defined(SENTRY_PLATFORM_MACOS) // If process is being debugged and there are breakpoints set it will cause // task_set_exception_ports to crash the whole process and debugger - backend->data = new (std::nothrow) google_breakpad::ExceptionHandler( - current_run_folder->path, nullptr, breakpad_backend_callback, nullptr, - !IsDebuggerActive(), nullptr); + backend->data = new (std::nothrow) + google_breakpad::ExceptionHandler(current_run_folder->path, nullptr, + breakpad_backend_callback, nullptr, !IsDebuggerActive(), nullptr); #elif defined(SENTRY_PLATFORM_IOS) - backend->data - = new (std::nothrow) google_breakpad::ExceptionHandler(current_run_folder->path, - nullptr, breakpad_backend_callback, nullptr, true, nullptr); + backend->data = new (std::nothrow) + google_breakpad::ExceptionHandler(current_run_folder->path, nullptr, + breakpad_backend_callback, nullptr, true, nullptr); #else google_breakpad::MinidumpDescriptor descriptor(current_run_folder->path); backend->data = new (std::nothrow) google_breakpad::ExceptionHandler( diff --git a/src/backends/sentry_backend_inproc.c b/src/backends/sentry_backend_inproc.c index db911a151..270c65aa8 100644 --- a/src/backends/sentry_backend_inproc.c +++ b/src/backends/sentry_backend_inproc.c @@ -855,6 +855,32 @@ stop_handler_thread(void) sentry__atomic_store(&g_handler_should_exit, 0); } +static bool +did_handler_thread_crash(void) +{ + const sentry_threadid_t current_thread = sentry__current_thread(); + if (g_handler_thread_ready + && sentry__threadid_equal(current_thread, g_handler_thread)) { + // This means our handler thread crashed, there is no safe way out: + // fall back to previous handler + static const char msg[] = "[sentry] FATAL crash in handler thread, " + "falling back to previous handler\n"; +#ifdef SENTRY_PLATFORM_UNIX + const ssize_t rv = write(STDERR_FILENO, msg, sizeof(msg) - 1); + (void)rv; +#else + OutputDebugStringA(msg); + HANDLE stderr_handle = GetStdHandle(STD_ERROR_HANDLE); + if (stderr_handle && stderr_handle != INVALID_HANDLE_VALUE) { + DWORD written; + WriteFile(stderr_handle, msg, (DWORD)strlen(msg), &written, NULL); + } +#endif + return true; + } + return false; +} + static void dispatch_ucontext( const sentry_ucontext_t *uctx, const struct signal_slot *sig_slot) @@ -1010,25 +1036,7 @@ process_ucontext(const sentry_ucontext_t *uctx) sentry__page_allocator_enable(); #endif - const sentry_threadid_t current_thread = sentry__current_thread(); - if (g_handler_thread_ready - && sentry__threadid_equal(current_thread, g_handler_thread)) { - // This means our handler thread crashed, there is no safe way out: - // make an async-signal-safe log and defer to previous - static const char msg[] = "[sentry] FATAL crash in handler thread, " - "falling back to previous handler\n"; -#ifdef SENTRY_PLATFORM_UNIX - const ssize_t rv = write(STDERR_FILENO, msg, sizeof(msg) - 1); - (void)rv; -#else - OutputDebugStringA(msg); - HANDLE stderr_handle = GetStdHandle(STD_ERROR_HANDLE); - if (stderr_handle && stderr_handle != INVALID_HANDLE_VALUE) { - DWORD written; - WriteFile(stderr_handle, msg, (DWORD)strlen(msg), &written, NULL); - } -#endif - } else { + if (!did_handler_thread_crash()) { // invoke the handler thread for signal unsafe actions dispatch_ucontext(uctx, sig_slot); } diff --git a/src/sentry_sync.h b/src/sentry_sync.h index 8761cd6ab..6157e5fb8 100644 --- a/src/sentry_sync.h +++ b/src/sentry_sync.h @@ -207,6 +207,18 @@ typedef CONDITION_VARIABLE sentry_cond_t; # define sentry__cond_wait(CondVar, Lock) \ sentry__cond_wait_timeout(CondVar, Lock, INFINITE) +static inline sentry_threadid_t +sentry__current_thread(void) +{ + return GetCurrentThread(); +} + +static inline int +sentry__threadid_equal(sentry_threadid_t a, sentry_threadid_t b) +{ + return GetThreadId(a) == GetThreadId(b); +} + #else # include # include From 8b93ccac3764881ccef6f9b241ddc0f0b167620e Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Mon, 10 Nov 2025 11:04:36 +0100 Subject: [PATCH 014/130] further clean of inproc --- src/backends/sentry_backend_inproc.c | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/src/backends/sentry_backend_inproc.c b/src/backends/sentry_backend_inproc.c index 270c65aa8..63aab60be 100644 --- a/src/backends/sentry_backend_inproc.c +++ b/src/backends/sentry_backend_inproc.c @@ -647,9 +647,9 @@ make_signal_event(const struct signal_slot *sig_slot, * requires stdio, time-formatting/-capture or serialization must happen here. * * Although we can use signal-unsafe functions here, this should still be - * written with care. Don't overly rely on thread synchronization since the - * program is in a crashed state. At least one thread no longer progresses and - * memory can be corrupted. + * written with care. Don't rely on thread synchronization or the system + * allocator since the program is in a crashed state. At least one thread no + * longer progresses and memory can be corrupted. */ static void process_ucontext_deferred( @@ -861,14 +861,15 @@ did_handler_thread_crash(void) const sentry_threadid_t current_thread = sentry__current_thread(); if (g_handler_thread_ready && sentry__threadid_equal(current_thread, g_handler_thread)) { - // This means our handler thread crashed, there is no safe way out: - // fall back to previous handler - static const char msg[] = "[sentry] FATAL crash in handler thread, " - "falling back to previous handler\n"; #ifdef SENTRY_PLATFORM_UNIX + static const char msg[] + = "[sentry] FATAL crash in handler thread, " + "falling back to previous handler\n"; const ssize_t rv = write(STDERR_FILENO, msg, sizeof(msg) - 1); (void)rv; #else + static const char msg[] = "[sentry] FATAL crash in handler thread, " + "UEF continues search\n"; OutputDebugStringA(msg); HANDLE stderr_handle = GetStdHandle(STD_ERROR_HANDLE); if (stderr_handle && stderr_handle != INVALID_HANDLE_VALUE) { @@ -944,12 +945,14 @@ dispatch_ucontext( * This is the signal-safe part of the inproc handler. Everything in here should * not defer to more than the set of functions listed in: * https://www.man7.org/linux/man-pages/man7/signal-safety.7.html + * Since this function runs as an UnhandledExceptionFilter on Windows, the rules + * are less strict, but similar in nature. * * That means: * - no heap allocations except for sentry_malloc() (page allocator enabled!!!) * - no stdio or any kind of libc string formatting * - no logging (at least not with the printf-based default logger) - * - no pthread synchronization (SENTRY_WITH_OPTIONS will terminate with a log) + * - no thread synchronization (SENTRY_WITH_OPTIONS will terminate with a log) * - in particular, don't access sentry interfaces that could request * access to options or the scope, those should go to the handler thread * - sentry_value_* and sentry_malloc are generally fine, because we use a safe @@ -1076,8 +1079,7 @@ handle_exception(EXCEPTION_POINTERS *ExceptionInfo) return EXCEPTION_CONTINUE_SEARCH; } - sentry_ucontext_t uctx; - memset(&uctx, 0, sizeof(uctx)); + sentry_ucontext_t uctx = { 0 }; uctx.exception_ptrs = *ExceptionInfo; process_ucontext(&uctx); return EXCEPTION_CONTINUE_SEARCH; From c4f662a191d62ca19c47577e11ff23864f8ea682 Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Mon, 10 Nov 2025 11:19:54 +0100 Subject: [PATCH 015/130] review and fix/remove remaining TODOs from branch changes --- src/backends/sentry_backend_inproc.c | 15 +++++++-------- src/sentry_sync.c | 5 +---- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/src/backends/sentry_backend_inproc.c b/src/backends/sentry_backend_inproc.c index 63aab60be..849956177 100644 --- a/src/backends/sentry_backend_inproc.c +++ b/src/backends/sentry_backend_inproc.c @@ -593,11 +593,11 @@ make_signal_event(const struct signal_slot *sig_slot, if (sig_slot) { sentry_value_set_by_key( signal_meta, "name", sentry_value_new_string(sig_slot->signame)); - // at least on windows, the signum is a true u32 which we can't - // otherwise represent. - // TODO: does that still match reality? - sentry_value_set_by_key(signal_meta, "number", - sentry_value_new_double((double)sig_slot->signum)); + // relay interprets the signal number as an i64: + // https://github.com/getsentry/relay/blob/e96e4b037cfddaa7b0fb97a0909d100dde034f8e/relay-event-schema/src/protocol/mechanism.rs#L52-L53 + // This covers the signal number ranges of all supported platforms. + sentry_value_set_by_key( + signal_meta, "number", sentry_value_new_int64(sig_slot->signum)); } sentry_value_set_by_key(mechanism_meta, "signal", signal_meta); sentry_value_set_by_key( @@ -862,9 +862,8 @@ did_handler_thread_crash(void) if (g_handler_thread_ready && sentry__threadid_equal(current_thread, g_handler_thread)) { #ifdef SENTRY_PLATFORM_UNIX - static const char msg[] - = "[sentry] FATAL crash in handler thread, " - "falling back to previous handler\n"; + static const char msg[] = "[sentry] FATAL crash in handler thread, " + "falling back to previous handler\n"; const ssize_t rv = write(STDERR_FILENO, msg, sizeof(msg) - 1); (void)rv; #else diff --git a/src/sentry_sync.c b/src/sentry_sync.c index 1af5979b7..5e7bfe118 100644 --- a/src/sentry_sync.c +++ b/src/sentry_sync.c @@ -567,8 +567,7 @@ sentry__enter_signal_handler(void) // entering a signal handler while another runs, should block us while (__atomic_load_n(&g_in_signal_handler, __ATOMIC_RELAXED)) { // however, if we re-enter most likely a signal was raised from - // within the handler and then we should proceed. - // TODO: maybe pass in the signum and check for SIGABRT loops here + // within the signal handler and then we should proceed. if (is_handling_thread()) { return; } @@ -599,8 +598,6 @@ sentry__switch_handler_thread(void) sentry_threadid_t current = sentry__current_thread(); __atomic_store_n(&g_signal_handling_thread, current, __ATOMIC_RELEASE); - // TODO: this is still insufficient as a safe-guard when crashing in the - // handler thread # ifdef SENTRY_BACKEND_INPROC __atomic_store_n(&g_signal_handler_can_lock, 1, __ATOMIC_RELEASE); # endif From 84b3a7a812141a924cf2f23546ab3fe43c9c721e Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Mon, 10 Nov 2025 12:07:51 +0100 Subject: [PATCH 016/130] eliminate multichar warning in crashpad backend when using GCC --- src/backends/sentry_backend_crashpad.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/backends/sentry_backend_crashpad.cpp b/src/backends/sentry_backend_crashpad.cpp index 17ca48fa3..72de92078 100644 --- a/src/backends/sentry_backend_crashpad.cpp +++ b/src/backends/sentry_backend_crashpad.cpp @@ -32,6 +32,7 @@ extern "C" { #if defined(__GNUC__) # pragma GCC diagnostic push # pragma GCC diagnostic ignored "-Wunused-parameter" +# pragma GCC diagnostic ignored "-Wmultichar" #elif defined(_MSC_VER) # pragma warning(push) # pragma warning(disable : 4100) // unreferenced formal parameter From ef7b03b4f4c88ee34cc098c80aaf69403b549599 Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Mon, 10 Nov 2025 12:09:17 +0100 Subject: [PATCH 017/130] ensure libunwind in benchmark and codeql workflows --- .github/workflows/benchmark.yml | 2 +- .github/workflows/codeql.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index e4b73d0b8..024d6fd74 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -44,7 +44,7 @@ jobs: if: ${{ runner.os == 'Linux' }} run: | sudo apt update - sudo apt install -y libcurl4-openssl-dev + sudo apt install -y libcurl4-openssl-dev libunwind-dev - name: Benchmark shell: bash diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index fb5468074..e6fd5681d 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -37,7 +37,7 @@ jobs: - name: Installing Linux Dependencies run: | sudo apt update - sudo apt install cmake clang-14 clang-tools llvm kcov g++-12 valgrind zlib1g-dev libcurl4-openssl-dev + sudo apt install cmake clang-14 clang-tools llvm kcov g++-12 valgrind zlib1g-dev libcurl4-openssl-dev libunwind-dev - if: matrix.language == 'java' name: Setup Java Version From 6ae25c3b9079e794f49e9969774df6505911b3e6 Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Mon, 10 Nov 2025 12:25:51 +0100 Subject: [PATCH 018/130] fix Linux ARM64 build (warning in libunwind) --- src/unwinder/sentry_unwinder_libunwind.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/unwinder/sentry_unwinder_libunwind.c b/src/unwinder/sentry_unwinder_libunwind.c index 844558dd7..3445cd172 100644 --- a/src/unwinder/sentry_unwinder_libunwind.c +++ b/src/unwinder/sentry_unwinder_libunwind.c @@ -21,7 +21,12 @@ sentry__unwind_stack_libunwind( } } else { unw_context_t uc; +// This pragma is required to build with Werror on ARM64 Ubuntu +#pragma clang diagnostic push +#pragma clang diagnostic ignored \ + "-Wgnu-statement-expression-from-macro-expansion" int ret = unw_getcontext(&uc); +#pragma clang diagnostic pop if (ret != 0) { SENTRY_WARN("Failed to retrieve context with libunwind"); return 0; From a0195dbcb87b15914f37f786a52ddf24e00da912 Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Mon, 10 Nov 2025 14:09:13 +0100 Subject: [PATCH 019/130] provide an x86_64 ucontext stackwalker for macOS --- CMakeLists.txt | 10 +- src/unwinder/sentry_unwinder_libunwind_mac.c | 154 +++++++++++-------- 2 files changed, 102 insertions(+), 62 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 41f46f9d5..0f72823e1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -835,11 +835,17 @@ if(SENTRY_BUILD_EXAMPLES) set_target_properties(sentry_example PROPERTIES FOLDER ${SENTRY_FOLDER}) endif() + if(CMAKE_GENERATOR STREQUAL "Xcode") + set(SENTRY_FIXTURE_OUTPUT_DIR "$/..") + else() + set(SENTRY_FIXTURE_OUTPUT_DIR "$") + endif() + add_custom_command(TARGET sentry_example POST_BUILD - COMMAND ${CMAKE_COMMAND} -E copy_if_different "${CMAKE_CURRENT_SOURCE_DIR}/tests/fixtures/minidump.dmp" "$/minidump.dmp") + COMMAND ${CMAKE_COMMAND} -E copy_if_different "${CMAKE_CURRENT_SOURCE_DIR}/tests/fixtures/minidump.dmp" "${SENTRY_FIXTURE_OUTPUT_DIR}/minidump.dmp") add_custom_command(TARGET sentry_example POST_BUILD - COMMAND ${CMAKE_COMMAND} -E copy_if_different "${CMAKE_CURRENT_SOURCE_DIR}/tests/fixtures/view-hierarchy.json" "$/view-hierarchy.json") + COMMAND ${CMAKE_COMMAND} -E copy_if_different "${CMAKE_CURRENT_SOURCE_DIR}/tests/fixtures/view-hierarchy.json" "${SENTRY_FIXTURE_OUTPUT_DIR}/view-hierarchy.json") endif() # Limit the exported symbols when sentry is built as a shared library to those with a "sentry_" prefix: diff --git a/src/unwinder/sentry_unwinder_libunwind_mac.c b/src/unwinder/sentry_unwinder_libunwind_mac.c index 176fb287b..b228300a0 100644 --- a/src/unwinder/sentry_unwinder_libunwind_mac.c +++ b/src/unwinder/sentry_unwinder_libunwind_mac.c @@ -10,82 +10,116 @@ valid_ptr(uintptr_t p) } size_t -sentry__unwind_stack_libunwind_mac( - void *addr, const sentry_ucontext_t *uctx, void **ptrs, size_t max_frames) +fp_walk_from_uctx(const sentry_ucontext_t *uctx, void **ptrs, size_t max_frames) { - if (addr) { - return 0; - } size_t frame_idx = 0; + struct __darwin_mcontext64 *mctx = uctx->user_context->uc_mcontext; +#if defined(__arm64__) + uintptr_t pc = (uintptr_t)mctx->__ss.__pc; + uintptr_t fp = (uintptr_t)mctx->__ss.__fp; + uintptr_t lr = (uintptr_t)mctx->__ss.__lr; - if (uctx) { - // TODO: this is a working ARM64 from ucontext unwinder using FP that - // doesn't have the issues we see backtrace() (which isn't even - // signal-safe) but also the system provided libunwind() on macOS - // - clean this up - // - implement an x86_64 ucontext unwinder + // top frame: adjust pc−1 so it symbolizes inside the function + if (pc) { + ptrs[frame_idx++] = (void *)(pc - 1); + } - struct __darwin_mcontext64 *mctx = uctx->user_context->uc_mcontext; - uintptr_t pc = (uintptr_t)mctx->__ss.__pc; - uintptr_t fp = (uintptr_t)mctx->__ss.__fp; - uintptr_t lr = (uintptr_t)mctx->__ss.__lr; + // next frame is from saved LR at current FP record + if (lr) { + ptrs[frame_idx++] = (void *)(lr - 1); + } - // top frame: adjust pc−1 so it symbolizes inside the function - if (pc) { - ptrs[frame_idx++] = (void *)(pc - 1); + for (size_t i = 0; i < max_frames; ++i) { + if (!valid_ptr(fp)) { + break; } - // next frame is from saved LR at current FP record - if (lr) { - ptrs[frame_idx++] = (void *)(lr - 1); + // arm64 frame record layout: [prev_fp, saved_lr] at fp and fp+8 + uintptr_t *record = (uintptr_t *)fp; + uintptr_t next_fp = record[0]; + uintptr_t ret_addr = record[1]; + if (!valid_ptr(next_fp) || !ret_addr) { + break; } - for (size_t i = 0; i < max_frames; ++i) { - if (!valid_ptr(fp)) { - break; - } + ptrs[frame_idx++] = (void *)(ret_addr - 1); + if (next_fp <= fp) { + break; // prevent loops + } + fp = next_fp; + } +#elif defined(__x86_64__) + uintptr_t ip = (uintptr_t)mctx->__ss.__rip; + uintptr_t bp = (uintptr_t)mctx->__ss.__rbp; - // arm64 frame record layout: [prev_fp, saved_lr] at fp and fp+8 - uintptr_t *record = (uintptr_t *)fp; - uintptr_t next_fp = record[0]; - uintptr_t retaddr = record[1]; - if (!valid_ptr(next_fp) || !retaddr) { - break; - } + // top frame: adjust ip−1 so it symbolizes inside the function + if (ip) { + ptrs[frame_idx++] = (void *)(ip - 1); + } - ptrs[frame_idx++] = (void *)(retaddr - 1); - if (next_fp <= fp) { - break; // prevent loops - } - fp = next_fp; + for (size_t i = 0; i < max_frames; ++i) { + if (!valid_ptr(bp)) { + break; } - } else { - unw_context_t uc; - int ret = unw_getcontext(&uc); - if (ret != 0) { - SENTRY_WARN("Failed to retrieve context with libunwind"); - return 0; + // x86_64 frame record layout: [prev_rbp, saved_retaddr] at bp and bp+8 + uintptr_t *record = (uintptr_t *)bp; + uintptr_t next_bp = record[0]; + uintptr_t ret_addr = record[1]; + if (!valid_ptr(next_bp) || !ret_addr) { + break; } - - unw_cursor_t cursor; - ret = unw_init_local(&cursor, &uc); - if (ret != 0) { - SENTRY_WARN("Failed to initialize libunwind with local context"); - return 0; + ptrs[frame_idx++] = (void *)(ret_addr - 1); + if (next_bp <= bp) { + break; } - while (unw_step(&cursor) > 0 && frame_idx < max_frames - 1) { - unw_word_t ip = 0; - SENTRY_INFOF("ip: %p", ip); - unw_get_reg(&cursor, UNW_REG_IP, &ip); + bp = next_bp; + } +#else +# error "Unsupported CPU architecture for macOS stackwalker" +#endif + return frame_idx + 1; +} + +size_t +sentry__unwind_stack_libunwind_mac( + void *addr, const sentry_ucontext_t *uctx, void **ptrs, size_t max_frames) +{ + if (addr) { + // we don't support stack walks from arbitrary addresses + return 0; + } + + if (uctx) { + return fp_walk_from_uctx(uctx, ptrs, max_frames); + } + + // fall back on the system `libunwind` for stack-traces "from call-site" + unw_context_t uc; + int ret = unw_getcontext(&uc); + if (ret != 0) { + SENTRY_WARN("Failed to retrieve context with libunwind"); + return 0; + } + + unw_cursor_t cursor; + ret = unw_init_local(&cursor, &uc); + if (ret != 0) { + SENTRY_WARN("Failed to initialize libunwind with local context"); + return 0; + } + size_t frame_idx = 0; + while (unw_step(&cursor) > 0 && frame_idx < max_frames - 1) { + unw_word_t ip = 0; + SENTRY_INFOF("ip: %p", ip); + unw_get_reg(&cursor, UNW_REG_IP, &ip); #if defined(__arm64__) - // Strip pointer authentication, for some reason ptrauth_strip() not - // working - // https://developer.apple.com/documentation/security/preparing_your_app_to_work_with_pointer_authentication - ip &= 0x7fffffffffffull; + // Strip pointer authentication, for some reason ptrauth_strip() not + // working + // https://developer.apple.com/documentation/security/preparing_your_app_to_work_with_pointer_authentication + ip &= 0x7fffffffffffull; #endif - ptrs[frame_idx] = (void *)ip; - frame_idx++; - } + ptrs[frame_idx] = (void *)ip; + frame_idx++; } return frame_idx + 1; From 8a63e64a6a46f7a0b2bd35092eede623456c682b Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Mon, 10 Nov 2025 14:17:15 +0100 Subject: [PATCH 020/130] bump lower-end GCC to 10.5.0 --- .github/workflows/ci.yml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 140384c1f..343eb90ba 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -39,15 +39,15 @@ jobs: fail-fast: false matrix: include: - - name: Linux (GCC 9.5.0, 32-bit) + - name: Linux (GCC 10.5.0, 32-bit) os: ubuntu-22.04 - CC: gcc-9 - CXX: g++-9 + CC: gcc-10 + CXX: g++-10 TEST_X86: 1 - - name: Linux (GCC 9.5.0) + - name: Linux (GCC 10.5.0) os: ubuntu-22.04 - CC: gcc-9 - CXX: g++-9 + CC: gcc-10 + CXX: g++-10 # ERROR_ON_WARNINGS: 1 - name: Linux (GCC 12.3.0) os: ubuntu-24.04 @@ -213,7 +213,7 @@ jobs: make sudo make install - - name: Installing Linux GCC 9.4.0 Dependencies + - name: Installing Linux GCC 10.5.0 Dependencies if: ${{ runner.os == 'Linux' && matrix.os == 'ubuntu-22.04' && !env['TEST_X86'] && !matrix.container }} run: | sudo apt update @@ -224,7 +224,7 @@ jobs: run: | sudo dpkg --add-architecture i386 sudo apt update - sudo apt install cmake gcc-9-multilib g++-9-multilib zlib1g-dev:i386 libssl-dev:i386 libcurl4-openssl-dev:i386 + sudo apt install cmake gcc-10-multilib g++-10-multilib zlib1g-dev:i386 libssl-dev:i386 libcurl4-openssl-dev:i386 - name: Installing Alpine Linux Dependencies if: ${{ contains(matrix.container, 'alpine') }} From c1fd0a870248dd333fca1bc95f1c04ea48a5a378 Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Mon, 10 Nov 2025 14:18:06 +0100 Subject: [PATCH 021/130] remove obsolete ASAN patch from CI --- .github/workflows/ci.yml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 343eb90ba..77db85925 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -232,11 +232,6 @@ jobs: apk update apk add curl-dev libunwind-dev libunwind-static xz-dev - # https://github.com/actions/runner-images/issues/9491 - - name: Decrease vm.mmap_rnd_bit to prevent ASLR ASAN issues - if: ${{ runner.os == 'Linux' && contains(env['RUN_ANALYZER'], 'asan') }} - run: sudo sysctl vm.mmap_rnd_bits=28 - - name: Installing CodeChecker if: ${{ contains(env['RUN_ANALYZER'], 'code-checker') }} run: sudo snap install codechecker --classic From 7c97dbb02f185ba298e29ae08672668fee522729 Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Mon, 10 Nov 2025 14:36:08 +0100 Subject: [PATCH 022/130] ensure lower-end GCC also finds libunwind --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 77db85925..f67cf3f29 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -217,14 +217,14 @@ jobs: if: ${{ runner.os == 'Linux' && matrix.os == 'ubuntu-22.04' && !env['TEST_X86'] && !matrix.container }} run: | sudo apt update - sudo apt install cmake llvm kcov g++ valgrind zlib1g-dev libcurl4-openssl-dev + sudo apt install cmake llvm kcov g++ valgrind zlib1g-dev libcurl4-openssl-dev libunwind-dev - name: Installing Linux 32-bit Dependencies if: ${{ runner.os == 'Linux' && env['TEST_X86'] && !matrix.container }} run: | sudo dpkg --add-architecture i386 sudo apt update - sudo apt install cmake gcc-10-multilib g++-10-multilib zlib1g-dev:i386 libssl-dev:i386 libcurl4-openssl-dev:i386 + sudo apt install cmake gcc-10-multilib g++-10-multilib zlib1g-dev:i386 libssl-dev:i386 libcurl4-openssl-dev:i386 libunwind-dev - name: Installing Alpine Linux Dependencies if: ${{ contains(matrix.container, 'alpine') }} From 524d8b4c17c55cbad0cb96c58c84a6fa8a35df90 Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Mon, 10 Nov 2025 14:48:19 +0100 Subject: [PATCH 023/130] disable LTO for the example too --- CMakeLists.txt | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 0f72823e1..f31f1f6a5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -820,9 +820,17 @@ if(SENTRY_BUILD_EXAMPLES) # Most Linux distros will build executables with PIE. However, if we build a static library, not all static # dependencies being packaged with the same distro, are built with PIC enabled. This will lead `ld` to error on # relocation data, thus failing the build/tests. There is no need to build the example with PIE enabled. + # Similarly, LTO must be disabled since the static library archive might have different LTO bytecode version. if (LINUX AND NOT SENTRY_BUILD_SHARED_LIBS) - target_link_options(sentry_example PRIVATE -no-pie) - target_compile_options(sentry_example PRIVATE -fno-pie) + target_link_options(sentry_example PRIVATE + -no-pie + -no-lto + -Wl,--disable-lto + ) + target_compile_options(sentry_example PRIVATE + -fno-pie + -fno-lto + ) endif() endif() From 1c378c3af09d22ea67bd071793f102b4584ae8df Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Mon, 10 Nov 2025 14:49:00 +0100 Subject: [PATCH 024/130] use clang diagnostics only for __clang__ --- src/unwinder/sentry_unwinder_libunwind.c | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/unwinder/sentry_unwinder_libunwind.c b/src/unwinder/sentry_unwinder_libunwind.c index 3445cd172..6ddf3c7df 100644 --- a/src/unwinder/sentry_unwinder_libunwind.c +++ b/src/unwinder/sentry_unwinder_libunwind.c @@ -21,12 +21,16 @@ sentry__unwind_stack_libunwind( } } else { unw_context_t uc; +#ifdef __clang__ // This pragma is required to build with Werror on ARM64 Ubuntu -#pragma clang diagnostic push -#pragma clang diagnostic ignored \ - "-Wgnu-statement-expression-from-macro-expansion" +# pragma clang diagnostic push +# pragma clang diagnostic ignored \ + "-Wgnu-statement-expression-from-macro-expansion" +#endif int ret = unw_getcontext(&uc); -#pragma clang diagnostic pop +#ifdef __clang__ +# pragma clang diagnostic pop +#endif if (ret != 0) { SENTRY_WARN("Failed to retrieve context with libunwind"); return 0; From 4912a824091e407e5e85abddc5e942679293447f Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Mon, 10 Nov 2025 15:00:39 +0100 Subject: [PATCH 025/130] install 32-bit libunwind package for 32-bit Linux CI test config --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f67cf3f29..18de82c8d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -224,7 +224,7 @@ jobs: run: | sudo dpkg --add-architecture i386 sudo apt update - sudo apt install cmake gcc-10-multilib g++-10-multilib zlib1g-dev:i386 libssl-dev:i386 libcurl4-openssl-dev:i386 libunwind-dev + sudo apt install cmake gcc-10-multilib g++-10-multilib zlib1g-dev:i386 libssl-dev:i386 libcurl4-openssl-dev:i386 libunwind-dev:i386 - name: Installing Alpine Linux Dependencies if: ${{ contains(matrix.container, 'alpine') }} From 0c24c4fb2a4db02e2137fe41125f1e8de66ae9fb Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Mon, 10 Nov 2025 15:32:14 +0100 Subject: [PATCH 026/130] fix weird linker asymmetry --- CMakeLists.txt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index f31f1f6a5..0a6fd0109 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -824,8 +824,7 @@ if(SENTRY_BUILD_EXAMPLES) if (LINUX AND NOT SENTRY_BUILD_SHARED_LIBS) target_link_options(sentry_example PRIVATE -no-pie - -no-lto - -Wl,--disable-lto + -fno-lto ) target_compile_options(sentry_example PRIVATE -fno-pie From 70dd979e8c09897065e4e716a8e81980a4b739c6 Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Mon, 10 Nov 2025 15:51:03 +0100 Subject: [PATCH 027/130] provide empty path-suffix so that CMake can find libunwind on platforms with architecture prefixes (32-bit Linux) --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 0a6fd0109..4da7f7c8c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -615,7 +615,7 @@ endif() if(SENTRY_WITH_LIBUNWIND) if(LINUX) option(SENTRY_LIBUNWIND_SHARED "Link to shared libunwind" ${SENTRY_BUILD_SHARED_LIBS}) - find_path(LIBUNWIND_INCLUDE_DIR libunwind.h PATH_SUFFIXES libunwind REQUIRED) + find_path(LIBUNWIND_INCLUDE_DIR libunwind.h PATH_SUFFIXES "" libunwind REQUIRED) if(SENTRY_LIBUNWIND_SHARED) find_library(LIBUNWIND_LIBRARIES unwind REQUIRED) else() From 4449859801c31371eb0a04448eaf60831d8117b2 Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Mon, 10 Nov 2025 20:28:42 +0100 Subject: [PATCH 028/130] remove can_lock mechanism from the handler block API --- src/sentry_sync.c | 29 ----------------------------- 1 file changed, 29 deletions(-) diff --git a/src/sentry_sync.c b/src/sentry_sync.c index 5e7bfe118..a46acc2ff 100644 --- a/src/sentry_sync.c +++ b/src/sentry_sync.c @@ -510,20 +510,6 @@ sentry__bgworker_get_thread_name(sentry_bgworker_t *bgw) static sig_atomic_t g_in_signal_handler __attribute__((aligned(64))) = 0; static sentry_threadid_t g_signal_handling_thread = { 0 }; -# ifdef SENTRY_BACKEND_INPROC -static sig_atomic_t g_signal_handler_can_lock __attribute__((aligned(64))) = 0; - -static void -fatal_signal_lock_violation(void) -{ - static const char msg[] - = "[sentry] FATAL attempted to acquire mutex inside signal handler\n"; - const ssize_t rv = write(STDERR_FILENO, msg, sizeof(msg) - 1); - (void)rv; - _exit(1); -} -# endif - bool sentry__block_for_signal_handler(void) { @@ -538,12 +524,6 @@ sentry__block_for_signal_handler(void) = __atomic_load_n(&g_signal_handling_thread, __ATOMIC_ACQUIRE); if (sentry__threadid_equal(current, handling)) { -# ifdef SENTRY_BACKEND_INPROC - if (!__atomic_load_n( - &g_signal_handler_can_lock, __ATOMIC_ACQUIRE)) { - fatal_signal_lock_violation(); - } -# endif return true; } @@ -579,9 +559,6 @@ sentry__enter_signal_handler(void) // update the thread, now that no one else can and leave __atomic_store_n( &g_signal_handling_thread, current, __ATOMIC_RELEASE); -# ifdef SENTRY_BACKEND_INPROC - __atomic_store_n(&g_signal_handler_can_lock, 0, __ATOMIC_RELEASE); -# endif return; } @@ -598,9 +575,6 @@ sentry__switch_handler_thread(void) sentry_threadid_t current = sentry__current_thread(); __atomic_store_n(&g_signal_handling_thread, current, __ATOMIC_RELEASE); -# ifdef SENTRY_BACKEND_INPROC - __atomic_store_n(&g_signal_handler_can_lock, 1, __ATOMIC_RELEASE); -# endif return true; } @@ -611,9 +585,6 @@ sentry__leave_signal_handler(void) // clean up the thread-id and drop the reentrancy guard __atomic_store_n( &g_signal_handling_thread, (sentry_threadid_t) { 0 }, __ATOMIC_RELAXED); -# ifdef SENTRY_BACKEND_INPROC - __atomic_store_n(&g_signal_handler_can_lock, 0, __ATOMIC_RELAXED); -# endif __sync_lock_release(&g_in_signal_handler); } #endif From 4ce794095e6d9e9776d6c17db5b0d8f9f05fbe78 Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Mon, 10 Nov 2025 23:11:40 +0100 Subject: [PATCH 029/130] use PRIx64 without ull cast in sentry__value_new_addr() --- src/sentry_value.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/sentry_value.c b/src/sentry_value.c index af4c8709b..7e4da8d94 100644 --- a/src/sentry_value.c +++ b/src/sentry_value.c @@ -1191,8 +1191,7 @@ sentry_value_t sentry__value_new_addr(uint64_t addr) { char buf[32]; - size_t written = (size_t)snprintf( - buf, sizeof(buf), "0x%llx", (unsigned long long)addr); + size_t written = (size_t)snprintf(buf, sizeof(buf), "0x%" PRIx64, addr); if (written >= sizeof(buf)) { return sentry_value_new_null(); } From 895492836e4618d8b0c223686898afde417d34ba Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Mon, 10 Nov 2025 23:17:42 +0100 Subject: [PATCH 030/130] fix off-by-one bug when returning the trace length after walking the stack also ensure to get the first frame harmonize libunwind usage --- src/unwinder/sentry_unwinder_libunwind.c | 38 ++++++++++--- src/unwinder/sentry_unwinder_libunwind_mac.c | 57 +++++++++++++++----- 2 files changed, 73 insertions(+), 22 deletions(-) diff --git a/src/unwinder/sentry_unwinder_libunwind.c b/src/unwinder/sentry_unwinder_libunwind.c index 6ddf3c7df..bd6ae8dd4 100644 --- a/src/unwinder/sentry_unwinder_libunwind.c +++ b/src/unwinder/sentry_unwinder_libunwind.c @@ -43,14 +43,36 @@ sentry__unwind_stack_libunwind( } } - size_t frame_idx = 0; - while (unw_step(&cursor) > 0 && frame_idx < max_frames - 1) { + size_t n = 0; + // get the first frame + if (n < max_frames) { unw_word_t ip = 0; - unw_get_reg(&cursor, UNW_REG_IP, &ip); - ptrs[frame_idx] = (void *)ip; - unw_word_t sp = 0; - unw_get_reg(&cursor, UNW_REG_SP, &sp); - frame_idx++; + if (unw_get_reg(&cursor, UNW_REG_IP, &ip) == 0) { + return n; + } + ptrs[n++] = (void *)ip; + } + // walk the callers + unw_word_t prev_ip = (unw_word_t)ptrs[0]; + unw_word_t prev_sp = 0; + (void)unw_get_reg(&cursor, UNW_REG_SP, &prev_sp); + + while (n < max_frames && unw_step(&cursor) > 0) { + unw_word_t ip = 0, sp = 0; + // stop the walk if we fail to read IP + if (unw_get_reg(&cursor, UNW_REG_IP, &ip) < 0) { + break; + } + // SP is optional for progress + (void)unw_get_reg(&cursor, UNW_REG_SP, &sp); + + // stop the walk if there is _no_ progress + if (ip == prev_ip && sp == prev_sp) { + break; + } + prev_ip = ip; + prev_sp = sp; + ptrs[n++] = (void *)ip; } - return frame_idx + 1; + return n; } diff --git a/src/unwinder/sentry_unwinder_libunwind_mac.c b/src/unwinder/sentry_unwinder_libunwind_mac.c index b228300a0..f9bc2a0d2 100644 --- a/src/unwinder/sentry_unwinder_libunwind_mac.c +++ b/src/unwinder/sentry_unwinder_libunwind_mac.c @@ -12,7 +12,7 @@ valid_ptr(uintptr_t p) size_t fp_walk_from_uctx(const sentry_ucontext_t *uctx, void **ptrs, size_t max_frames) { - size_t frame_idx = 0; + size_t n = 0; struct __darwin_mcontext64 *mctx = uctx->user_context->uc_mcontext; #if defined(__arm64__) uintptr_t pc = (uintptr_t)mctx->__ss.__pc; @@ -21,12 +21,12 @@ fp_walk_from_uctx(const sentry_ucontext_t *uctx, void **ptrs, size_t max_frames) // top frame: adjust pc−1 so it symbolizes inside the function if (pc) { - ptrs[frame_idx++] = (void *)(pc - 1); + ptrs[n++] = (void *)(pc - 1); } // next frame is from saved LR at current FP record if (lr) { - ptrs[frame_idx++] = (void *)(lr - 1); + ptrs[n++] = (void *)(lr - 1); } for (size_t i = 0; i < max_frames; ++i) { @@ -42,7 +42,7 @@ fp_walk_from_uctx(const sentry_ucontext_t *uctx, void **ptrs, size_t max_frames) break; } - ptrs[frame_idx++] = (void *)(ret_addr - 1); + ptrs[n++] = (void *)(ret_addr - 1); if (next_fp <= fp) { break; // prevent loops } @@ -54,7 +54,7 @@ fp_walk_from_uctx(const sentry_ucontext_t *uctx, void **ptrs, size_t max_frames) // top frame: adjust ip−1 so it symbolizes inside the function if (ip) { - ptrs[frame_idx++] = (void *)(ip - 1); + ptrs[n++] = (void *)(ip - 1); } for (size_t i = 0; i < max_frames; ++i) { @@ -68,7 +68,7 @@ fp_walk_from_uctx(const sentry_ucontext_t *uctx, void **ptrs, size_t max_frames) if (!valid_ptr(next_bp) || !ret_addr) { break; } - ptrs[frame_idx++] = (void *)(ret_addr - 1); + ptrs[n++] = (void *)(ret_addr - 1); if (next_bp <= bp) { break; } @@ -77,7 +77,7 @@ fp_walk_from_uctx(const sentry_ucontext_t *uctx, void **ptrs, size_t max_frames) #else # error "Unsupported CPU architecture for macOS stackwalker" #endif - return frame_idx + 1; + return n; } size_t @@ -107,20 +107,49 @@ sentry__unwind_stack_libunwind_mac( SENTRY_WARN("Failed to initialize libunwind with local context"); return 0; } - size_t frame_idx = 0; - while (unw_step(&cursor) > 0 && frame_idx < max_frames - 1) { + size_t n = 0; + // get the first frame + if (n < max_frames) { unw_word_t ip = 0; - SENTRY_INFOF("ip: %p", ip); - unw_get_reg(&cursor, UNW_REG_IP, &ip); + if (unw_get_reg(&cursor, UNW_REG_IP, &ip) >= 0) { +#if defined(__arm64__) + // Strip pointer authentication, for some reason ptrauth_strip() not + // working + // https://developer.apple.com/documentation/security/preparing_your_app_to_work_with_pointer_authentication + ip &= 0x7fffffffffffull; +#endif + ptrs[n++] = (void *)ip; + } else { + return 0; + } + } + // walk the callers + unw_word_t prev_ip = (uintptr_t)ptrs[0]; + unw_word_t prev_sp = 0; + (void)unw_get_reg(&cursor, UNW_REG_SP, &prev_sp); + while (n < max_frames && unw_step(&cursor) > 0) { + unw_word_t ip = 0, sp = 0; + // stop the walk if we fail to read IP + if (unw_get_reg(&cursor, UNW_REG_IP, &ip) < 0) { + break; + } + // SP is optional for progress + (void)unw_get_reg(&cursor, UNW_REG_SP, &sp); + + // stop the walk if there is _no_ progress + if (ip == prev_ip && sp == prev_sp) { + break; + } #if defined(__arm64__) // Strip pointer authentication, for some reason ptrauth_strip() not // working // https://developer.apple.com/documentation/security/preparing_your_app_to_work_with_pointer_authentication ip &= 0x7fffffffffffull; #endif - ptrs[frame_idx] = (void *)ip; - frame_idx++; + prev_ip = ip; + prev_sp = sp; + ptrs[n++] = (void *)ip; } - return frame_idx + 1; + return n; } From e633f2640da97e8739b07a046e6d5fc998014180 Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Tue, 11 Nov 2025 09:35:08 +0100 Subject: [PATCH 031/130] clean up cmake configure stage in pytest --- tests/cmake.py | 106 +++++++++++++++++++++++++++---------------------- 1 file changed, 59 insertions(+), 47 deletions(-) diff --git a/tests/cmake.py b/tests/cmake.py index d49d0243f..082141de8 100644 --- a/tests/cmake.py +++ b/tests/cmake.py @@ -154,9 +154,7 @@ def cmake(cwd, targets, options=None, cflags=None): config_cmd = cmake.copy() if os.environ.get("VS_GENERATOR_TOOLSET") == "ClangCL": - config_cmd.append("-G Visual Studio 17 2022") - config_cmd.append("-A x64") - config_cmd.append("-T ClangCL") + configure_clang_cl(config_cmd) for key, value in options.items(): config_cmd.append("-D{}={}".format(key, value)) @@ -167,17 +165,7 @@ def cmake(cwd, targets, options=None, cflags=None): if "asan" in os.environ.get("RUN_ANALYZER", ""): config_cmd.append("-DWITH_ASAN_OPTION=ON") if "tsan" in os.environ.get("RUN_ANALYZER", ""): - module_dir = Path(__file__).resolve().parent - tsan_options = { - "verbosity": 2, - "detect_deadlocks": 1, - "second_deadlock_stack": 1, - "suppressions": module_dir / "tsan.supp", - } - os.environ["TSAN_OPTIONS"] = ":".join( - f"{key}={value}" for key, value in tsan_options.items() - ) - config_cmd.append("-DWITH_TSAN_OPTION=ON") + configure_tsan(config_cmd) # we have to set `-Werror` for this cmake invocation only, otherwise # completely unrelated things will break @@ -190,39 +178,7 @@ def cmake(cwd, targets, options=None, cflags=None): if "gcc" in os.environ.get("RUN_ANALYZER", ""): cflags.append("-fanalyzer") if "llvm-cov" in os.environ.get("RUN_ANALYZER", ""): - if False and os.environ.get("VS_GENERATOR_TOOLSET") == "ClangCL": - # for clang-cl in CI we have to use `--coverage` rather than `fprofile-instr-generate` and provide an - # architecture-specific profiling library for it work: - # TODO: This currently doesn't work due to https://gitlab.kitware.com/cmake/cmake/-/issues/24025 - # The issue describes a behavior where generated object-name is specified via `/fo` using a target - # directory, rather than a file-name (this is CMake behavior). While the `clang-cl` suggest that this - # is supported the flag produces `.gcda` and `.gcno` files, which have no relation with the object- - # file and which leads to failure to accumulate coverage data. - # This would have to be fixed in clang-cl: https://github.com/llvm/llvm-project/issues/87304 - # Let's leave this in here for posterity, it would be great to get coverage analysis on Windows. - flags = "--coverage" - profile_lib = "clang_rt.profile-x86_64.lib" - config_cmd.append(f"-DCMAKE_EXE_LINKER_FLAGS='{profile_lib}'") - config_cmd.append(f"-DCMAKE_SHARED_LINKER_FLAGS='{profile_lib}'") - config_cmd.append(f"-DCMAKE_MODULE_LINKER_FLAGS='{profile_lib}'") - else: - flags = "-fprofile-instr-generate -fcoverage-mapping" - config_cmd.append("-DCMAKE_C_FLAGS='{}'".format(flags)) - - # Since we overwrite `CXXFLAGS` below, we must add the experimental library here for the GHA runner that builds - # sentry-native with LLVM clang for macOS (to run ASAN on macOS) rather than the version coming with XCode. - # TODO: remove this if the GHA runner image for macOS ever updates beyond llvm15. - if ( - sys.platform == "darwin" - and os.environ.get("CC", "") == "clang" - and shutil.which("clang") == "/usr/local/opt/llvm@15/bin/clang" - ): - flags = ( - flags - + " -L/usr/local/opt/llvm@15/lib/c++ -fexperimental-library -Wno-unused-command-line-argument" - ) - - config_cmd.append("-DCMAKE_CXX_FLAGS='{}'".format(flags)) + configure_llvm_cov(config_cmd) if "CMAKE_DEFINES" in os.environ: config_cmd.extend(os.environ.get("CMAKE_DEFINES").split()) env = dict(os.environ) @@ -317,3 +273,59 @@ def cmake(cwd, targets, options=None, cflags=None): cwd=cwd, check=True, ) + + +def configure_clang_cl(config_cmd: list[str]): + config_cmd.append("-G Visual Studio 17 2022") + config_cmd.append("-A x64") + config_cmd.append("-T ClangCL") + + +def configure_tsan(config_cmd: list[str]): + module_dir = Path(__file__).resolve().parent + tsan_options = { + "verbosity": 2, + "detect_deadlocks": 1, + "second_deadlock_stack": 1, + "suppressions": module_dir / "tsan.supp", + } + os.environ["TSAN_OPTIONS"] = ":".join( + f"{key}={value}" for key, value in tsan_options.items() + ) + config_cmd.append("-DWITH_TSAN_OPTION=ON") + + +def configure_llvm_cov(config_cmd: list[str]): + if False and os.environ.get("VS_GENERATOR_TOOLSET") == "ClangCL": + # for clang-cl in CI we have to use `--coverage` rather than `fprofile-instr-generate` and provide an + # architecture-specific profiling library for it work: + # TODO: This currently doesn't work due to https://gitlab.kitware.com/cmake/cmake/-/issues/24025 + # The issue describes a behavior where generated object-name is specified via `/fo` using a target + # directory, rather than a file-name (this is CMake behavior). While the `clang-cl` suggest that this + # is supported the flag produces `.gcda` and `.gcno` files, which have no relation with the object- + # file and which leads to failure to accumulate coverage data. + # This would have to be fixed in clang-cl: https://github.com/llvm/llvm-project/issues/87304 + # Let's leave this in here for posterity, it would be great to get coverage analysis on Windows. + flags = "--coverage" + profile_lib = "clang_rt.profile-x86_64.lib" + config_cmd.append(f"-DCMAKE_EXE_LINKER_FLAGS='{profile_lib}'") + config_cmd.append(f"-DCMAKE_SHARED_LINKER_FLAGS='{profile_lib}'") + config_cmd.append(f"-DCMAKE_MODULE_LINKER_FLAGS='{profile_lib}'") + else: + flags = "-fprofile-instr-generate -fcoverage-mapping" + config_cmd.append("-DCMAKE_C_FLAGS='{}'".format(flags)) + + # Since we overwrite `CXXFLAGS` below, we must add the experimental library here for the GHA runner that builds + # sentry-native with LLVM clang for macOS (to run ASAN on macOS) rather than the version coming with XCode. + # TODO: remove this if the GHA runner image for macOS ever updates beyond llvm15. + if ( + sys.platform == "darwin" + and os.environ.get("CC", "") == "clang" + and shutil.which("clang") == "/usr/local/opt/llvm@15/bin/clang" + ): + flags = ( + flags + + " -L/usr/local/opt/llvm@15/lib/c++ -fexperimental-library -Wno-unused-command-line-argument" + ) + + config_cmd.append("-DCMAKE_CXX_FLAGS='{}'".format(flags)) From f7f4b4ebe124925a8709671f303688559591b1a2 Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Tue, 11 Nov 2025 09:36:41 +0100 Subject: [PATCH 032/130] skip tests if zlib wasn't found during cmake configure stage --- tests/cmake.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/tests/cmake.py b/tests/cmake.py index 082141de8..62484910a 100644 --- a/tests/cmake.py +++ b/tests/cmake.py @@ -188,9 +188,18 @@ def cmake(cwd, targets, options=None, cflags=None): print("\n{} > {}".format(cwd, " ".join(config_cmd)), flush=True) try: - subprocess.run(config_cmd, cwd=cwd, env=env, check=True) - except subprocess.CalledProcessError: - raise pytest.fail.Exception("cmake configure failed") from None + result = subprocess.run( + config_cmd, cwd=cwd, env=env, check=True, capture_output=True, text=True + ) + sys.stdout.write(result.stdout) + sys.stderr.write(result.stderr or "") + except subprocess.CalledProcessError as e: + if "Could NOT find ZLIB" in e.stderr: + pytest.skip("ZLIB not found") + else: + sys.stdout.write(e.stdout) + sys.stderr.write(e.stderr or "") + raise pytest.fail.Exception("cmake configure failed") from None # CodeChecker invocations and options are documented here: # https://github.com/Ericsson/codechecker/blob/master/docs/analyzer/user_guide.md From 66618cdd4522851550a7980e92ff108b26a79e36 Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Tue, 11 Nov 2025 09:46:28 +0100 Subject: [PATCH 033/130] fix stupid comparison error that led to all failing Linux tests --- src/unwinder/sentry_unwinder_libunwind.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/unwinder/sentry_unwinder_libunwind.c b/src/unwinder/sentry_unwinder_libunwind.c index bd6ae8dd4..a47d4ca9b 100644 --- a/src/unwinder/sentry_unwinder_libunwind.c +++ b/src/unwinder/sentry_unwinder_libunwind.c @@ -47,7 +47,7 @@ sentry__unwind_stack_libunwind( // get the first frame if (n < max_frames) { unw_word_t ip = 0; - if (unw_get_reg(&cursor, UNW_REG_IP, &ip) == 0) { + if (unw_get_reg(&cursor, UNW_REG_IP, &ip) < 0) { return n; } ptrs[n++] = (void *)ip; From 3432dea63fd5513e3e7e1e6b9ca48e5707c47ec7 Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Tue, 11 Nov 2025 10:01:54 +0100 Subject: [PATCH 034/130] exclude `logger_level` from transport unit tests. --- tests/test_unit.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/test_unit.py b/tests/test_unit.py index 7ce28d04f..c38fffb91 100644 --- a/tests/test_unit.py +++ b/tests/test_unit.py @@ -17,7 +17,11 @@ def test_unit(cmake, unittest): @pytest.mark.skipif(not has_http, reason="tests need http transport") def test_unit_transport(cmake, unittest): - if unittest in ["custom_logger", "logger_enable_disable_functionality"]: + if unittest in [ + "custom_logger", + "logger_enable_disable_functionality", + "logger_level", + ]: pytest.skip("excluded from transport test-suite") cwd = cmake( From c0aa3995dce3408a87a3f65bd392bec9f3c58d78 Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Tue, 11 Nov 2025 10:56:21 +0100 Subject: [PATCH 035/130] let find_path search the library architecture --- CMakeLists.txt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 4da7f7c8c..bd89f20eb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -615,7 +615,11 @@ endif() if(SENTRY_WITH_LIBUNWIND) if(LINUX) option(SENTRY_LIBUNWIND_SHARED "Link to shared libunwind" ${SENTRY_BUILD_SHARED_LIBS}) - find_path(LIBUNWIND_INCLUDE_DIR libunwind.h PATH_SUFFIXES "" libunwind REQUIRED) + find_path(LIBUNWIND_INCLUDE_DIR libunwind.h + PATH_SUFFIXES + ${CMAKE_LIBRARY_ARCHITECTURE} + libunwind + REQUIRED) if(SENTRY_LIBUNWIND_SHARED) find_library(LIBUNWIND_LIBRARIES unwind REQUIRED) else() From 96afd3d551883dba9fd901a0efefe48b7044580c Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Tue, 11 Nov 2025 12:02:23 +0100 Subject: [PATCH 036/130] support i386 multilib on Ubuntu/Debian --- CMakeLists.txt | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index bd89f20eb..0e2a798e7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -614,17 +614,21 @@ endif() if(SENTRY_WITH_LIBUNWIND) if(LINUX) + set(_multiarch_suffixes + i386-linux-gnu # Ubuntu/Debian i386 multilib + ${CMAKE_LIBRARY_ARCHITECTURE} # e.g. x86_64-linux-gnu, aarch64-linux-gnu + ) option(SENTRY_LIBUNWIND_SHARED "Link to shared libunwind" ${SENTRY_BUILD_SHARED_LIBS}) find_path(LIBUNWIND_INCLUDE_DIR libunwind.h PATH_SUFFIXES - ${CMAKE_LIBRARY_ARCHITECTURE} - libunwind + ${_multiarch_suffixes} + libunwind # some distros put the libunwind headers into a subdirectory REQUIRED) if(SENTRY_LIBUNWIND_SHARED) - find_library(LIBUNWIND_LIBRARIES unwind REQUIRED) + find_library(LIBUNWIND_LIBRARIES unwind PATH_SUFFIXES ${_multiarch_suffixes} REQUIRED) else() - find_library(LIBUNWIND_LIBRARIES libunwind.a REQUIRED) - find_library(LZMA_LIBRARY lzma) + find_library(LIBUNWIND_LIBRARIES libunwind.a PATH_SUFFIXES ${_multiarch_suffixes} REQUIRED) + find_library(LZMA_LIBRARY lzma PATH_SUFFIXES ${_multiarch_suffixes}) if(LZMA_LIBRARY) list(APPEND LIBUNWIND_LIBRARIES ${LZMA_LIBRARY}) endif() From bea2b65524281a749311218521bcc08011b5b2cb Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Tue, 11 Nov 2025 14:03:53 +0100 Subject: [PATCH 037/130] add 32-bit lzma package to Linux 32-bit build --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 18de82c8d..660ebbabc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -224,7 +224,7 @@ jobs: run: | sudo dpkg --add-architecture i386 sudo apt update - sudo apt install cmake gcc-10-multilib g++-10-multilib zlib1g-dev:i386 libssl-dev:i386 libcurl4-openssl-dev:i386 libunwind-dev:i386 + sudo apt install cmake gcc-10-multilib g++-10-multilib zlib1g-dev:i386 libssl-dev:i386 libcurl4-openssl-dev:i386 libunwind-dev:i386 lzma-dev:i386 - name: Installing Alpine Linux Dependencies if: ${{ contains(matrix.container, 'alpine') }} From dc117e51291e71a621ef7a542d52db04d789d4df Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Tue, 11 Nov 2025 14:49:07 +0100 Subject: [PATCH 038/130] typo --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 660ebbabc..c12b4e80f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -224,7 +224,7 @@ jobs: run: | sudo dpkg --add-architecture i386 sudo apt update - sudo apt install cmake gcc-10-multilib g++-10-multilib zlib1g-dev:i386 libssl-dev:i386 libcurl4-openssl-dev:i386 libunwind-dev:i386 lzma-dev:i386 + sudo apt install cmake gcc-10-multilib g++-10-multilib zlib1g-dev:i386 libssl-dev:i386 libcurl4-openssl-dev:i386 libunwind-dev:i386 liblzma-dev:i386 - name: Installing Alpine Linux Dependencies if: ${{ contains(matrix.container, 'alpine') }} From 14af96d575c5eb63c0bc4387b5b9e92df2e30bb5 Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Tue, 11 Nov 2025 15:44:51 +0100 Subject: [PATCH 039/130] make even more elaborate multilib triplet and suffice handling based on FORCE32 --- CMakeLists.txt | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 0e2a798e7..0abb29572 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -614,21 +614,31 @@ endif() if(SENTRY_WITH_LIBUNWIND) if(LINUX) - set(_multiarch_suffixes - i386-linux-gnu # Ubuntu/Debian i386 multilib - ${CMAKE_LIBRARY_ARCHITECTURE} # e.g. x86_64-linux-gnu, aarch64-linux-gnu - ) option(SENTRY_LIBUNWIND_SHARED "Link to shared libunwind" ${SENTRY_BUILD_SHARED_LIBS}) + + # ensure suffix and search paths for include and library search (specifically for multilib) + if(SENTRY_BUILD_FORCE32) + set(_unwind_triplet "i386-linux-gnu") + set(_i386_libpaths /usr/lib/i386-linux-gnu /lib/i386-linux-gnu /usr/lib32 /lib32) + else() + set(_unwind_triplet "${CMAKE_LIBRARY_ARCHITECTURE}") + endif() + find_path(LIBUNWIND_INCLUDE_DIR libunwind.h PATH_SUFFIXES - ${_multiarch_suffixes} + ${_unwind_triplet} libunwind # some distros put the libunwind headers into a subdirectory REQUIRED) if(SENTRY_LIBUNWIND_SHARED) - find_library(LIBUNWIND_LIBRARIES unwind PATH_SUFFIXES ${_multiarch_suffixes} REQUIRED) + find_library(LIBUNWIND_LIBRARIES unwind PATH_SUFFIXES ${_unwind_triplet} REQUIRED) else() - find_library(LIBUNWIND_LIBRARIES libunwind.a PATH_SUFFIXES ${_multiarch_suffixes} REQUIRED) - find_library(LZMA_LIBRARY lzma PATH_SUFFIXES ${_multiarch_suffixes}) + find_library(LIBUNWIND_LIBRARIES libunwind.a PATH_SUFFIXES ${_unwind_triplet} REQUIRED) + if(SENTRY_BUILD_FORCE32) + find_library(LZMA_LIBRARY lzma PATHS ${_i386_libpaths} NO_DEFAULT_PATH) + else() + find_library(LZMA_LIBRARY lzma PATH_SUFFIXES ${_unwind_triplet}) + endif() + if(LZMA_LIBRARY) list(APPEND LIBUNWIND_LIBRARIES ${LZMA_LIBRARY}) endif() From 2e921a003b2f0bbba40b5caa5c7fc50217c8a9cc Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Tue, 11 Nov 2025 15:47:02 +0100 Subject: [PATCH 040/130] be even more careful wrt to max_frames in the fp walker and check on every access for overrun (+ track with while instead for-looping) --- src/unwinder/sentry_unwinder_libunwind_mac.c | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/unwinder/sentry_unwinder_libunwind_mac.c b/src/unwinder/sentry_unwinder_libunwind_mac.c index f9bc2a0d2..2843b0727 100644 --- a/src/unwinder/sentry_unwinder_libunwind_mac.c +++ b/src/unwinder/sentry_unwinder_libunwind_mac.c @@ -20,16 +20,16 @@ fp_walk_from_uctx(const sentry_ucontext_t *uctx, void **ptrs, size_t max_frames) uintptr_t lr = (uintptr_t)mctx->__ss.__lr; // top frame: adjust pc−1 so it symbolizes inside the function - if (pc) { + if (pc && n < max_frames) { ptrs[n++] = (void *)(pc - 1); } // next frame is from saved LR at current FP record - if (lr) { + if (lr && n < max_frames) { ptrs[n++] = (void *)(lr - 1); } - for (size_t i = 0; i < max_frames; ++i) { + while (n < max_frames) { if (!valid_ptr(fp)) { break; } @@ -53,11 +53,11 @@ fp_walk_from_uctx(const sentry_ucontext_t *uctx, void **ptrs, size_t max_frames) uintptr_t bp = (uintptr_t)mctx->__ss.__rbp; // top frame: adjust ip−1 so it symbolizes inside the function - if (ip) { + if (ip && n < max_frames) { ptrs[n++] = (void *)(ip - 1); } - for (size_t i = 0; i < max_frames; ++i) { + while (n < max_frames) { if (!valid_ptr(bp)) { break; } From e3643c7c8998495e7675fbfc5f5b750505049094 Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Tue, 11 Nov 2025 16:17:38 +0100 Subject: [PATCH 041/130] eliminate the non-atomic access to g_handler_thread_ready from has_handler_thread_crashed --- src/backends/sentry_backend_inproc.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/backends/sentry_backend_inproc.c b/src/backends/sentry_backend_inproc.c index 849956177..5bb82eed0 100644 --- a/src/backends/sentry_backend_inproc.c +++ b/src/backends/sentry_backend_inproc.c @@ -856,10 +856,10 @@ stop_handler_thread(void) } static bool -did_handler_thread_crash(void) +has_handler_thread_crashed(void) { const sentry_threadid_t current_thread = sentry__current_thread(); - if (g_handler_thread_ready + if (sentry__atomic_fetch(&g_handler_thread_ready) && sentry__threadid_equal(current_thread, g_handler_thread)) { #ifdef SENTRY_PLATFORM_UNIX static const char msg[] = "[sentry] FATAL crash in handler thread, " @@ -1038,7 +1038,7 @@ process_ucontext(const sentry_ucontext_t *uctx) sentry__page_allocator_enable(); #endif - if (!did_handler_thread_crash()) { + if (!has_handler_thread_crashed()) { // invoke the handler thread for signal unsafe actions dispatch_ucontext(uctx, sig_slot); } From 3ffe195319331c8012f72cfdefc2b8c540c73f9b Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Tue, 11 Nov 2025 17:05:56 +0100 Subject: [PATCH 042/130] bring spinlock and pageallocator into the atomic fold --- src/sentry_unix_pageallocator.c | 22 ++++++++++++++-------- src/sentry_unix_spinlock.h | 12 +++++++++--- 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/src/sentry_unix_pageallocator.c b/src/sentry_unix_pageallocator.c index 0226c9b09..7c0e4ec40 100644 --- a/src/sentry_unix_pageallocator.c +++ b/src/sentry_unix_pageallocator.c @@ -33,20 +33,26 @@ static sentry_spinlock_t g_lock = SENTRY__SPINLOCK_INIT; bool sentry__page_allocator_enabled(void) { - return !!g_alloc; + return __atomic_load_n(&g_alloc, __ATOMIC_ACQUIRE) != NULL; } void sentry__page_allocator_enable(void) { + if (sentry__page_allocator_enabled()) { + return; + } sentry__spinlock_lock(&g_lock); - if (!g_alloc) { - g_alloc = &g_page_allocator_backing; - g_alloc->page_size = getpagesize(); - g_alloc->last_page = NULL; - g_alloc->current_page = NULL; - g_alloc->page_offset = 0; - g_alloc->pages_allocated = 0; + if (__atomic_load_n(&g_alloc, __ATOMIC_RELAXED) == NULL) { + struct page_allocator_s *p = &g_page_allocator_backing; + + p->page_size = getpagesize(); + p->last_page = NULL; + p->current_page = NULL; + p->page_offset = 0; + p->pages_allocated = 0; + + __atomic_store_n(&g_alloc, p, __ATOMIC_RELEASE); } sentry__spinlock_unlock(&g_lock); } diff --git a/src/sentry_unix_spinlock.h b/src/sentry_unix_spinlock.h index 575f86d67..b0b9f2b46 100644 --- a/src/sentry_unix_spinlock.h +++ b/src/sentry_unix_spinlock.h @@ -14,9 +14,15 @@ typedef volatile sig_atomic_t sentry_spinlock_t; #define SENTRY__SPINLOCK_INIT 0 #define sentry__spinlock_lock(spinlock_ref) \ - while (!__sync_bool_compare_and_swap(spinlock_ref, 0, 1)) { \ - sentry__cpu_relax(); \ + for (;;) { \ + while (__atomic_load_n(spinlock_ref, __ATOMIC_RELAXED)) { \ + sentry__cpu_relax(); \ + } \ + if (__atomic_exchange_n(spinlock_ref, 1, __ATOMIC_ACQUIRE) == 0) { \ + break; \ + } \ } -#define sentry__spinlock_unlock(spinlock_ref) (*spinlock_ref = 0) +#define sentry__spinlock_unlock(spinlock_ref) \ + (__atomic_store_n(spinlock_ref, 0, __ATOMIC_RELEASE)) #endif From 7eff2d1288b1befeb08b26347d77a37a28c95ff1 Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Tue, 11 Nov 2025 17:19:15 +0100 Subject: [PATCH 043/130] fix: add GNU-stack note to crashpad_info_note.S --- external/crashpad | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/external/crashpad b/external/crashpad index d8990d2f6..2c2ae3c8a 160000 --- a/external/crashpad +++ b/external/crashpad @@ -1 +1 @@ -Subproject commit d8990d2f686b8827a21532748c6c42add21c21ea +Subproject commit 2c2ae3c8acc4fe1bf193d5b8979dd5fab232ee7c From 01061fdcd67a11511727305d3806351219767e0e Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Tue, 11 Nov 2025 17:29:56 +0100 Subject: [PATCH 044/130] update crashpad --- external/crashpad | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/external/crashpad b/external/crashpad index 2c2ae3c8a..a64405e09 160000 --- a/external/crashpad +++ b/external/crashpad @@ -1 +1 @@ -Subproject commit 2c2ae3c8acc4fe1bf193d5b8979dd5fab232ee7c +Subproject commit a64405e09ca9bb98ab169e594e1c17771414ec1b From fd584038b50321e3d16ab99411dccb105d146f54 Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Tue, 11 Nov 2025 17:30:46 +0100 Subject: [PATCH 045/130] remove deprecated macOS 13 and replace it with macOS 26 --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c12b4e80f..0126b5693 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -122,8 +122,8 @@ jobs: os: macos-14 ERROR_ON_WARNINGS: 1 SYSTEM_VERSION_COMPAT: 0 - - name: macOS 13 (xcode llvm) - os: macos-13 + - name: macOS 26 (xcode llvm) + os: macos-26 ERROR_ON_WARNINGS: 1 SYSTEM_VERSION_COMPAT: 0 - name: macOS 14 (xcode llvm + universal) From e8a393e1432b5bf1d67e9dd43bdaaa17784ae847 Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Wed, 12 Nov 2025 11:33:42 +0100 Subject: [PATCH 046/130] update crashpad after merging to getsentry --- external/crashpad | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/external/crashpad b/external/crashpad index a64405e09..19d9d8a06 160000 --- a/external/crashpad +++ b/external/crashpad @@ -1 +1 @@ -Subproject commit a64405e09ca9bb98ab169e594e1c17771414ec1b +Subproject commit 19d9d8a06f6bbf3837db2464675e4545fff411f6 From b197e1ea64dedcb3b7284b151d40780e8547e10e Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Wed, 12 Nov 2025 11:35:04 +0100 Subject: [PATCH 047/130] simplify signal handler blocker by leaving and entering around the dispatch to the handler thread --- src/backends/sentry_backend_inproc.c | 5 ++- src/sentry_sync.c | 55 ++++------------------------ src/sentry_sync.h | 1 - 3 files changed, 12 insertions(+), 49 deletions(-) diff --git a/src/backends/sentry_backend_inproc.c b/src/backends/sentry_backend_inproc.c index 5bb82eed0..573d6c88c 100644 --- a/src/backends/sentry_backend_inproc.c +++ b/src/backends/sentry_backend_inproc.c @@ -910,6 +910,9 @@ dispatch_ucontext( } else { g_handler_state.uctx.user_context = NULL; } + + // we leave the handler + sentry__leave_signal_handler(); #endif sentry__atomic_store(&g_handler_work_done, 0); @@ -936,7 +939,7 @@ dispatch_ucontext( } #ifdef SENTRY_PLATFORM_UNIX - sentry__switch_handler_thread(); + sentry__enter_signal_handler(); #endif } diff --git a/src/sentry_sync.c b/src/sentry_sync.c index a46acc2ff..5c71e6b9c 100644 --- a/src/sentry_sync.c +++ b/src/sentry_sync.c @@ -508,22 +508,13 @@ sentry__bgworker_get_thread_name(sentry_bgworker_t *bgw) # include static sig_atomic_t g_in_signal_handler __attribute__((aligned(64))) = 0; -static sentry_threadid_t g_signal_handling_thread = { 0 }; bool sentry__block_for_signal_handler(void) { for (;;) { // if there is no signal handler active, we don't need to block - if (!__atomic_load_n(&g_in_signal_handler, __ATOMIC_RELAXED)) { - return true; - } - - sentry_threadid_t current = sentry__current_thread(); - sentry_threadid_t handling - = __atomic_load_n(&g_signal_handling_thread, __ATOMIC_ACQUIRE); - - if (sentry__threadid_equal(current, handling)) { + if (!__atomic_load_n(&g_in_signal_handler, __ATOMIC_ACQUIRE)) { return true; } @@ -532,59 +523,29 @@ sentry__block_for_signal_handler(void) } } -static bool -is_handling_thread(void) -{ - sentry_threadid_t handling - = __atomic_load_n(&g_signal_handling_thread, __ATOMIC_ACQUIRE); - return sentry__threadid_equal(handling, sentry__current_thread()); -} - void sentry__enter_signal_handler(void) { for (;;) { // entering a signal handler while another runs, should block us while (__atomic_load_n(&g_in_signal_handler, __ATOMIC_RELAXED)) { - // however, if we re-enter most likely a signal was raised from - // within the signal handler and then we should proceed. - if (is_handling_thread()) { - return; - } + sentry__cpu_relax(); } - // RMW that both tests AND sets atomically so we know we won the race - if (__sync_lock_test_and_set(&g_in_signal_handler, 1) == 0) { - sentry_threadid_t current = sentry__current_thread(); - // update the thread, now that no one else can and leave - __atomic_store_n( - &g_signal_handling_thread, current, __ATOMIC_RELEASE); + // atomically try to take ownership + sig_atomic_t prev + = __atomic_exchange_n(&g_in_signal_handler, 1, __ATOMIC_ACQ_REL); + if (prev == 0) { return; } - // otherwise, spin - } -} - -bool -sentry__switch_handler_thread(void) -{ - if (!__atomic_load_n(&g_in_signal_handler, __ATOMIC_ACQUIRE)) { - return false; + // otherwise we've been raced, spin } - - sentry_threadid_t current = sentry__current_thread(); - __atomic_store_n(&g_signal_handling_thread, current, __ATOMIC_RELEASE); - - return true; } void sentry__leave_signal_handler(void) { - // clean up the thread-id and drop the reentrancy guard - __atomic_store_n( - &g_signal_handling_thread, (sentry_threadid_t) { 0 }, __ATOMIC_RELAXED); - __sync_lock_release(&g_in_signal_handler); + __atomic_store_n(&g_in_signal_handler, 0, __ATOMIC_RELEASE); } #endif diff --git a/src/sentry_sync.h b/src/sentry_sync.h index 6157e5fb8..964822e6f 100644 --- a/src/sentry_sync.h +++ b/src/sentry_sync.h @@ -239,7 +239,6 @@ sentry__threadid_equal(sentry_threadid_t a, sentry_threadid_t b) bool sentry__block_for_signal_handler(void); void sentry__enter_signal_handler(void); void sentry__leave_signal_handler(void); -bool sentry__switch_handler_thread(void); typedef pthread_t sentry_threadid_t; typedef pthread_mutex_t sentry_mutex_t; From 4ff5a6e0fc239cba114cc6a55c5eae9afd45a215 Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Wed, 12 Nov 2025 11:38:15 +0100 Subject: [PATCH 048/130] remove left-over handler switch --- src/backends/sentry_backend_inproc.c | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/backends/sentry_backend_inproc.c b/src/backends/sentry_backend_inproc.c index 573d6c88c..a5caf745a 100644 --- a/src/backends/sentry_backend_inproc.c +++ b/src/backends/sentry_backend_inproc.c @@ -750,9 +750,6 @@ handler_thread_main(void *UNUSED(data)) continue; } -#ifdef SENTRY_PLATFORM_UNIX - sentry__switch_handler_thread(); -#endif process_ucontext_deferred( &g_handler_state.uctx, g_handler_state.sig_slot); sentry__atomic_store(&g_handler_has_work, 0); From 192e0338cf69a738b93492066f2847be782183ea Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Wed, 12 Nov 2025 12:17:52 +0100 Subject: [PATCH 049/130] reintroduce handling thread into cleaned up signal blocker for the remaining backends --- src/sentry_sync.c | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/src/sentry_sync.c b/src/sentry_sync.c index 5c71e6b9c..1b91bf310 100644 --- a/src/sentry_sync.c +++ b/src/sentry_sync.c @@ -507,6 +507,7 @@ sentry__bgworker_get_thread_name(sentry_bgworker_t *bgw) # include "sentry_cpu_relax.h" # include +static sentry_threadid_t g_signal_handling_thread = { 0 }; static sig_atomic_t g_in_signal_handler __attribute__((aligned(64))) = 0; bool @@ -514,11 +515,18 @@ sentry__block_for_signal_handler(void) { for (;;) { // if there is no signal handler active, we don't need to block - if (!__atomic_load_n(&g_in_signal_handler, __ATOMIC_ACQUIRE)) { - return true; + // we can spin cheaply, but for the return we must acquire + if (!__atomic_load_n(&g_in_signal_handler, __ATOMIC_RELAXED)) { + return __atomic_load_n(&g_in_signal_handler, __ATOMIC_ACQUIRE) == 0; } - // otherwise, spin + // if we are on the signal handler thread we can also leave + if (sentry__threadid_equal(sentry__current_thread(), + __atomic_load_n(&g_signal_handling_thread, __ATOMIC_ACQUIRE))) { + return false; + } + + // otherwise, we spin sentry__cpu_relax(); } } @@ -533,9 +541,11 @@ sentry__enter_signal_handler(void) } // atomically try to take ownership - sig_atomic_t prev - = __atomic_exchange_n(&g_in_signal_handler, 1, __ATOMIC_ACQ_REL); - if (prev == 0) { + if (__atomic_exchange_n(&g_in_signal_handler, 1, __ATOMIC_ACQ_REL) + == 0) { + // once we have, publish the handling thread too + sentry_threadid_t me = sentry__current_thread(); + __atomic_store_n(&g_signal_handling_thread, me, __ATOMIC_RELEASE); return; } @@ -546,6 +556,11 @@ sentry__enter_signal_handler(void) void sentry__leave_signal_handler(void) { + // reset handling thread + __atomic_store_n( + &g_signal_handling_thread, (sentry_threadid_t) { 0 }, __ATOMIC_RELAXED); + + // reset handler flag __atomic_store_n(&g_in_signal_handler, 0, __ATOMIC_RELEASE); } #endif From 2ef97e100cde67ccc71533b26caf4d31c428ee86 Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Wed, 12 Nov 2025 14:33:24 +0100 Subject: [PATCH 050/130] add valgrind suppression for false-positive deep in libunwind --- tests/valgrind.txt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/valgrind.txt b/tests/valgrind.txt index 92b756580..955977747 100644 --- a/tests/valgrind.txt +++ b/tests/valgrind.txt @@ -37,3 +37,10 @@ fun:sentry__bgworker_start fun:test_sentry_task_queue } +{ + # writes of uninit buffers with padding or foreign-stack pointers lead to false-positive during stack unwinding + libunwind writes buffers we do not manage + Memcheck:Param + write(buf) + obj:*/libunwind.so* +} \ No newline at end of file From d72fb8729fcbc9014ec379c037cfae2a0f1822bd Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Wed, 12 Nov 2025 14:58:56 +0100 Subject: [PATCH 051/130] track failing ARM64/Linux clang-tsan build across available toolchain versions --- .github/workflows/ci.yml | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0126b5693..cb3e40b6b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -89,12 +89,36 @@ jobs: CXX: clang++-19 ERROR_ON_WARNINGS: 1 RUN_ANALYZER: tsan + - name: Linux Arm64 (clang 16 + tsan) + os: ubuntu-24.04-arm + CC: clang-16 + CXX: clang++-16 + ERROR_ON_WARNINGS: 1 + RUN_ANALYZER: tsan + - name: Linux Arm64 (clang 17 + tsan) + os: ubuntu-24.04-arm + CC: clang-17 + CXX: clang++-17 + ERROR_ON_WARNINGS: 1 + RUN_ANALYZER: tsan + - name: Linux Arm64 (clang 18 + tsan) + os: ubuntu-24.04-arm + CC: clang-18 + CXX: clang++-18 + ERROR_ON_WARNINGS: 1 + RUN_ANALYZER: tsan - name: Linux Arm64 (clang 19 + tsan) os: ubuntu-24.04-arm CC: clang-19 CXX: clang++-19 ERROR_ON_WARNINGS: 1 RUN_ANALYZER: tsan + - name: Linux Arm64 (clang 20 + tsan) + os: ubuntu-24.04-arm + CC: clang-20 + CXX: clang++-20 + ERROR_ON_WARNINGS: 1 + RUN_ANALYZER: tsan - name: Linux (clang 19 + kcov) os: ubuntu-24.04 CC: clang-19 @@ -200,7 +224,7 @@ jobs: if: ${{ runner.os == 'Linux' && matrix.os != 'ubuntu-22.04' && !env['TEST_X86'] && !matrix.container }} run: | sudo apt update - sudo apt install cmake clang-19 llvm g++-12 valgrind zlib1g-dev libcurl4-openssl-dev libunwind-dev + sudo apt install cmake clang-16 clang-17 clang-18 clang-19 clang-20 llvm g++-12 valgrind zlib1g-dev libcurl4-openssl-dev libunwind-dev # Install kcov from source sudo apt-get install binutils-dev libssl-dev libelf-dev libstdc++-12-dev libdw-dev libiberty-dev git clone https://github.com/SimonKagstrom/kcov.git From 3e51843f529875bf610cb5dca1fde26e9011ab39 Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Thu, 13 Nov 2025 14:49:43 +0100 Subject: [PATCH 052/130] let valgrind match any frame between the libunwind obj matcher and the error site. --- tests/valgrind.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/valgrind.txt b/tests/valgrind.txt index 955977747..aaf7a10b5 100644 --- a/tests/valgrind.txt +++ b/tests/valgrind.txt @@ -42,5 +42,6 @@ libunwind writes buffers we do not manage Memcheck:Param write(buf) + ... obj:*/libunwind.so* } \ No newline at end of file From 21697c946c5068beb89065f0dd14252ab12c47d6 Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Thu, 13 Nov 2025 15:47:20 +0100 Subject: [PATCH 053/130] move the check for a crashed handler thread into dispatch_ucontext() where we execute the unsafe part directly in the signal-handler if the handler thread is dead as a last chance report --- src/backends/sentry_backend_inproc.c | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/backends/sentry_backend_inproc.c b/src/backends/sentry_backend_inproc.c index a5caf745a..623c92a48 100644 --- a/src/backends/sentry_backend_inproc.c +++ b/src/backends/sentry_backend_inproc.c @@ -882,7 +882,10 @@ static void dispatch_ucontext( const sentry_ucontext_t *uctx, const struct signal_slot *sig_slot) { - if (!sentry__atomic_fetch(&g_handler_thread_ready)) { + if (!sentry__atomic_fetch(&g_handler_thread_ready) + || has_handler_thread_crashed()) { + // directly execute unsafe part in signal handler as a last chance to + // report an error when the handler thread is unavailable. process_ucontext_deferred(uctx, sig_slot); return; } @@ -1038,10 +1041,8 @@ process_ucontext(const sentry_ucontext_t *uctx) sentry__page_allocator_enable(); #endif - if (!has_handler_thread_crashed()) { - // invoke the handler thread for signal unsafe actions - dispatch_ucontext(uctx, sig_slot); - } + // invoke the handler thread for signal unsafe actions + dispatch_ucontext(uctx, sig_slot); #ifdef SENTRY_PLATFORM_UNIX // reset signal handlers and invoke the original ones. This will then tear From 9388b49ac0897562d23a36fdcb4d69857aacf92a Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Thu, 13 Nov 2025 15:49:50 +0100 Subject: [PATCH 054/130] isolate the libunwind walker as a tsan cause in CI --- src/unwinder/sentry_unwinder_libunwind.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/unwinder/sentry_unwinder_libunwind.c b/src/unwinder/sentry_unwinder_libunwind.c index a47d4ca9b..645cf55fb 100644 --- a/src/unwinder/sentry_unwinder_libunwind.c +++ b/src/unwinder/sentry_unwinder_libunwind.c @@ -57,6 +57,7 @@ sentry__unwind_stack_libunwind( unw_word_t prev_sp = 0; (void)unw_get_reg(&cursor, UNW_REG_SP, &prev_sp); +#ifdef TSAN_CI_ISSUE_ISOLATION while (n < max_frames && unw_step(&cursor) > 0) { unw_word_t ip = 0, sp = 0; // stop the walk if we fail to read IP @@ -74,5 +75,6 @@ sentry__unwind_stack_libunwind( prev_sp = sp; ptrs[n++] = (void *)ip; } +#endif return n; } From 37477baf1616a4b7861cb1be135c76cd85b453ee Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Thu, 13 Nov 2025 16:01:47 +0100 Subject: [PATCH 055/130] exclude walker variables to prevent unused variables Werror --- src/unwinder/sentry_unwinder_libunwind.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/unwinder/sentry_unwinder_libunwind.c b/src/unwinder/sentry_unwinder_libunwind.c index 645cf55fb..74b038199 100644 --- a/src/unwinder/sentry_unwinder_libunwind.c +++ b/src/unwinder/sentry_unwinder_libunwind.c @@ -53,11 +53,11 @@ sentry__unwind_stack_libunwind( ptrs[n++] = (void *)ip; } // walk the callers +#ifdef TSAN_CI_ISSUE_ISOLATION unw_word_t prev_ip = (unw_word_t)ptrs[0]; unw_word_t prev_sp = 0; (void)unw_get_reg(&cursor, UNW_REG_SP, &prev_sp); -#ifdef TSAN_CI_ISSUE_ISOLATION while (n < max_frames && unw_step(&cursor) > 0) { unw_word_t ip = 0, sp = 0; // stop the walk if we fail to read IP From fcee5a511c084900acd32dbc4360d738e293fc31 Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Thu, 13 Nov 2025 16:22:55 +0100 Subject: [PATCH 056/130] isolation pinpoint fine, but only exclude when using ucontext. --- src/unwinder/sentry_unwinder_libunwind.c | 38 ++++++++++++------------ 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/src/unwinder/sentry_unwinder_libunwind.c b/src/unwinder/sentry_unwinder_libunwind.c index 74b038199..c2e770007 100644 --- a/src/unwinder/sentry_unwinder_libunwind.c +++ b/src/unwinder/sentry_unwinder_libunwind.c @@ -53,28 +53,28 @@ sentry__unwind_stack_libunwind( ptrs[n++] = (void *)ip; } // walk the callers -#ifdef TSAN_CI_ISSUE_ISOLATION - unw_word_t prev_ip = (unw_word_t)ptrs[0]; - unw_word_t prev_sp = 0; - (void)unw_get_reg(&cursor, UNW_REG_SP, &prev_sp); + if (!uctx) { + unw_word_t prev_ip = (unw_word_t)ptrs[0]; + unw_word_t prev_sp = 0; + (void)unw_get_reg(&cursor, UNW_REG_SP, &prev_sp); - while (n < max_frames && unw_step(&cursor) > 0) { - unw_word_t ip = 0, sp = 0; - // stop the walk if we fail to read IP - if (unw_get_reg(&cursor, UNW_REG_IP, &ip) < 0) { - break; - } - // SP is optional for progress - (void)unw_get_reg(&cursor, UNW_REG_SP, &sp); + while (n < max_frames && unw_step(&cursor) > 0) { + unw_word_t ip = 0, sp = 0; + // stop the walk if we fail to read IP + if (unw_get_reg(&cursor, UNW_REG_IP, &ip) < 0) { + break; + } + // SP is optional for progress + (void)unw_get_reg(&cursor, UNW_REG_SP, &sp); - // stop the walk if there is _no_ progress - if (ip == prev_ip && sp == prev_sp) { - break; + // stop the walk if there is _no_ progress + if (ip == prev_ip && sp == prev_sp) { + break; + } + prev_ip = ip; + prev_sp = sp; + ptrs[n++] = (void *)ip; } - prev_ip = ip; - prev_sp = sp; - ptrs[n++] = (void *)ip; } -#endif return n; } From eda314b4b31676606ebf36de8794faf57c361b52 Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Thu, 13 Nov 2025 18:34:43 +0100 Subject: [PATCH 057/130] silence clang-20 warning for crashpad --- external/crashpad | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/external/crashpad b/external/crashpad index 19d9d8a06..fbb5badde 160000 --- a/external/crashpad +++ b/external/crashpad @@ -1 +1 @@ -Subproject commit 19d9d8a06f6bbf3837db2464675e4545fff411f6 +Subproject commit fbb5badde25e9602b9fe44be329ddd005be7e637 From 89d95d21ab94fed630094094dff2cd48c53e3b40 Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Fri, 14 Nov 2025 17:01:17 +0100 Subject: [PATCH 058/130] update `crashpad` after merging to `getsentry` --- external/crashpad | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/external/crashpad b/external/crashpad index fbb5badde..60dd8995c 160000 --- a/external/crashpad +++ b/external/crashpad @@ -1 +1 @@ -Subproject commit fbb5badde25e9602b9fe44be329ddd005be7e637 +Subproject commit 60dd8995c6a8539718c878f9b41063604abe737c From 4d5114badfb43e6ad0c52e134303d35cae200cf2 Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Fri, 14 Nov 2025 18:50:52 +0100 Subject: [PATCH 059/130] we know the walk crashes in aarch64 tsan, now introduce stack bound reader in the libunwind walker for Linux and log as much as possible to understand where the actual crash happens --- src/unwinder/sentry_unwinder_libunwind.c | 119 ++++++++++++++++++----- tests/test_integration_stdout.py | 2 +- 2 files changed, 97 insertions(+), 24 deletions(-) diff --git a/src/unwinder/sentry_unwinder_libunwind.c b/src/unwinder/sentry_unwinder_libunwind.c index c2e770007..e75ef841e 100644 --- a/src/unwinder/sentry_unwinder_libunwind.c +++ b/src/unwinder/sentry_unwinder_libunwind.c @@ -2,6 +2,49 @@ #include "sentry_logger.h" #define UNW_LOCAL_ONLY #include +#include + +typedef struct { + uintptr_t lo, hi; +} mem_range_t; + +/** + * Looks up the memory range for a given pointer in /proc/self/maps. + * `range` is an output parameter. Returns 0 on success. + * Note: it is safe to use this function as long as we are running in a healthy + * thread or in the handler thread. The function is unsafe for a signal handler. + */ +static int +find_mem_range(uintptr_t ptr, mem_range_t *range) +{ + FILE *fp = fopen("/proc/self/maps", "r"); + if (!fp) { + return 1; + } + int result = 1; + char line[512]; + while (fgets(line, sizeof line, fp)) { + unsigned long lo, hi; + SENTRY_INFOF("%s", line); + if (sscanf(line, "%lx-%lx", &lo, &hi) == 2) { + // our bounds are [lo, hi) + if (ptr >= lo && ptr < hi) { + range->lo = (uintptr_t)lo; + range->hi = (uintptr_t)hi; + SENTRY_INFOF("Found ptr (0x%" PRIx64 + ") in the range %0x%" PRIx64 "-%0x%" PRIx64, + ptr, lo, hi); + result = 0; + break; + } + } + } + fclose(fp); + if (result) { + SENTRY_WARNF("Failed to find range for ptr (0x%" PRIx64 ")", ptr); + } + return result; +} size_t sentry__unwind_stack_libunwind( @@ -45,36 +88,66 @@ sentry__unwind_stack_libunwind( size_t n = 0; // get the first frame + unw_word_t ip = 0; + if (unw_get_reg(&cursor, UNW_REG_IP, &ip) < 0) { + return n; + } if (n < max_frames) { - unw_word_t ip = 0; - if (unw_get_reg(&cursor, UNW_REG_IP, &ip) < 0) { - return n; - } ptrs[n++] = (void *)ip; } - // walk the callers - if (!uctx) { - unw_word_t prev_ip = (unw_word_t)ptrs[0]; - unw_word_t prev_sp = 0; - (void)unw_get_reg(&cursor, UNW_REG_SP, &prev_sp); + unw_word_t sp = 0; + (void)unw_get_reg(&cursor, UNW_REG_SP, &sp); - while (n < max_frames && unw_step(&cursor) > 0) { - unw_word_t ip = 0, sp = 0; - // stop the walk if we fail to read IP - if (unw_get_reg(&cursor, UNW_REG_IP, &ip) < 0) { - break; + mem_range_t stack = { 0, 0 }; + int have_bounds = 0; + long page_size = 0; + if (uctx) { + if (find_mem_range((uintptr_t)sp, &stack) == 0) { + have_bounds = 1; + page_size = sysconf(_SC_PAGESIZE); + if (page_size <= 0) { + page_size = 4096; } - // SP is optional for progress - (void)unw_get_reg(&cursor, UNW_REG_SP, &sp); + } + } - // stop the walk if there is _no_ progress - if (ip == prev_ip && sp == prev_sp) { - break; - } - prev_ip = ip; - prev_sp = sp; - ptrs[n++] = (void *)ip; + // walk the callers + unw_word_t prev_ip = ip; + unw_word_t prev_sp = sp; + size_t step_idx = 0; + while (n < max_frames) { + SENTRY_DEBUGF("unwind: about to unw_step, step=%zu, prev_ip=%p prev_sp=%p", + step_idx, (void*)prev_ip, (void*)prev_sp); + if (unw_step(&cursor) <= 0) { + SENTRY_DEBUGF("unwind: unw_step failed at step=%zu", step_idx); + break; } + SENTRY_DEBUGF("unwind: unw_step success at step=%zu", step_idx); + + // stop the walk if we fail to read IP + if (unw_get_reg(&cursor, UNW_REG_IP, &ip) < 0) { + SENTRY_DEBUGF("unwind: no progress at step=%zu", step_idx); + break; + } + // SP is optional for progress + (void)unw_get_reg(&cursor, UNW_REG_SP, &sp); + + // stop the walk if there is _no_ progress + if (ip == prev_ip && sp == prev_sp) { + break; + } + + if (have_bounds) { + intptr_t d_lo = (intptr_t)((uintptr_t)sp - stack.lo); + intptr_t d_hi = (intptr_t)((uintptr_t)stack.hi - sp); + SENTRY_DEBUGF("unwind: unw_step %zu: ip=%p, sp=%p, d_lo=%zd, d_hi=%zd", n, + (void *)ip, (void *)sp, d_lo, d_hi); + } + + prev_ip = ip; + prev_sp = sp; + ptrs[n++] = (void *)ip; + step_idx++; } return n; } diff --git a/tests/test_integration_stdout.py b/tests/test_integration_stdout.py index fdded435a..d7ed20ccb 100644 --- a/tests/test_integration_stdout.py +++ b/tests/test_integration_stdout.py @@ -240,7 +240,7 @@ def test_inproc_crash_stdout_before_send_and_on_crash(cmake): ) def test_inproc_stack_overflow_stdout(cmake, build_args): tmp_path, output = run_stdout_for( - "inproc", cmake, ["attachment", "stack-overflow"], build_args + "inproc", cmake, ["log", "attachment", "stack-overflow"], build_args ) envelope = Envelope.deserialize(output) From 13dfdbcc358d37b5c49fd130bf93f0f6419d68b3 Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Fri, 14 Nov 2025 18:51:19 +0100 Subject: [PATCH 060/130] format --- src/unwinder/sentry_unwinder_libunwind.c | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/unwinder/sentry_unwinder_libunwind.c b/src/unwinder/sentry_unwinder_libunwind.c index e75ef841e..596d859bc 100644 --- a/src/unwinder/sentry_unwinder_libunwind.c +++ b/src/unwinder/sentry_unwinder_libunwind.c @@ -116,8 +116,9 @@ sentry__unwind_stack_libunwind( unw_word_t prev_sp = sp; size_t step_idx = 0; while (n < max_frames) { - SENTRY_DEBUGF("unwind: about to unw_step, step=%zu, prev_ip=%p prev_sp=%p", - step_idx, (void*)prev_ip, (void*)prev_sp); + SENTRY_DEBUGF( + "unwind: about to unw_step, step=%zu, prev_ip=%p prev_sp=%p", + step_idx, (void *)prev_ip, (void *)prev_sp); if (unw_step(&cursor) <= 0) { SENTRY_DEBUGF("unwind: unw_step failed at step=%zu", step_idx); break; @@ -140,7 +141,8 @@ sentry__unwind_stack_libunwind( if (have_bounds) { intptr_t d_lo = (intptr_t)((uintptr_t)sp - stack.lo); intptr_t d_hi = (intptr_t)((uintptr_t)stack.hi - sp); - SENTRY_DEBUGF("unwind: unw_step %zu: ip=%p, sp=%p, d_lo=%zd, d_hi=%zd", n, + SENTRY_DEBUGF( + "unwind: unw_step %zu: ip=%p, sp=%p, d_lo=%zd, d_hi=%zd", n, (void *)ip, (void *)sp, d_lo, d_hi); } From 72612cfaf8b3bdfd2cec602a90ddd6efc5dc6b2a Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Fri, 14 Nov 2025 19:22:18 +0100 Subject: [PATCH 061/130] add unistd.h for musl --- src/unwinder/sentry_unwinder_libunwind.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/unwinder/sentry_unwinder_libunwind.c b/src/unwinder/sentry_unwinder_libunwind.c index 596d859bc..74a3b5f7b 100644 --- a/src/unwinder/sentry_unwinder_libunwind.c +++ b/src/unwinder/sentry_unwinder_libunwind.c @@ -3,6 +3,7 @@ #define UNW_LOCAL_ONLY #include #include +#include typedef struct { uintptr_t lo, hi; From df9272853311ae5fbd9bba0299e3f76ffc8221ad Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Fri, 14 Nov 2025 20:35:09 +0100 Subject: [PATCH 062/130] eliminate the logs and ensure we never walk the callers if the SP is in unmapped memory --- src/unwinder/sentry_unwinder_libunwind.c | 59 ++++++------------------ 1 file changed, 13 insertions(+), 46 deletions(-) diff --git a/src/unwinder/sentry_unwinder_libunwind.c b/src/unwinder/sentry_unwinder_libunwind.c index 74a3b5f7b..d2c817ce9 100644 --- a/src/unwinder/sentry_unwinder_libunwind.c +++ b/src/unwinder/sentry_unwinder_libunwind.c @@ -3,7 +3,6 @@ #define UNW_LOCAL_ONLY #include #include -#include typedef struct { uintptr_t lo, hi; @@ -11,40 +10,33 @@ typedef struct { /** * Looks up the memory range for a given pointer in /proc/self/maps. - * `range` is an output parameter. Returns 0 on success. + * `range` is an output parameter. Returns `true` if a range was found. * Note: it is safe to use this function as long as we are running in a healthy * thread or in the handler thread. The function is unsafe for a signal handler. */ -static int +static bool find_mem_range(uintptr_t ptr, mem_range_t *range) { + bool found = false; FILE *fp = fopen("/proc/self/maps", "r"); if (!fp) { - return 1; + return found; } - int result = 1; char line[512]; while (fgets(line, sizeof line, fp)) { unsigned long lo, hi; - SENTRY_INFOF("%s", line); if (sscanf(line, "%lx-%lx", &lo, &hi) == 2) { // our bounds are [lo, hi) if (ptr >= lo && ptr < hi) { range->lo = (uintptr_t)lo; range->hi = (uintptr_t)hi; - SENTRY_INFOF("Found ptr (0x%" PRIx64 - ") in the range %0x%" PRIx64 "-%0x%" PRIx64, - ptr, lo, hi); - result = 0; + found = true; break; } } } fclose(fp); - if (result) { - SENTRY_WARNF("Failed to find range for ptr (0x%" PRIx64 ")", ptr); - } - return result; + return found; } size_t @@ -88,7 +80,7 @@ sentry__unwind_stack_libunwind( } size_t n = 0; - // get the first frame + // get the first frame and stack pointer unw_word_t ip = 0; if (unw_get_reg(&cursor, UNW_REG_IP, &ip) < 0) { return n; @@ -99,36 +91,20 @@ sentry__unwind_stack_libunwind( unw_word_t sp = 0; (void)unw_get_reg(&cursor, UNW_REG_SP, &sp); + // ensure we have a valid stack pointer otherwise we only send the top frame mem_range_t stack = { 0, 0 }; - int have_bounds = 0; - long page_size = 0; - if (uctx) { - if (find_mem_range((uintptr_t)sp, &stack) == 0) { - have_bounds = 1; - page_size = sysconf(_SC_PAGESIZE); - if (page_size <= 0) { - page_size = 4096; - } - } + if (uctx && !find_mem_range((uintptr_t)sp, &stack) == 0) { + SENTRY_WARNF("unwinder: SP (%p) is in unmapped memory likely due to stack overflow", + (void *)sp); + return n; } // walk the callers unw_word_t prev_ip = ip; unw_word_t prev_sp = sp; - size_t step_idx = 0; - while (n < max_frames) { - SENTRY_DEBUGF( - "unwind: about to unw_step, step=%zu, prev_ip=%p prev_sp=%p", - step_idx, (void *)prev_ip, (void *)prev_sp); - if (unw_step(&cursor) <= 0) { - SENTRY_DEBUGF("unwind: unw_step failed at step=%zu", step_idx); - break; - } - SENTRY_DEBUGF("unwind: unw_step success at step=%zu", step_idx); - + while (n < max_frames && unw_step(&cursor) > 0) { // stop the walk if we fail to read IP if (unw_get_reg(&cursor, UNW_REG_IP, &ip) < 0) { - SENTRY_DEBUGF("unwind: no progress at step=%zu", step_idx); break; } // SP is optional for progress @@ -139,18 +115,9 @@ sentry__unwind_stack_libunwind( break; } - if (have_bounds) { - intptr_t d_lo = (intptr_t)((uintptr_t)sp - stack.lo); - intptr_t d_hi = (intptr_t)((uintptr_t)stack.hi - sp); - SENTRY_DEBUGF( - "unwind: unw_step %zu: ip=%p, sp=%p, d_lo=%zd, d_hi=%zd", n, - (void *)ip, (void *)sp, d_lo, d_hi); - } - prev_ip = ip; prev_sp = sp; ptrs[n++] = (void *)ip; - step_idx++; } return n; } From bdf6b2573146d889487cf3689cb23aa3f80b8d91 Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Fri, 14 Nov 2025 20:36:13 +0100 Subject: [PATCH 063/130] format --- src/unwinder/sentry_unwinder_libunwind.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/unwinder/sentry_unwinder_libunwind.c b/src/unwinder/sentry_unwinder_libunwind.c index d2c817ce9..a9d086392 100644 --- a/src/unwinder/sentry_unwinder_libunwind.c +++ b/src/unwinder/sentry_unwinder_libunwind.c @@ -94,7 +94,8 @@ sentry__unwind_stack_libunwind( // ensure we have a valid stack pointer otherwise we only send the top frame mem_range_t stack = { 0, 0 }; if (uctx && !find_mem_range((uintptr_t)sp, &stack) == 0) { - SENTRY_WARNF("unwinder: SP (%p) is in unmapped memory likely due to stack overflow", + SENTRY_WARNF("unwinder: SP (%p) is in unmapped memory likely due to " + "stack overflow", (void *)sp); return n; } From 7e4e1fa71dadde76dfd75d42f148634d3f3b67a9 Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Fri, 14 Nov 2025 21:07:50 +0100 Subject: [PATCH 064/130] fix invalid `find_mem_range()` return value check --- src/unwinder/sentry_unwinder_libunwind.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/unwinder/sentry_unwinder_libunwind.c b/src/unwinder/sentry_unwinder_libunwind.c index a9d086392..563228e9d 100644 --- a/src/unwinder/sentry_unwinder_libunwind.c +++ b/src/unwinder/sentry_unwinder_libunwind.c @@ -93,7 +93,7 @@ sentry__unwind_stack_libunwind( // ensure we have a valid stack pointer otherwise we only send the top frame mem_range_t stack = { 0, 0 }; - if (uctx && !find_mem_range((uintptr_t)sp, &stack) == 0) { + if (uctx && !find_mem_range((uintptr_t)sp, &stack)) { SENTRY_WARNF("unwinder: SP (%p) is in unmapped memory likely due to " "stack overflow", (void *)sp); From 0966118fa88e7da880615e2ddaed42125c26ea8e Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Fri, 14 Nov 2025 21:08:44 +0100 Subject: [PATCH 065/130] remove macOS left-overs in the `libbacktrace` unwinder module --- src/unwinder/sentry_unwinder_libbacktrace.c | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/src/unwinder/sentry_unwinder_libbacktrace.c b/src/unwinder/sentry_unwinder_libbacktrace.c index dd671a958..fbfb0f895 100644 --- a/src/unwinder/sentry_unwinder_libbacktrace.c +++ b/src/unwinder/sentry_unwinder_libbacktrace.c @@ -2,7 +2,7 @@ // XXX: Make into a CMake check // XXX: IBM i PASE offers libbacktrace in libutil, but not available in AIX -#if defined(SENTRY_PLATFORM_DARWIN) || defined(__GLIBC__) || defined(__PASE__) +#if defined(__GLIBC__) || defined(__PASE__) # define HAS_EXECINFO_H #endif @@ -14,15 +14,7 @@ size_t sentry__unwind_stack_libbacktrace( void *addr, const sentry_ucontext_t *uctx, void **ptrs, size_t max_frames) { - if (addr) { -#if defined(SENTRY_PLATFORM_MACOS) && defined(MAC_OS_X_VERSION_10_14) \ - && __has_builtin(__builtin_available) - if (__builtin_available(macOS 10.14, *)) { - return (size_t)backtrace_from_fp(addr, ptrs, (int)max_frames); - } -#endif - return 0; - } else if (uctx) { + if (addr || uctx) { return 0; } #ifdef HAS_EXECINFO_H From 549293a2ef5af17168ad16631eee2715c6a06937 Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Fri, 14 Nov 2025 21:26:16 +0100 Subject: [PATCH 066/130] extract fp_walk for the macOS unwinder so we can also provide a stack-trace from an arbitrary frame-pointer --- src/unwinder/sentry_unwinder_libunwind_mac.c | 72 +++++++++----------- 1 file changed, 34 insertions(+), 38 deletions(-) diff --git a/src/unwinder/sentry_unwinder_libunwind_mac.c b/src/unwinder/sentry_unwinder_libunwind_mac.c index 2843b0727..6e87b7d8b 100644 --- a/src/unwinder/sentry_unwinder_libunwind_mac.c +++ b/src/unwinder/sentry_unwinder_libunwind_mac.c @@ -9,7 +9,36 @@ valid_ptr(uintptr_t p) return p && (p % sizeof(uintptr_t) == 0); } -size_t +/** + * This does the same frame-pointer walk for arm64 and x86_64, with the only + * difference being which registers value is used as frame-pointer (fp vs rbp) + */ +static void +fp_walk(uintptr_t fp, size_t *n, void **ptrs, size_t max_frames) +{ + while (*n < max_frames) { + if (!valid_ptr(fp)) { + break; + } + + // arm64 frame record layout: [prev_fp, saved_lr] at fp and fp+8 + // x86_64 frame record layout: [prev_rbp, saved_retaddr] at bp and bp+8 + const uintptr_t *record = (uintptr_t *)fp; + const uintptr_t next_fp = record[0]; + const uintptr_t ret_addr = record[1]; + if (!valid_ptr(next_fp) || !ret_addr) { + break; + } + + ptrs[(*n)++] = (void *)(ret_addr - 1); + if (next_fp <= fp) { + break; // prevent loops + } + fp = next_fp; + } +} + +static size_t fp_walk_from_uctx(const sentry_ucontext_t *uctx, void **ptrs, size_t max_frames) { size_t n = 0; @@ -29,25 +58,7 @@ fp_walk_from_uctx(const sentry_ucontext_t *uctx, void **ptrs, size_t max_frames) ptrs[n++] = (void *)(lr - 1); } - while (n < max_frames) { - if (!valid_ptr(fp)) { - break; - } - - // arm64 frame record layout: [prev_fp, saved_lr] at fp and fp+8 - uintptr_t *record = (uintptr_t *)fp; - uintptr_t next_fp = record[0]; - uintptr_t ret_addr = record[1]; - if (!valid_ptr(next_fp) || !ret_addr) { - break; - } - - ptrs[n++] = (void *)(ret_addr - 1); - if (next_fp <= fp) { - break; // prevent loops - } - fp = next_fp; - } + fp_walk(fp, &n, ptrs, max_frames); #elif defined(__x86_64__) uintptr_t ip = (uintptr_t)mctx->__ss.__rip; uintptr_t bp = (uintptr_t)mctx->__ss.__rbp; @@ -57,23 +68,7 @@ fp_walk_from_uctx(const sentry_ucontext_t *uctx, void **ptrs, size_t max_frames) ptrs[n++] = (void *)(ip - 1); } - while (n < max_frames) { - if (!valid_ptr(bp)) { - break; - } - // x86_64 frame record layout: [prev_rbp, saved_retaddr] at bp and bp+8 - uintptr_t *record = (uintptr_t *)bp; - uintptr_t next_bp = record[0]; - uintptr_t ret_addr = record[1]; - if (!valid_ptr(next_bp) || !ret_addr) { - break; - } - ptrs[n++] = (void *)(ret_addr - 1); - if (next_bp <= bp) { - break; - } - bp = next_bp; - } + fp_walk(bp, &n, ptrs, max_frames); #else # error "Unsupported CPU architecture for macOS stackwalker" #endif @@ -85,7 +80,8 @@ sentry__unwind_stack_libunwind_mac( void *addr, const sentry_ucontext_t *uctx, void **ptrs, size_t max_frames) { if (addr) { - // we don't support stack walks from arbitrary addresses + size_t n = 0; + fp_walk((uintptr_t)addr, &n, ptrs, max_frames); return 0; } From be0b2458c616ca6cf9c21b0a3c99f3ccaf41a112 Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Fri, 14 Nov 2025 21:34:24 +0100 Subject: [PATCH 067/130] for targets that must use `backtrace()` as an unwinder we fall back und running the deferred code directly inside the signal handler. Nothing changes for them. --- src/backends/sentry_backend_inproc.c | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/src/backends/sentry_backend_inproc.c b/src/backends/sentry_backend_inproc.c index 623c92a48..d911ae7d0 100644 --- a/src/backends/sentry_backend_inproc.c +++ b/src/backends/sentry_backend_inproc.c @@ -882,6 +882,12 @@ static void dispatch_ucontext( const sentry_ucontext_t *uctx, const struct signal_slot *sig_slot) { +#ifdef SENTRY_WITH_UNWINDER_LIBBACKTRACE + // For targets that still use `backtrace()` as the sole unwinder we must + // run the signal-unsafe part in the signal handler like we did before. + process_ucontext_deferred(uctx, sig_slot); + return; +#else if (!sentry__atomic_fetch(&g_handler_thread_ready) || has_handler_thread_crashed()) { // directly execute unsafe part in signal handler as a last chance to @@ -893,7 +899,7 @@ dispatch_ucontext( g_handler_state.uctx = *uctx; g_handler_state.sig_slot = sig_slot; -#ifdef SENTRY_PLATFORM_UNIX +# ifdef SENTRY_PLATFORM_UNIX if (uctx->siginfo) { memcpy(&g_handler_state.siginfo_storage, uctx->siginfo, sizeof(g_handler_state.siginfo_storage)); @@ -913,13 +919,13 @@ dispatch_ucontext( // we leave the handler sentry__leave_signal_handler(); -#endif +# endif sentry__atomic_store(&g_handler_work_done, 0); sentry__atomic_store(&g_handler_has_work, 1); // signal the handler thread to start working -#ifdef SENTRY_PLATFORM_UNIX +# ifdef SENTRY_PLATFORM_UNIX if (g_handler_pipe[1] >= 0) { char c = 1; ssize_t rv; @@ -927,19 +933,21 @@ dispatch_ucontext( rv = write(g_handler_pipe[1], &c, 1); } while (rv == -1 && errno == EINTR); } -#elif defined(SENTRY_PLATFORM_WINDOWS) +# elif defined(SENTRY_PLATFORM_WINDOWS) if (g_handler_semaphore) { ReleaseSemaphore(g_handler_semaphore, 1, NULL); } -#endif +# endif // wait until the handler has done its work while (!sentry__atomic_fetch(&g_handler_work_done)) { sentry__cpu_relax(); } -#ifdef SENTRY_PLATFORM_UNIX +# ifdef SENTRY_PLATFORM_UNIX sentry__enter_signal_handler(); +# endif + #endif } From 1c8dbf9dfe0ecfc75e2cc39323c09faa5fc95769 Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Mon, 17 Nov 2025 20:20:32 +0100 Subject: [PATCH 068/130] instead of spinning on atomic in the signal-handler add ACK pipe/semaphore on the return channel and let the OS block and wait. Also check the return value of startup_handler_thread in the initialization and propagate the failure. --- src/backends/sentry_backend_inproc.c | 77 ++++++++++++++++++++++++---- 1 file changed, 67 insertions(+), 10 deletions(-) diff --git a/src/backends/sentry_backend_inproc.c b/src/backends/sentry_backend_inproc.c index d911ae7d0..c51c56fe1 100644 --- a/src/backends/sentry_backend_inproc.c +++ b/src/backends/sentry_backend_inproc.c @@ -56,14 +56,14 @@ static volatile long g_handler_thread_ready = 0; static volatile long g_handler_should_exit = 0; // signal handler tells handler thread to start working static volatile long g_handler_has_work = 0; -// handler thread wakes signal handler from suspension after finishing the work -static volatile long g_handler_work_done = 0; -// trigger/schedule primitives that block the handler thread until we need it +// trigger/schedule primitives that block the other side until this side is done #ifdef SENTRY_PLATFORM_UNIX static int g_handler_pipe[2] = { -1, -1 }; +static int g_handler_ack_pipe[2] = { -1, -1 }; #elif defined(SENTRY_PLATFORM_WINDOWS) static HANDLE g_handler_semaphore = NULL; +static HANDLE g_handler_ack_semaphore = NULL; #endif static int start_handler_thread(void); @@ -137,7 +137,9 @@ startup_inproc_backend( backend->data = &g_backend_config; } - start_handler_thread(); + if (start_handler_thread() != 0) { + return 1; + } // save the old signal handlers memset(g_previous_handlers, 0, sizeof(g_previous_handlers)); @@ -247,7 +249,9 @@ startup_inproc_backend( && defined(SENTRY_THREAD_STACK_GUARANTEE_AUTO_INIT) sentry__set_default_thread_stack_guarantee(); # endif - start_handler_thread(); + if (start_handler_thread() != 0) { + return 1; + } g_previous_handler = SetUnhandledExceptionFilter(&handle_exception); SetErrorMode(SEM_FAILCRITICALERRORS); return 0; @@ -753,7 +757,19 @@ handler_thread_main(void *UNUSED(data)) process_ucontext_deferred( &g_handler_state.uctx, g_handler_state.sig_slot); sentry__atomic_store(&g_handler_has_work, 0); - sentry__atomic_store(&g_handler_work_done, 1); +#ifdef SENTRY_PLATFORM_UNIX + if (g_handler_ack_pipe[1] >= 0) { + char c = 1; + ssize_t rv; + do { + rv = write(g_handler_ack_pipe[1], &c, 1); + } while (rv == -1 && errno == EINTR); + } +#elif defined(SENTRY_PLATFORM_WINDOWS) + if (g_handler_ack_semaphore) { + ReleaseSemaphore(g_handler_ack_semaphore, 1, NULL); + } +#endif } #ifdef SENTRY_PLATFORM_WINDOWS @@ -773,19 +789,33 @@ start_handler_thread(void) sentry__thread_init(&g_handler_thread); sentry__atomic_store(&g_handler_should_exit, 0); sentry__atomic_store(&g_handler_has_work, 0); - sentry__atomic_store(&g_handler_work_done, 0); #ifdef SENTRY_PLATFORM_UNIX if (pipe(g_handler_pipe) != 0) { SENTRY_WARNF("failed to create handler pipe: %s", strerror(errno)); return 1; } + if (pipe(g_handler_ack_pipe) != 0) { + SENTRY_WARNF("failed to create handler ack pipe: %s", strerror(errno)); + close(g_handler_pipe[0]); + close(g_handler_pipe[1]); + g_handler_pipe[0] = -1; + g_handler_pipe[1] = -1; + return 1; + } #elif defined(SENTRY_PLATFORM_WINDOWS) g_handler_semaphore = CreateSemaphoreW(NULL, 0, LONG_MAX, NULL); if (!g_handler_semaphore) { SENTRY_WARN("failed to create handler semaphore"); return 1; } + g_handler_ack_semaphore = CreateSemaphoreW(NULL, 0, LONG_MAX, NULL); + if (!g_handler_ack_semaphore) { + SENTRY_WARN("failed to create handler ack semaphore"); + CloseHandle(g_handler_semaphore); + g_handler_semaphore = NULL; + return 1; + } #endif if (sentry__thread_spawn(&g_handler_thread, handler_thread_main, NULL) @@ -796,9 +826,15 @@ start_handler_thread(void) close(g_handler_pipe[1]); g_handler_pipe[0] = -1; g_handler_pipe[1] = -1; + close(g_handler_ack_pipe[0]); + close(g_handler_ack_pipe[1]); + g_handler_ack_pipe[0] = -1; + g_handler_ack_pipe[1] = -1; #elif defined(SENTRY_PLATFORM_WINDOWS) CloseHandle(g_handler_semaphore); g_handler_semaphore = NULL; + CloseHandle(g_handler_ack_semaphore); + g_handler_ack_semaphore = NULL; #endif return 1; } @@ -841,11 +877,23 @@ stop_handler_thread(void) close(g_handler_pipe[1]); g_handler_pipe[1] = -1; } + if (g_handler_ack_pipe[0] >= 0) { + close(g_handler_ack_pipe[0]); + g_handler_ack_pipe[0] = -1; + } + if (g_handler_ack_pipe[1] >= 0) { + close(g_handler_ack_pipe[1]); + g_handler_ack_pipe[1] = -1; + } #elif defined(SENTRY_PLATFORM_WINDOWS) if (g_handler_semaphore) { CloseHandle(g_handler_semaphore); g_handler_semaphore = NULL; } + if (g_handler_ack_semaphore) { + CloseHandle(g_handler_ack_semaphore); + g_handler_ack_semaphore = NULL; + } #endif sentry__atomic_store(&g_handler_thread_ready, 0); @@ -921,7 +969,6 @@ dispatch_ucontext( sentry__leave_signal_handler(); # endif - sentry__atomic_store(&g_handler_work_done, 0); sentry__atomic_store(&g_handler_has_work, 1); // signal the handler thread to start working @@ -940,9 +987,19 @@ dispatch_ucontext( # endif // wait until the handler has done its work - while (!sentry__atomic_fetch(&g_handler_work_done)) { - sentry__cpu_relax(); +# ifdef SENTRY_PLATFORM_UNIX + if (g_handler_ack_pipe[0] >= 0) { + char c = 0; + ssize_t rv; + do { + rv = read(g_handler_ack_pipe[0], &c, 1); + } while (rv == -1 && errno == EINTR); } +# elif defined(SENTRY_PLATFORM_WINDOWS) + if (g_handler_ack_semaphore) { + WaitForSingleObject(g_handler_ack_semaphore, INFINITE); + } +# endif # ifdef SENTRY_PLATFORM_UNIX sentry__enter_signal_handler(); From 1d85149fa0d46ffd859943c13b76b2a2e6565bbb Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Tue, 18 Nov 2025 00:11:52 +0100 Subject: [PATCH 069/130] actually check the FP against mach_vm_region bounds in the validation --- src/CMakeLists.txt | 3 ++ src/unwinder/sentry_unwinder.h | 8 +++ src/unwinder/sentry_unwinder_libunwind.c | 5 +- src/unwinder/sentry_unwinder_libunwind_mac.c | 51 +++++++++++++++++++- 4 files changed, 61 insertions(+), 6 deletions(-) create mode 100644 src/unwinder/sentry_unwinder.h diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index e212c702c..2aff0b054 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -153,6 +153,9 @@ elseif(SENTRY_BACKEND_NONE) endif() # unwinder +sentry_target_sources_cwd(sentry + unwinder/sentry_unwinder.h +) if(SENTRY_WITH_LIBBACKTRACE) target_compile_definitions(sentry PRIVATE SENTRY_WITH_UNWINDER_LIBBACKTRACE) sentry_target_sources_cwd(sentry diff --git a/src/unwinder/sentry_unwinder.h b/src/unwinder/sentry_unwinder.h new file mode 100644 index 000000000..889d20ca1 --- /dev/null +++ b/src/unwinder/sentry_unwinder.h @@ -0,0 +1,8 @@ +#ifndef SENTRY_UNWINDER_H_INCLUDED +#define SENTRY_UNWINDER_H_INCLUDED + +typedef struct { + uintptr_t lo, hi; +} mem_range_t; + +#endif // SENTRY_UNWINDER_H_INCLUDED diff --git a/src/unwinder/sentry_unwinder_libunwind.c b/src/unwinder/sentry_unwinder_libunwind.c index 563228e9d..27439c20c 100644 --- a/src/unwinder/sentry_unwinder_libunwind.c +++ b/src/unwinder/sentry_unwinder_libunwind.c @@ -1,13 +1,10 @@ #include "sentry_boot.h" #include "sentry_logger.h" +#include "sentry_unwinder.h" #define UNW_LOCAL_ONLY #include #include -typedef struct { - uintptr_t lo, hi; -} mem_range_t; - /** * Looks up the memory range for a given pointer in /proc/self/maps. * `range` is an output parameter. Returns `true` if a range was found. diff --git a/src/unwinder/sentry_unwinder_libunwind_mac.c b/src/unwinder/sentry_unwinder_libunwind_mac.c index 6e87b7d8b..0190821de 100644 --- a/src/unwinder/sentry_unwinder_libunwind_mac.c +++ b/src/unwinder/sentry_unwinder_libunwind_mac.c @@ -1,12 +1,59 @@ #include "sentry_boot.h" #include "sentry_logger.h" +#include "sentry_unwinder.h" #include +#include +#include + +// Basic pointer validation to make sure we stay inside mapped memory. +static bool +is_readable_ptr(uintptr_t p, size_t size) +{ + if (!p) { + return false; + } + + mach_vm_address_t address = (mach_vm_address_t)p; + mach_vm_size_t region_size = 0; + vm_region_basic_info_data_64_t info; + mach_msg_type_number_t count = VM_REGION_BASIC_INFO_COUNT_64; + mach_port_t object = MACH_PORT_NULL; + + kern_return_t kr = mach_vm_region(mach_task_self(), &address, ®ion_size, + VM_REGION_BASIC_INFO_64, (vm_region_info_t)&info, &count, &object); + if (object != MACH_PORT_NULL) { + mach_port_deallocate(mach_task_self(), object); + } + if (kr != KERN_SUCCESS) { + return false; + } + + if (!(info.protection & VM_PROT_READ)) { + return false; + } + + mem_range_t vm_region + = { (uintptr_t)address, (uintptr_t)address + (uintptr_t)region_size }; + if (vm_region.hi < vm_region.lo) { + return false; + } + + uintptr_t end = p + (uintptr_t)size; + if (end < p) { + return false; + } + + return p >= vm_region.lo && end <= vm_region.hi; +} -// a very cheap pointer validation for starters static bool valid_ptr(uintptr_t p) { - return p && (p % sizeof(uintptr_t) == 0); + if (!p || (p % sizeof(uintptr_t)) != 0) { + return false; + } + + return is_readable_ptr(p, sizeof(uintptr_t) * 2); } /** From cd130d37cbc1eb323242f62957d148ac6a43e854 Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Tue, 18 Nov 2025 00:23:02 +0100 Subject: [PATCH 070/130] check mach_vm_region bounds only on macOS builds --- src/unwinder/sentry_unwinder_libunwind_mac.c | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/unwinder/sentry_unwinder_libunwind_mac.c b/src/unwinder/sentry_unwinder_libunwind_mac.c index 0190821de..91ceda367 100644 --- a/src/unwinder/sentry_unwinder_libunwind_mac.c +++ b/src/unwinder/sentry_unwinder_libunwind_mac.c @@ -2,9 +2,13 @@ #include "sentry_logger.h" #include "sentry_unwinder.h" #include -#include -#include +#if defined(SENTRY_PLATFORM_MACOS) +# include +# include +#endif + +#if defined(SENTRY_PLATFORM_MACOS) // Basic pointer validation to make sure we stay inside mapped memory. static bool is_readable_ptr(uintptr_t p, size_t size) @@ -45,6 +49,7 @@ is_readable_ptr(uintptr_t p, size_t size) return p >= vm_region.lo && end <= vm_region.hi; } +#endif static bool valid_ptr(uintptr_t p) @@ -53,7 +58,11 @@ valid_ptr(uintptr_t p) return false; } +#if defined(SENTRY_PLATFORM_MACOS) return is_readable_ptr(p, sizeof(uintptr_t) * 2); +#else + return true; +#endif } /** From 9e9d933bdf8301414defc0148327d77a700e39b7 Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Tue, 18 Nov 2025 08:39:44 +0100 Subject: [PATCH 071/130] ensure we conditionally return on acquire in block_for_signal_handler --- src/sentry_sync.c | 4 +++- tests/unit/tests.inc | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/sentry_sync.c b/src/sentry_sync.c index 1b91bf310..299e444a7 100644 --- a/src/sentry_sync.c +++ b/src/sentry_sync.c @@ -517,7 +517,9 @@ sentry__block_for_signal_handler(void) // if there is no signal handler active, we don't need to block // we can spin cheaply, but for the return we must acquire if (!__atomic_load_n(&g_in_signal_handler, __ATOMIC_RELAXED)) { - return __atomic_load_n(&g_in_signal_handler, __ATOMIC_ACQUIRE) == 0; + if (__atomic_load_n(&g_in_signal_handler, __ATOMIC_ACQUIRE) == 0) { + return true; + } } // if we are on the signal handler thread we can also leave diff --git a/tests/unit/tests.inc b/tests/unit/tests.inc index f8fa3c920..d09250663 100644 --- a/tests/unit/tests.inc +++ b/tests/unit/tests.inc @@ -8,7 +8,6 @@ XX(attachments_bytes) XX(attachments_extend) XX(background_worker) XX(basic_consent_tracking) -XX(query_consent_requirement) XX(basic_function_transport) XX(basic_function_transport_transaction) XX(basic_function_transport_transaction_ts) @@ -122,6 +121,7 @@ XX(process_invalid) XX(process_spawn) XX(procmaps_parser) XX(propagation_context_init) +XX(query_consent_requirement) XX(rate_limit_parsing) XX(read_envelope_from_file) XX(read_write_envelope_to_file_null) From 6b6e5457ef9d8e8d819028562503b7d7923baa9b Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Tue, 18 Nov 2025 09:23:58 +0100 Subject: [PATCH 072/130] move chain-first strategy completely outside the signal handler reentrancy guard * up to now, we've been serializing signal handling even though we didn't know whether it was a runtime signal or one we should be handling * this meant that we blocked all our critical sections during a managed exception * it also meant that we blocked any concurrent managed exceptions * it also meant that we introduced a race window during the time when we chained, because incoming signal on other threads would have gotten next in line, before we even completed the current signal handler by moving it completely outside our synchronization we truly chain at start and don't interfere until we know we must. --- src/backends/sentry_backend_inproc.c | 29 ++++++++-------------------- 1 file changed, 8 insertions(+), 21 deletions(-) diff --git a/src/backends/sentry_backend_inproc.c b/src/backends/sentry_backend_inproc.c index c51c56fe1..8a08dad4b 100644 --- a/src/backends/sentry_backend_inproc.c +++ b/src/backends/sentry_backend_inproc.c @@ -1035,14 +1035,6 @@ dispatch_ucontext( static void process_ucontext(const sentry_ucontext_t *uctx) { -#ifdef SENTRY_PLATFORM_UNIX - sentry__enter_signal_handler(); -#endif - - if (!g_backend_config.enable_logging_when_crashed) { - sentry__logger_disable(); - } - #ifdef SENTRY_PLATFORM_LINUX if (g_backend_config.handler_strategy == SENTRY_HANDLER_STRATEGY_CHAIN_AT_START) { @@ -1051,14 +1043,6 @@ process_ucontext(const sentry_ucontext_t *uctx) // cases, we shouldn't react to the signal at all and let their handler // discontinue the signal chain by invoking the runtime handler before // we process the signal. - // there is a good chance that we won't return from the previous - // handler and that would mean we couldn't enter this handler with - // the next signal coming in if we didn't "leave" here. - sentry__leave_signal_handler(); - if (!g_backend_config.enable_logging_when_crashed) { - sentry__logger_enable(); - } - uintptr_t ip = get_instruction_pointer(uctx); uintptr_t sp = get_stack_pointer(uctx); @@ -1077,16 +1061,19 @@ process_ucontext(const sentry_ucontext_t *uctx) return; } - // let's re-enter because it means this was an actual native crash - if (!g_backend_config.enable_logging_when_crashed) { - sentry__logger_disable(); - } - sentry__enter_signal_handler(); // return from runtime handler; continue processing the crash on the // signal thread until the worker takes over } #endif +#ifdef SENTRY_PLATFORM_UNIX + sentry__enter_signal_handler(); +#endif + + if (!g_backend_config.enable_logging_when_crashed) { + sentry__logger_disable(); + } + const struct signal_slot *sig_slot = NULL; for (int i = 0; i < SIGNAL_COUNT; ++i) { #ifdef SENTRY_PLATFORM_UNIX From a011327964940adf9c3958ac06e351f6aa01068a Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Tue, 18 Nov 2025 13:30:12 +0100 Subject: [PATCH 073/130] update changelog --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7eaa1b765..a3d2228cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,12 +2,23 @@ ## Unreleased +**Breaking**: + +- inproc(Linux): the `inproc` backend on Linux now depends on "nognu" `libunwind`. ([#1446](https://github.com/getsentry/sentry-native/pull/1446)) +- inproc: since we split `inproc` into signal-handler/UEF part and a separate handler thread, `before_send` and `on_crash` could be called from other threads than the one that crashed. While this was never part of the contract, if your code relies on this, it will no longer work. ([#1446](https://github.com/getsentry/sentry-native/pull/1446)) + **Features**: - Add custom attributes API for logs. When `logs_with_attributes` is set to `true`, treats the first `varg` passed into `sentry_logs_X(message,...)` as a `sentry_value_t` object of attributes. ([#1435](https://github.com/getsentry/sentry-native/pull/1435)) - Add runtime API to query user consent requirement. ([#1443](https://github.com/getsentry/sentry-native/pull/1443)) - Add logs flush on `sentry_flush()`. ([#1434](https://github.com/getsentry/sentry-native/pull/1434)) +**Fixes**: + +- Make the signal-handler synchronization fully atomic to fix rare race scenarios. ([#1446](https://github.com/getsentry/sentry-native/pull/1446)) +- Reintroduce an FP-based stack-walker for macOS that can start from a user context. This also makes `inproc` backend functional again on macOS 13+. ([#1446](https://github.com/getsentry/sentry-native/pull/1446)) +- Split the `inproc` signal handler (and UEF on Windows) into a safe handler part and an "unsafe" handler thread. This minimizes exposure to undefined behavior inside the signal handler. ([#1446](https://github.com/getsentry/sentry-native/pull/1446)) + ## 0.12.1 **Fixes**: From 4a62c6e6e0585a9d9533f0f17c1b23dce7e2d022 Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Thu, 20 Nov 2025 11:51:50 +0100 Subject: [PATCH 074/130] add inproc module-level docs for developers --- src/backends/sentry_backend_inproc.c | 97 +++++++++++++++++++++++++++- 1 file changed, 96 insertions(+), 1 deletion(-) diff --git a/src/backends/sentry_backend_inproc.c b/src/backends/sentry_backend_inproc.c index 8a08dad4b..ec65cf0e1 100644 --- a/src/backends/sentry_backend_inproc.c +++ b/src/backends/sentry_backend_inproc.c @@ -23,8 +23,103 @@ #include #include -#define SIGNAL_DEF(Sig, Desc) { Sig, #Sig, Desc } +/** + * Inproc Backend Introduction + * + * As the name suggests the inproc backend runs the crash handling entirely + * inside the process and thus is the right choice for platforms that + * are limited in process creation/spawning/cloning (or even deploying a + * separate release artifact like with `crashpad`). It is also very lightweight + * in terms of toolchain dependencies because it does not require a C++ standard + * library. + * + * It targets UNIX and Windows (effectively supporting all target platforms of + * the Native SDK) and uses POSIX signal handling on UNIX and unhandled + * exception filters (UEF) on Windows. Whenever a signal handler is mentioned + * in the code or comments, one can replace that with UEF on Windows. + * + * In its current implementation it only gathers the crash context for the + * crashed thread and does not attempt to stop any other threads. While this + * can be considered a downside for some users, it allows additional handlers + * to process the crashed process again, which the other backends currenlty + * can't guarantee to work. Additional crash signals coming from other threads + * will be blocked indefinitely until previous handler takes over. + * + * The inproc backend splits the handler in two parts: + * - a signal handler/unhandled exception filter that severely limits what we + * can do, focusing on response to the OS mechanism and almost zero policy. + * - a separate handler thread that does most of the typical sentry error + * handling and policy implementation, with a bit more freedom. + * + * Only if the handler thread has crashed or is otherwise unavailable, will we + * execute the unsafe part inside the signal-handler itself, as a last chance + * fallback for report creation. The signal-handler part should not use any + * synchronization or signal-unsafe function from `libc` (see function-level + * comment), even access to options is ideally done before during + * initailization. If access to option or scope (or any other global context) + * is required this should happen in the handler thread. + * + * The handler thread is started during backend initialization and will be + * triggered by the signal handler via a POSIX pipe on which the handler thread + * blocks from the start (similarly on Windows, which uses a Semaphore). While + * the handler thread handles a crash, the signal handler (or UEF) blocks itself + * on an ACK pipe/semaphore. Once the handler thread is done processing the + * crash, it will unblock the signal handler which resets the synchronization + * during crash handling and invokes the handler chain. + * + * The most important functions and their meaning: + * + * - `handle_signal`/`handle_exception`: top-level entry points called directly + * from the operating system. They pack sentry_ucontext_t and call... + * - `process_ucontext`: the actual signal-handler/UEF, primarily manages the + * interaction with the OS and other handlers and calls.. + * - `dispatch_ucontext`: this is the place that decides on where to run the + * sentry error event creation and that does the synchronization with... + * - `handler_thread_main`: implements the handler thread loop, blocks until + * unblocked by the signal handler and finally calls... + * - `process_ucontext_deferred`: the implementation of sentry specific + * handler policy leading to crash event construction, it defers to... + * - `make_signal_event`: that is purely about making a crash event object and + * filling the context data + * + * The `on_crash` and `before_send` hook usually run on the handler thread + * during `process_ucontext_deferred` but users cannot rely on any particular + * thread to call their callbacks. However, they can be sure that the crashed + * thread won't run during their the execution of their callback code. + * + * Note on unwinders: + * + * The backend relies on an unwinder that can backtrace from a user context. + * This is important because the unwinder usually runs in the context of the + * handler thread, where a direct backtrace makes no longer any sense (even if + * it was signal safe). We do not dispatch to the handler thread for targets + * that still use `libbacktrace`, and instead run the unsafe part directly in + * the signal handler. This is primarily to not break these target, but in + * general the `libbacktrace`-based unwinder should be considered deprecated. + * + * Notes on signal handling in other runtimes: + * + * The .net runtimes currently rely on signal handling to deliver managed + * exceptions caused from the generated native code. Due to the initialization + * order the inproc backend will receive those signals which it should not + * process. On setups like these it offers a handler strategy that chains the + * previous signal handler first, allowing the .net runtime handler to either + * immediately jump back into runtime code or reset IP/SP so that the returning + * signal handler continues from the managed exception rather then the crashed + * instruction. + * + * The Android runtime (ART) otoh, while also relying heavily on signal handling + * to communicate between generated code and the garbage collector entirely + * shields the signals from us (via `libsigchain` special handler ordering) and + * only forwards signals that are not relevant to runtime. However, it relies on + * each thread having a specific sigaltstack setup, which can lead to crashes if + * overriden. For this reason, we do not set the sigaltstack of any thread if + * one was already configured even if the size is smaller than we'd want. Since + * most of the handler runs in a separate thread the size limitation of any pre- + * configured `sigaltstack` is not a problem to our more complex handler code. + */ +#define SIGNAL_DEF(Sig, Desc) { Sig, #Sig, Desc } #define MAX_FRAMES 128 // the data exchange between the signal handler and the handler thread From 7d6142f4e9d87903831f7b89f0bfb6efd974d714 Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Thu, 20 Nov 2025 12:16:53 +0100 Subject: [PATCH 075/130] fix: return the number of frames when doing a stack walk from an FP directly. --- src/unwinder/sentry_unwinder_libunwind_mac.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/unwinder/sentry_unwinder_libunwind_mac.c b/src/unwinder/sentry_unwinder_libunwind_mac.c index 91ceda367..b9a909828 100644 --- a/src/unwinder/sentry_unwinder_libunwind_mac.c +++ b/src/unwinder/sentry_unwinder_libunwind_mac.c @@ -138,7 +138,7 @@ sentry__unwind_stack_libunwind_mac( if (addr) { size_t n = 0; fp_walk((uintptr_t)addr, &n, ptrs, max_frames); - return 0; + return n; } if (uctx) { From eaa02c276de43cc942b3a5a754cb7d7c027899cc Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Mon, 9 Feb 2026 17:34:14 +0100 Subject: [PATCH 076/130] clean up inline docs --- src/backends/sentry_backend_inproc.c | 90 +++++++++++++++++++++++----- 1 file changed, 76 insertions(+), 14 deletions(-) diff --git a/src/backends/sentry_backend_inproc.c b/src/backends/sentry_backend_inproc.c index ae061ce39..b6ae66e0e 100644 --- a/src/backends/sentry_backend_inproc.c +++ b/src/backends/sentry_backend_inproc.c @@ -42,11 +42,11 @@ * In its current implementation it only gathers the crash context for the * crashed thread and does not attempt to stop any other threads. While this * can be considered a downside for some users, it allows additional handlers - * to process the crashed process again, which the other backends currenlty + * to process the crashed process again, which the other backends currently * can't guarantee to work. Additional crash signals coming from other threads * will be blocked indefinitely until previous handler takes over. * - * The inproc backend splits the handler in two parts: + * The inproc backend splits the handler into two parts: * - a signal handler/unhandled exception filter that severely limits what we * can do, focusing on response to the OS mechanism and almost zero policy. * - a separate handler thread that does most of the typical sentry error @@ -57,7 +57,7 @@ * fallback for report creation. The signal-handler part should not use any * synchronization or signal-unsafe function from `libc` (see function-level * comment), even access to options is ideally done before during - * initailization. If access to option or scope (or any other global context) + * initialization. If access to option or scope (or any other global context) * is required this should happen in the handler thread. * * The handler thread is started during backend initialization and will be @@ -86,7 +86,7 @@ * The `on_crash` and `before_send` hook usually run on the handler thread * during `process_ucontext_deferred` but users cannot rely on any particular * thread to call their callbacks. However, they can be sure that the crashed - * thread won't run during their the execution of their callback code. + * thread won't progress during the execution of their callback code. * * Note on unwinders: * @@ -106,7 +106,7 @@ * process. On setups like these it offers a handler strategy that chains the * previous signal handler first, allowing the .net runtime handler to either * immediately jump back into runtime code or reset IP/SP so that the returning - * signal handler continues from the managed exception rather then the crashed + * signal handler continues from the managed exception rather than the crashed * instruction. * * The Android runtime (ART) otoh, while also relying heavily on signal handling @@ -114,7 +114,7 @@ * shields the signals from us (via `libsigchain` special handler ordering) and * only forwards signals that are not relevant to runtime. However, it relies on * each thread having a specific sigaltstack setup, which can lead to crashes if - * overriden. For this reason, we do not set the sigaltstack of any thread if + * overridden. For this reason, we do not set the sigaltstack of any thread if * one was already configured even if the size is smaller than we'd want. Since * most of the handler runs in a separate thread the size limitation of any pre- * configured `sigaltstack` is not a problem to our more complex handler code. @@ -152,6 +152,16 @@ static volatile long g_handler_thread_ready = 0; static volatile long g_handler_should_exit = 0; // signal handler tells handler thread to start working static volatile long g_handler_has_work = 0; +// blocks pure reentrancy into the inproc signal handler after first crash +// while `sentry__enter_signal_handler` also blocks reentrancy, it specifically +// blocks all our critical sections from entering during the signal handler. +// But +// a. it does this solely for UNIX and +// b. it does not consider that we cannot guarantee to safely run another +// signal handler once the current one "leaves". +// This is trivial cross-platform guard that blocks all follow-up signals routed +// to us indefinitely until the process terminates or the backend shut down. +static volatile long g_signal_reentrancy_block = 0; // trigger/schedule primitives that block the other side until this side is done #ifdef SENTRY_PLATFORM_UNIX @@ -221,6 +231,10 @@ static int startup_inproc_backend( sentry_backend_t *backend, const sentry_options_t *options) { +# ifdef SENTRY_WITH_UNWINDER_LIBBACKTRACE + SENTRY_WARN("Using `backtrace()` for stack traces together with the inproc " + "backend is signal-unsafe. This is a fallback configuration."); +# endif // get option state so we don't need to sync read during signal handling g_backend_config.enable_logging_when_crashed = options ? options->enable_logging_when_crashed : true; @@ -288,9 +302,13 @@ shutdown_inproc_backend(sentry_backend_t *backend) g_signal_stack.ss_sp = NULL; } reset_signal_handlers(); + if (backend) { backend->data = NULL; } + + // allow tests or orderly shutdown to re-arm the backend once unregistered + sentry__atomic_store(&g_signal_reentrancy_block, 0); } #elif defined(SENTRY_PLATFORM_WINDOWS) @@ -363,9 +381,13 @@ shutdown_inproc_backend(sentry_backend_t *backend) if (current_handler != &handle_exception) { SetUnhandledExceptionFilter(current_handler); } + if (backend) { backend->data = NULL; } + + // the inproc handler is now unregistered; re-arm the guard for future use + sentry__atomic_store(&g_signal_reentrancy_block, 0); } #endif @@ -864,10 +886,10 @@ handler_thread_main(void *UNUSED(data)) { sentry__atomic_store(&g_handler_thread_ready, 1); - while (!sentry__atomic_fetch(&g_handler_should_exit)) { + for (;;) { #ifdef SENTRY_PLATFORM_UNIX char command = 0; - ssize_t rv = read(g_handler_pipe[0], &command, 1); + const ssize_t rv = read(g_handler_pipe[0], &command, 1); if (rv == -1 && errno == EINTR) { continue; } @@ -882,12 +904,12 @@ handler_thread_main(void *UNUSED(data)) if (wait_result != WAIT_OBJECT_0) { continue; } - if (sentry__atomic_fetch(&g_handler_should_exit)) { - break; - } #endif if (!sentry__atomic_fetch(&g_handler_has_work)) { + if (sentry__atomic_fetch(&g_handler_should_exit)) { + break; + } continue; } @@ -901,12 +923,25 @@ handler_thread_main(void *UNUSED(data)) do { rv = write(g_handler_ack_pipe[1], &c, 1); } while (rv == -1 && errno == EINTR); + if (rv != 1) { + // TODO: replace with signal-safe log + SENTRY_WARNF( + "failed to write handler ack: %s", strerror(errno)); + close(g_handler_ack_pipe[1]); + g_handler_ack_pipe[1] = -1; + sentry__atomic_store(&g_handler_should_exit, 1); + break; + } } #elif defined(SENTRY_PLATFORM_WINDOWS) if (g_handler_ack_semaphore) { ReleaseSemaphore(g_handler_ack_semaphore, 1, NULL); } #endif + + if (sentry__atomic_fetch(&g_handler_should_exit)) { + break; + } } #ifdef SENTRY_PLATFORM_WINDOWS @@ -1073,12 +1108,39 @@ dispatch_ucontext( process_ucontext_deferred(uctx, sig_slot); return; #else - if (!sentry__atomic_fetch(&g_handler_thread_ready) - || has_handler_thread_crashed()) { + if (has_handler_thread_crashed()) { // directly execute unsafe part in signal handler as a last chance to - // report an error when the handler thread is unavailable. + // report an error when the handler thread has crashed. process_ucontext_deferred(uctx, sig_slot); return; + } else { + // Once a crash is being handled, block any further handler entry until + // the backend is explicitly shut down or the process terminated. This + // avoids other signal handlers from concurrent event generation while + // the handler thread runs, or after we engaged the signal chain. If the + // guard is already set, spin in place to prevent progressing into the + // previous handlers (which could terminate while the first crash is + // still running). + while (sentry__atomic_store(&g_signal_reentrancy_block, 1) != 0) { + sentry__cpu_relax(); + } + // TODO: + // - we could move the entire blocker into the handler thread + // - make g_signal_reentrancy_block more statey where + // - 0 means: not yet handling + // - 1 means: handling in progress + // - 2 means: handling done + // which would allow threads to exit the loop once we are done + // - still check whether the thread that crashed is the signal thread + // - add another counter that prevents from looping on SIGABRT + + // after we can be sure to be the first to handle any signals we can + // also provide a fall-back if the handler thread is not available for + // some reason. + if (!sentry__atomic_fetch(&g_handler_thread_ready)) { + process_ucontext_deferred(uctx, sig_slot); + return; + } } g_handler_state.uctx = *uctx; From 75f44f1ce87b7396d2cd0d71c2b074fdaeb7a616 Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Mon, 9 Feb 2026 17:37:29 +0100 Subject: [PATCH 077/130] replace iOS build inproc for crashpad --- .github/workflows/ci.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 701ad4f55..3de4d187c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,15 +23,15 @@ jobs: - uses: actions/checkout@v4 - run: make style - build-ios-inproc: - name: Xcode Build for inproc on iOS + build-ios-crashpad: + name: Xcode Build for crashpad on iOS runs-on: macos-latest steps: - uses: actions/checkout@v4 with: submodules: "recursive" - run: | - cmake -B sentry-native-xcode -GXcode -DCMAKE_SYSTEM_NAME=iOS -DSENTRY_BACKEND=inproc + cmake -B sentry-native-xcode -GXcode -DCMAKE_SYSTEM_NAME=iOS -DSENTRY_BACKEND=crashpad xcodebuild build -project sentry-native-xcode/Sentry-Native.xcodeproj -sdk iphonesimulator build-ios-breakpad: From d5cae55a7f8047f6469fa2365d86fc804ca0d84f Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Mon, 9 Feb 2026 19:04:02 +0100 Subject: [PATCH 078/130] clean up PAC stripping and actually test it in an arm64e build config --- .github/workflows/ci.yml | 5 +++ src/unwinder/sentry_unwinder_libunwind_mac.c | 44 +++++++++++++------- 2 files changed, 33 insertions(+), 16 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3de4d187c..f356a695a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -167,6 +167,11 @@ jobs: ERROR_ON_WARNINGS: 1 SYSTEM_VERSION_COMPAT: 0 RUN_ANALYZER: asan,llvm-cov + - name: macOS 15 (arm64e + inproc PAC test) + os: macos-15-large + ERROR_ON_WARNINGS: 1 + SYSTEM_VERSION_COMPAT: 0 + CMAKE_DEFINES: -DCMAKE_OSX_ARCHITECTURES=arm64e -DSENTRY_BACKEND=inproc - name: Windows (old VS, 32-bit) os: windows-2022 TEST_X86: 1 diff --git a/src/unwinder/sentry_unwinder_libunwind_mac.c b/src/unwinder/sentry_unwinder_libunwind_mac.c index b9a909828..4543861d1 100644 --- a/src/unwinder/sentry_unwinder_libunwind_mac.c +++ b/src/unwinder/sentry_unwinder_libunwind_mac.c @@ -8,6 +8,17 @@ # include #endif +// On arm64(e), return addresses may have Pointer Authentication Code (PAC) bits +// set in the upper bits. These must be stripped before symbolization. +// The mask 0x7fffffffffff keeps the lower 47 bits which is the actual address. +// See: +// https://developer.apple.com/documentation/security/preparing_your_app_to_work_with_pointer_authentication +#if defined(__arm64__) +# define STRIP_PAC(addr) ((addr) & 0x7fffffffffffull) +#else +# define STRIP_PAC(addr) (addr) +#endif + #if defined(SENTRY_PLATFORM_MACOS) // Basic pointer validation to make sure we stay inside mapped memory. static bool @@ -81,11 +92,12 @@ fp_walk(uintptr_t fp, size_t *n, void **ptrs, size_t max_frames) // x86_64 frame record layout: [prev_rbp, saved_retaddr] at bp and bp+8 const uintptr_t *record = (uintptr_t *)fp; const uintptr_t next_fp = record[0]; - const uintptr_t ret_addr = record[1]; + uintptr_t ret_addr = record[1]; if (!valid_ptr(next_fp) || !ret_addr) { break; } + ret_addr = STRIP_PAC(ret_addr); ptrs[(*n)++] = (void *)(ret_addr - 1); if (next_fp <= fp) { break; // prevent loops @@ -100,9 +112,19 @@ fp_walk_from_uctx(const sentry_ucontext_t *uctx, void **ptrs, size_t max_frames) size_t n = 0; struct __darwin_mcontext64 *mctx = uctx->user_context->uc_mcontext; #if defined(__arm64__) - uintptr_t pc = (uintptr_t)mctx->__ss.__pc; - uintptr_t fp = (uintptr_t)mctx->__ss.__fp; - uintptr_t lr = (uintptr_t)mctx->__ss.__lr; + uintptr_t pc, fp, lr; + +# if defined(__arm64e__) + // arm64e uses opaque accessors that handle PAC authentication + pc = __darwin_arm_thread_state64_get_pc(mctx->__ss); + fp = __darwin_arm_thread_state64_get_fp(mctx->__ss); + lr = __darwin_arm_thread_state64_get_lr(mctx->__ss); +# else + // arm64 can access members directly, strip PAC defensively + pc = STRIP_PAC((uintptr_t)mctx->__ss.__pc); + fp = (uintptr_t)mctx->__ss.__fp; + lr = STRIP_PAC((uintptr_t)mctx->__ss.__lr); +# endif // top frame: adjust pc−1 so it symbolizes inside the function if (pc && n < max_frames) { @@ -164,12 +186,7 @@ sentry__unwind_stack_libunwind_mac( if (n < max_frames) { unw_word_t ip = 0; if (unw_get_reg(&cursor, UNW_REG_IP, &ip) >= 0) { -#if defined(__arm64__) - // Strip pointer authentication, for some reason ptrauth_strip() not - // working - // https://developer.apple.com/documentation/security/preparing_your_app_to_work_with_pointer_authentication - ip &= 0x7fffffffffffull; -#endif + ip = STRIP_PAC(ip); ptrs[n++] = (void *)ip; } else { return 0; @@ -192,12 +209,7 @@ sentry__unwind_stack_libunwind_mac( if (ip == prev_ip && sp == prev_sp) { break; } -#if defined(__arm64__) - // Strip pointer authentication, for some reason ptrauth_strip() not - // working - // https://developer.apple.com/documentation/security/preparing_your_app_to_work_with_pointer_authentication - ip &= 0x7fffffffffffull; -#endif + ip = STRIP_PAC(ip); prev_ip = ip; prev_sp = sp; ptrs[n++] = (void *)ip; From f16a07b23f394c2162cc36f309212211bba1e8a6 Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Mon, 9 Feb 2026 19:27:15 +0100 Subject: [PATCH 079/130] bulk commit of local branches: * introduces state-machine for in-progress crash handling vs other crashing threads * extract async-safe logging macros * adds arm64e support PAC registers (access to opaque fp, lr, sp, and pc) * skip on_crash if we handle a failing handler thread (TODO: also before_send) * handle failed pipe and semaphore signaling to handler thread * introduce even more fall backs to handling inside the signal handler where it makes sense (i.e. failed attempt to signal handler thread) --- src/backends/sentry_backend_inproc.c | 201 ++++++++++++++++++--------- 1 file changed, 137 insertions(+), 64 deletions(-) diff --git a/src/backends/sentry_backend_inproc.c b/src/backends/sentry_backend_inproc.c index b6ae66e0e..9085ba98a 100644 --- a/src/backends/sentry_backend_inproc.c +++ b/src/backends/sentry_backend_inproc.c @@ -24,6 +24,31 @@ #include #include +/** + * Signal/async-safe logging macro for use in signal handlers or other + * contexts where stdio and malloc are unsafe. Only supports static strings. + */ +#ifdef SENTRY_PLATFORM_UNIX +# include +# define SENTRY_SIGNAL_SAFE_LOG(msg) \ + do { \ + static const char _msg[] = "[sentry] " msg "\n"; \ + (void)write(STDERR_FILENO, _msg, sizeof(_msg) - 1); \ + } while (0) +#elif defined(SENTRY_PLATFORM_WINDOWS) +# define SENTRY_SIGNAL_SAFE_LOG(msg) \ + do { \ + static const char _msg[] = "[sentry] " msg "\n"; \ + OutputDebugStringA(_msg); \ + HANDLE _stderr = GetStdHandle(STD_ERROR_HANDLE); \ + if (_stderr && _stderr != INVALID_HANDLE_VALUE) { \ + DWORD _written; \ + WriteFile(_stderr, _msg, (DWORD)(sizeof(_msg) - 1), &_written, \ + NULL); \ + } \ + } while (0) +#endif + /** * Inproc Backend Introduction * @@ -73,7 +98,7 @@ * - `handle_signal`/`handle_exception`: top-level entry points called directly * from the operating system. They pack sentry_ucontext_t and call... * - `process_ucontext`: the actual signal-handler/UEF, primarily manages the - * interaction with the OS and other handlers and calls.. + * interaction with the OS and other handlers and calls... * - `dispatch_ucontext`: this is the place that decides on where to run the * sentry error event creation and that does the synchronization with... * - `handler_thread_main`: implements the handler thread loop, blocks until @@ -152,16 +177,20 @@ static volatile long g_handler_thread_ready = 0; static volatile long g_handler_should_exit = 0; // signal handler tells handler thread to start working static volatile long g_handler_has_work = 0; -// blocks pure reentrancy into the inproc signal handler after first crash -// while `sentry__enter_signal_handler` also blocks reentrancy, it specifically -// blocks all our critical sections from entering during the signal handler. -// But -// a. it does this solely for UNIX and -// b. it does not consider that we cannot guarantee to safely run another -// signal handler once the current one "leaves". -// This is trivial cross-platform guard that blocks all follow-up signals routed -// to us indefinitely until the process terminates or the backend shut down. -static volatile long g_signal_reentrancy_block = 0; +// State machine for crash handling coordination across threads: +// IDLE (0): No crash being handled, ready to accept +// HANDLING (1): A crash is being processed by another thread +// DONE (2): Crash handling complete, signal handlers reset +// +// Threads that crash while state is HANDLING will spin until state becomes +// DONE, then return from their signal handler. Since our signal handlers are +// unregistered before transitioning to DONE, re-executing the crashing +// instruction will invoke the default/previous handler (terminating the +// process) rather than re-entering our handler. +#define CRASH_STATE_IDLE 0 +#define CRASH_STATE_HANDLING 1 +#define CRASH_STATE_DONE 2 +static volatile long g_crash_handling_state = CRASH_STATE_IDLE; // trigger/schedule primitives that block the other side until this side is done #ifdef SENTRY_PLATFORM_UNIX @@ -308,7 +337,7 @@ shutdown_inproc_backend(sentry_backend_t *backend) } // allow tests or orderly shutdown to re-arm the backend once unregistered - sentry__atomic_store(&g_signal_reentrancy_block, 0); + sentry__atomic_store(&g_crash_handling_state, CRASH_STATE_IDLE); } #elif defined(SENTRY_PLATFORM_WINDOWS) @@ -387,7 +416,7 @@ shutdown_inproc_backend(sentry_backend_t *backend) } // the inproc handler is now unregistered; re-arm the guard for future use - sentry__atomic_store(&g_signal_reentrancy_block, 0); + sentry__atomic_store(&g_crash_handling_state, CRASH_STATE_IDLE); } #endif @@ -564,10 +593,26 @@ registers_from_uctx(const sentry_ucontext_t *uctx) SET_REG("x26", __x[26]); SET_REG("x27", __x[27]); SET_REG("x28", __x[28]); +# if defined(__arm64e__) + // arm64e uses opaque accessors for PAC-protected registers + sentry_value_set_by_key(registers, "fp", + sentry__value_new_addr( + (uint64_t)__darwin_arm_thread_state64_get_fp(*thread_state))); + sentry_value_set_by_key(registers, "lr", + sentry__value_new_addr( + (uint64_t)__darwin_arm_thread_state64_get_lr(*thread_state))); + sentry_value_set_by_key(registers, "sp", + sentry__value_new_addr( + (uint64_t)__darwin_arm_thread_state64_get_sp(*thread_state))); + sentry_value_set_by_key(registers, "pc", + sentry__value_new_addr( + (uint64_t)__darwin_arm_thread_state64_get_pc(*thread_state))); +# else SET_REG("fp", __fp); SET_REG("lr", __lr); SET_REG("sp", __sp); SET_REG("pc", __pc); +# endif # elif defined(__arm__) @@ -813,8 +858,8 @@ make_signal_event(const struct signal_slot *sig_slot, * longer progresses and memory can be corrupted. */ static void -process_ucontext_deferred( - const sentry_ucontext_t *uctx, const struct signal_slot *sig_slot) +process_ucontext_deferred(const sentry_ucontext_t *uctx, + const struct signal_slot *sig_slot, bool skip_on_crash) { SENTRY_INFO("entering signal handler"); @@ -828,7 +873,8 @@ process_ucontext_deferred( bool should_handle = true; sentry__write_crash_marker(options); - if (options->on_crash_func) { + // TODO: before_send too + if (options->on_crash_func && !skip_on_crash) { SENTRY_DEBUG("invoking `on_crash` hook"); event = options->on_crash_func(uctx, event, options->on_crash_data); should_handle = !sentry_value_is_null(event); @@ -889,7 +935,7 @@ handler_thread_main(void *UNUSED(data)) for (;;) { #ifdef SENTRY_PLATFORM_UNIX char command = 0; - const ssize_t rv = read(g_handler_pipe[0], &command, 1); + ssize_t rv = read(g_handler_pipe[0], &command, 1); if (rv == -1 && errno == EINTR) { continue; } @@ -914,19 +960,16 @@ handler_thread_main(void *UNUSED(data)) } process_ucontext_deferred( - &g_handler_state.uctx, g_handler_state.sig_slot); + &g_handler_state.uctx, g_handler_state.sig_slot, false); sentry__atomic_store(&g_handler_has_work, 0); #ifdef SENTRY_PLATFORM_UNIX if (g_handler_ack_pipe[1] >= 0) { char c = 1; - ssize_t rv; do { rv = write(g_handler_ack_pipe[1], &c, 1); } while (rv == -1 && errno == EINTR); if (rv != 1) { - // TODO: replace with signal-safe log - SENTRY_WARNF( - "failed to write handler ack: %s", strerror(errno)); + SENTRY_SIGNAL_SAFE_LOG("WARN failed to write handler ack"); close(g_handler_ack_pipe[1]); g_handler_ack_pipe[1] = -1; sentry__atomic_store(&g_handler_should_exit, 1); @@ -1079,19 +1122,11 @@ has_handler_thread_crashed(void) if (sentry__atomic_fetch(&g_handler_thread_ready) && sentry__threadid_equal(current_thread, g_handler_thread)) { #ifdef SENTRY_PLATFORM_UNIX - static const char msg[] = "[sentry] FATAL crash in handler thread, " - "falling back to previous handler\n"; - const ssize_t rv = write(STDERR_FILENO, msg, sizeof(msg) - 1); - (void)rv; + SENTRY_SIGNAL_SAFE_LOG( + "FATAL crash in handler thread, falling back to previous handler"); #else - static const char msg[] = "[sentry] FATAL crash in handler thread, " - "UEF continues search\n"; - OutputDebugStringA(msg); - HANDLE stderr_handle = GetStdHandle(STD_ERROR_HANDLE); - if (stderr_handle && stderr_handle != INVALID_HANDLE_VALUE) { - DWORD written; - WriteFile(stderr_handle, msg, (DWORD)strlen(msg), &written, NULL); - } + SENTRY_SIGNAL_SAFE_LOG( + "FATAL crash in handler thread, UEF continues search"); #endif return true; } @@ -1105,42 +1140,42 @@ dispatch_ucontext( #ifdef SENTRY_WITH_UNWINDER_LIBBACKTRACE // For targets that still use `backtrace()` as the sole unwinder we must // run the signal-unsafe part in the signal handler like we did before. - process_ucontext_deferred(uctx, sig_slot); + process_ucontext_deferred(uctx, sig_slot, false); return; #else if (has_handler_thread_crashed()) { // directly execute unsafe part in signal handler as a last chance to - // report an error when the handler thread has crashed. - process_ucontext_deferred(uctx, sig_slot); + // report an error when the handler thread has crashed. Skip the + // on_crash callback since that's likely what caused the crash. + process_ucontext_deferred(uctx, sig_slot, true); return; - } else { - // Once a crash is being handled, block any further handler entry until - // the backend is explicitly shut down or the process terminated. This - // avoids other signal handlers from concurrent event generation while - // the handler thread runs, or after we engaged the signal chain. If the - // guard is already set, spin in place to prevent progressing into the - // previous handlers (which could terminate while the first crash is - // still running). - while (sentry__atomic_store(&g_signal_reentrancy_block, 1) != 0) { + } + + // Try to become the crash handler. Only one thread can transition + // IDLE -> HANDLING; others will spin until DONE. + if (!sentry__atomic_compare_swap( + &g_crash_handling_state, CRASH_STATE_IDLE, CRASH_STATE_HANDLING)) { + // Another thread is already handling a crash. We need to release the + // signal handler lock before spinning, otherwise the winning thread + // won't be able to re-enter after the handler thread ACKs. +# ifdef SENTRY_PLATFORM_UNIX + sentry__leave_signal_handler(); +# endif + // Spin until they're done. Once state becomes DONE, our signal handlers + // are unregistered, so returning from this handler will re-execute the + // crash instruction and hit the default/previous handler. + while (sentry__atomic_fetch(&g_crash_handling_state) + == CRASH_STATE_HANDLING) { sentry__cpu_relax(); } - // TODO: - // - we could move the entire blocker into the handler thread - // - make g_signal_reentrancy_block more statey where - // - 0 means: not yet handling - // - 1 means: handling in progress - // - 2 means: handling done - // which would allow threads to exit the loop once we are done - // - still check whether the thread that crashed is the signal thread - // - add another counter that prevents from looping on SIGABRT - - // after we can be sure to be the first to handle any signals we can - // also provide a fall-back if the handler thread is not available for - // some reason. - if (!sentry__atomic_fetch(&g_handler_thread_ready)) { - process_ucontext_deferred(uctx, sig_slot); - return; - } + // State is now DONE: just return and let the signal propagate + return; + } + + // We are the first handler. Check if handler thread is available. + if (!sentry__atomic_fetch(&g_handler_thread_ready)) { + process_ucontext_deferred(uctx, sig_slot, false); + return; } g_handler_state.uctx = *uctx; @@ -1171,6 +1206,7 @@ dispatch_ucontext( sentry__atomic_store(&g_handler_has_work, 1); // signal the handler thread to start working + bool handler_signaled = false; # ifdef SENTRY_PLATFORM_UNIX if (g_handler_pipe[1] >= 0) { char c = 1; @@ -1178,10 +1214,36 @@ dispatch_ucontext( do { rv = write(g_handler_pipe[1], &c, 1); } while (rv == -1 && errno == EINTR); + + if (rv == 1) { + handler_signaled = true; + } else { + // Write failed (EPIPE, etc.) - handler thread may be dead + SENTRY_SIGNAL_SAFE_LOG( + "WARN failed to signal handler thread, processing in-handler"); + } + } + + if (!handler_signaled) { + // Fall back to in-handler processing + sentry__enter_signal_handler(); + process_ucontext_deferred(uctx, sig_slot, false); + return; } # elif defined(SENTRY_PLATFORM_WINDOWS) if (g_handler_semaphore) { - ReleaseSemaphore(g_handler_semaphore, 1, NULL); + if (ReleaseSemaphore(g_handler_semaphore, 1, NULL)) { + handler_signaled = true; + } else { + SENTRY_SIGNAL_SAFE_LOG( + "WARN failed to signal handler thread, processing in-handler"); + } + } + + if (!handler_signaled) { + // Fall back to in-handler processing + process_ucontext_deferred(uctx, sig_slot, false); + return; } # endif @@ -1301,12 +1363,23 @@ process_ucontext(const sentry_ucontext_t *uctx) // which recovers the process but this will cause a memory leak going // forward as we're not restoring the page allocator. reset_signal_handlers(); + + // Signal to any other threads spinning in dispatch_ucontext that we're + // done. They can now return from their signal handlers. Since our handlers + // are unregistered, re-executing their crash will hit the default handler. + sentry__atomic_store(&g_crash_handling_state, CRASH_STATE_DONE); + sentry__leave_signal_handler(); if (g_backend_config.handler_strategy != SENTRY_HANDLER_STRATEGY_CHAIN_AT_START) { invoke_signal_handler( uctx->signum, uctx->siginfo, (void *)uctx->user_context); } +#elif defined(SENTRY_PLATFORM_WINDOWS) + // Signal to any other threads spinning in dispatch_ucontext that we're + // done. They can now return EXCEPTION_CONTINUE_SEARCH from their UEF, + // allowing the exception to propagate and terminate the process. + sentry__atomic_store(&g_crash_handling_state, CRASH_STATE_DONE); #endif } From cb00bc434c57c8874b5da7a5128a51ab5e27da21 Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Mon, 9 Feb 2026 19:32:38 +0100 Subject: [PATCH 080/130] gate UB read in the mac libunwind stack walker --- src/unwinder/sentry_unwinder_libunwind_mac.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/unwinder/sentry_unwinder_libunwind_mac.c b/src/unwinder/sentry_unwinder_libunwind_mac.c index 4543861d1..becf59b0a 100644 --- a/src/unwinder/sentry_unwinder_libunwind_mac.c +++ b/src/unwinder/sentry_unwinder_libunwind_mac.c @@ -193,7 +193,7 @@ sentry__unwind_stack_libunwind_mac( } } // walk the callers - unw_word_t prev_ip = (uintptr_t)ptrs[0]; + unw_word_t prev_ip = n > 0 ? (uintptr_t)ptrs[0] : 0; unw_word_t prev_sp = 0; (void)unw_get_reg(&cursor, UNW_REG_SP, &prev_sp); while (n < max_frames && unw_step(&cursor) > 0) { From c2f2006947b6455d1ac3f9e0aed685b2f4db9647 Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Mon, 9 Feb 2026 19:36:04 +0100 Subject: [PATCH 081/130] after switching to nothrow new in the crashpad database clean up we actually have to check return values. --- src/backends/sentry_backend_crashpad.cpp | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/backends/sentry_backend_crashpad.cpp b/src/backends/sentry_backend_crashpad.cpp index 9a55564f3..7cfa36bc7 100644 --- a/src/backends/sentry_backend_crashpad.cpp +++ b/src/backends/sentry_backend_crashpad.cpp @@ -752,9 +752,16 @@ crashpad_backend_prune_database(sentry_backend_t *backend) // an embedded use-case, but minidumps on desktop can sometimes be quite // large. data->db->CleanDatabase(60 * 60 * 24 * 2); - crashpad::BinaryPruneCondition condition(crashpad::BinaryPruneCondition::OR, - new (std::nothrow) crashpad::DatabaseSizePruneCondition(1024 * 8), - new (std::nothrow) crashpad::AgePruneCondition(2)); + auto *size_condition + = new (std::nothrow) crashpad::DatabaseSizePruneCondition(1024 * 8); + auto *age_condition = new (std::nothrow) crashpad::AgePruneCondition(2); + if (!size_condition || !age_condition) { + delete size_condition; + delete age_condition; + return; + } + crashpad::BinaryPruneCondition condition( + crashpad::BinaryPruneCondition::OR, size_condition, age_condition); crashpad::PruneCrashReportDatabase(data->db, &condition); } From 3a4c0857833b4e02bb250334b9457b4e0b5d5509 Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Mon, 9 Feb 2026 19:43:16 +0100 Subject: [PATCH 082/130] revert ci build for ios back to inproc (no clue why this was switched) --- .github/workflows/ci.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f356a695a..0b8950f65 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,15 +23,15 @@ jobs: - uses: actions/checkout@v4 - run: make style - build-ios-crashpad: - name: Xcode Build for crashpad on iOS + build-ios-inproc: + name: Xcode Build for inproc on iOS runs-on: macos-latest steps: - uses: actions/checkout@v4 with: submodules: "recursive" - run: | - cmake -B sentry-native-xcode -GXcode -DCMAKE_SYSTEM_NAME=iOS -DSENTRY_BACKEND=crashpad + cmake -B sentry-native-xcode -GXcode -DCMAKE_SYSTEM_NAME=iOS -DSENTRY_BACKEND=inproc xcodebuild build -project sentry-native-xcode/Sentry-Native.xcodeproj -sdk iphonesimulator build-ios-breakpad: From e36c68f98933a9ebc34c6074af44432e78e4a762 Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Mon, 9 Feb 2026 20:23:05 +0100 Subject: [PATCH 083/130] remove stupid copypasta from ci workflow --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0b8950f65..d0bd15edf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -167,11 +167,11 @@ jobs: ERROR_ON_WARNINGS: 1 SYSTEM_VERSION_COMPAT: 0 RUN_ANALYZER: asan,llvm-cov - - name: macOS 15 (arm64e + inproc PAC test) + - name: macOS 15 (arm64e + PAC test) os: macos-15-large ERROR_ON_WARNINGS: 1 SYSTEM_VERSION_COMPAT: 0 - CMAKE_DEFINES: -DCMAKE_OSX_ARCHITECTURES=arm64e -DSENTRY_BACKEND=inproc + CMAKE_DEFINES: -DCMAKE_OSX_ARCHITECTURES=arm64e - name: Windows (old VS, 32-bit) os: windows-2022 TEST_X86: 1 From 8114149058a230fe5c95aa39a652c54594112d92 Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Tue, 10 Feb 2026 12:36:59 +0100 Subject: [PATCH 084/130] exclude breakpad from the arm64e tests on macOS --- tests/conditions.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/conditions.py b/tests/conditions.py index 5af7757fb..ebfdf82c3 100644 --- a/tests/conditions.py +++ b/tests/conditions.py @@ -8,6 +8,7 @@ is_tsan = "tsan" in os.environ.get("RUN_ANALYZER", "") is_kcov = "kcov" in os.environ.get("RUN_ANALYZER", "") is_valgrind = "valgrind" in os.environ.get("RUN_ANALYZER", "") +is_arm64e = "CMAKE_OSX_ARCHITECTURES=arm64e" in os.environ.get("CMAKE_DEFINES", "") has_http = not is_android and not (sys.platform == "linux" and is_x86) # breakpad does not work correctly when using kcov or valgrind @@ -22,6 +23,8 @@ # however running it from an `adb shell` does not work correctly :-( and not is_android and not (is_asan and sys.platform == "darwin") + # breakpad accesses thread state registers directly, which doesn't work on arm64e + and not (is_arm64e and sys.platform == "darwin") ) # crashpad requires http, needs porting to AIX, and doesn’t work with kcov/valgrind/tsan either has_crashpad = ( From 8d2c4d903d9578fc14342b7184c3ce0cdb5afea3 Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Tue, 10 Feb 2026 12:38:15 +0100 Subject: [PATCH 085/130] strip PAC also from unit-test function pointers --- src/backends/sentry_backend_inproc.c | 1 - tests/unit/test_symbolizer.c | 18 +++++++++++++++--- tests/unit/test_unwinder.c | 12 +++++++++++- 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/src/backends/sentry_backend_inproc.c b/src/backends/sentry_backend_inproc.c index 9085ba98a..d5c1f2393 100644 --- a/src/backends/sentry_backend_inproc.c +++ b/src/backends/sentry_backend_inproc.c @@ -594,7 +594,6 @@ registers_from_uctx(const sentry_ucontext_t *uctx) SET_REG("x27", __x[27]); SET_REG("x28", __x[28]); # if defined(__arm64e__) - // arm64e uses opaque accessors for PAC-protected registers sentry_value_set_by_key(registers, "fp", sentry__value_new_addr( (uint64_t)__darwin_arm_thread_state64_get_fp(*thread_state))); diff --git a/tests/unit/test_symbolizer.c b/tests/unit/test_symbolizer.c index 436eb4c46..292b14d96 100644 --- a/tests/unit/test_symbolizer.c +++ b/tests/unit/test_symbolizer.c @@ -1,6 +1,16 @@ #include "sentry_symbolizer.h" #include "sentry_testsupport.h" +// On arm64e, function pointers have PAC (Pointer Authentication Code) bits +// that must be stripped for comparison with addresses from dladdr(). +#if defined(__arm64e__) +# include +# define STRIP_PAC_FROM_FPTR(fptr) \ + ptrauth_strip(fptr, ptrauth_key_function_pointer) +#else +# define STRIP_PAC_FROM_FPTR(fptr) (fptr) +#endif + TEST_VISIBLE void test_function(void) { @@ -22,8 +32,9 @@ asserter(const sentry_frame_info_t *info, void *data) TEST_CHECK( info->instruction_addr == ((char *)*(void **)&test_function) + 1); #else - TEST_CHECK(info->symbol_addr == &test_function); - TEST_CHECK(info->instruction_addr == ((char *)(void *)&test_function) + 1); + void *expected_addr = STRIP_PAC_FROM_FPTR((void *)&test_function); + TEST_CHECK(info->symbol_addr == expected_addr); + TEST_CHECK(info->instruction_addr == ((char *)expected_addr) + 1); #endif *called += 1; } @@ -39,7 +50,8 @@ SENTRY_TEST(symbolizer) sentry__symbolize( ((char *)*(void **)&test_function) + 1, asserter, &called); #else - sentry__symbolize(((char *)(void *)&test_function) + 1, asserter, &called); + void *func_addr = STRIP_PAC_FROM_FPTR((void *)&test_function); + sentry__symbolize(((char *)func_addr) + 1, asserter, &called); #endif TEST_CHECK_INT_EQUAL(called, 1); } diff --git a/tests/unit/test_unwinder.c b/tests/unit/test_unwinder.c index 70568b94d..2dfb201c4 100644 --- a/tests/unit/test_unwinder.c +++ b/tests/unit/test_unwinder.c @@ -2,6 +2,16 @@ #include "sentry_symbolizer.h" #include "sentry_testsupport.h" +// On arm64e, function pointers have PAC (Pointer Authentication Code) bits +// that must be stripped for comparison with addresses from dladdr(). +#if defined(__arm64e__) +# include +# define STRIP_PAC_FROM_FPTR(fptr) \ + ptrauth_strip(fptr, ptrauth_key_function_pointer) +#else +# define STRIP_PAC_FROM_FPTR(fptr) (fptr) +#endif + #define MAX_FRAMES 128 TEST_VISIBLE size_t @@ -29,7 +39,7 @@ find_frame(const sentry_frame_info_t *info, void *data) // XXX: Should apply on _CALL_ELF == 1 when on PowerPC i.e. Linux *(void **)&invoke_unwinder; #else - &invoke_unwinder; + STRIP_PAC_FROM_FPTR((void *)&invoke_unwinder); #endif if (info->symbol_addr == unwinder_address) { *found_frame += 1; From 1b58c5f599b6aee4bf41112d62e768a4add63594 Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Tue, 10 Feb 2026 12:42:02 +0100 Subject: [PATCH 086/130] add inproc concurrent crash stress test --- tests/fixtures/inproc_stress/CMakeLists.txt | 21 ++ .../fixtures/inproc_stress/concurrent_crash.c | 147 +++++++++++ tests/fixtures/inproc_stress/main.c | 186 ++++++++++++++ tests/test_inproc_stress.py | 237 ++++++++++++++++++ 4 files changed, 591 insertions(+) create mode 100644 tests/fixtures/inproc_stress/CMakeLists.txt create mode 100644 tests/fixtures/inproc_stress/concurrent_crash.c create mode 100644 tests/fixtures/inproc_stress/main.c create mode 100644 tests/test_inproc_stress.py diff --git a/tests/fixtures/inproc_stress/CMakeLists.txt b/tests/fixtures/inproc_stress/CMakeLists.txt new file mode 100644 index 000000000..545b26938 --- /dev/null +++ b/tests/fixtures/inproc_stress/CMakeLists.txt @@ -0,0 +1,21 @@ +cmake_minimum_required(VERSION 3.10) +project(inproc_stress_test C) + +# SENTRY_LIB_DIR and SENTRY_INCLUDE_DIR are passed from the pytest +add_executable(inproc_stress_test + main.c + concurrent_crash.c +) + +target_include_directories(inproc_stress_test PRIVATE ${SENTRY_INCLUDE_DIR}) +target_link_directories(inproc_stress_test PRIVATE ${SENTRY_LIB_DIR}) +target_link_libraries(inproc_stress_test PRIVATE sentry) + +set_target_properties(inproc_stress_test PROPERTIES + BUILD_RPATH "${SENTRY_LIB_DIR}" +) + +if(NOT MSVC) + target_compile_options(inproc_stress_test PRIVATE -O0 -g) + target_link_libraries(inproc_stress_test PRIVATE pthread) +endif() diff --git a/tests/fixtures/inproc_stress/concurrent_crash.c b/tests/fixtures/inproc_stress/concurrent_crash.c new file mode 100644 index 000000000..c11af2c33 --- /dev/null +++ b/tests/fixtures/inproc_stress/concurrent_crash.c @@ -0,0 +1,147 @@ +/** + * Test fixture for stressing the inproc backend with concurrent crashes. + * + * This test spawns multiple threads that all crash concurrently to expose + * race conditions in the signal handler / handler thread synchronization. + */ + +#include +#include +#include + +#ifndef _WIN32 +# include +# include +# define sleep_ms(ms) usleep((ms) * 1000) +#else +# define WIN32_LEAN_AND_MEAN +# include +# define sleep_ms(ms) Sleep(ms) +#endif + +#define CRASH_THREADS 20 + +static void *invalid_mem = (void *)1; + +// Barrier for synchronizing threads +#ifndef _WIN32 +static volatile int g_barrier = 0; +static volatile int g_ready_count = 0; +#else +static volatile LONG g_barrier = 0; +static volatile LONG g_ready_count = 0; +#endif + +__attribute__((noinline)) void +do_crash(void) +{ + memset((char *)invalid_mem, 1, 100); +} + +static void +wait_at_barrier(void) +{ +#ifndef _WIN32 + __atomic_add_fetch(&g_ready_count, 1, __ATOMIC_SEQ_CST); + while (__atomic_load_n(&g_barrier, __ATOMIC_SEQ_CST) == 0) { + // spin + } +#else + InterlockedIncrement(&g_ready_count); + while (InterlockedCompareExchange(&g_barrier, 1, 1) == 0) { + // spin + } +#endif +} + +static void +release_barrier(void) +{ +#ifndef _WIN32 + __atomic_store_n(&g_barrier, 1, __ATOMIC_SEQ_CST); +#else + InterlockedExchange(&g_barrier, 1); +#endif +} + +static int +all_threads_ready(void) +{ +#ifndef _WIN32 + return __atomic_load_n(&g_ready_count, __ATOMIC_SEQ_CST) == CRASH_THREADS; +#else + return InterlockedCompareExchange( + &g_ready_count, CRASH_THREADS, CRASH_THREADS) + == CRASH_THREADS; +#endif +} + +#ifndef _WIN32 +static void * +crash_thread(void *param) +{ + (void)param; + wait_at_barrier(); + do_crash(); + return NULL; +} + +void +run_concurrent_crash(void) +{ + pthread_t threads[CRASH_THREADS]; + + for (int i = 0; i < CRASH_THREADS; i++) { + if (pthread_create(&threads[i], NULL, crash_thread, NULL) != 0) { + fprintf(stderr, "Failed to create thread %d\n", i); + exit(1); + } + } + + while (!all_threads_ready()) { + sleep_ms(1); + } + sleep_ms(10); + + release_barrier(); + + for (int i = 0; i < CRASH_THREADS; i++) { + pthread_join(threads[i], NULL); + } +} +#else +static DWORD WINAPI +crash_thread(LPVOID param) +{ + (void)param; + wait_at_barrier(); + do_crash(); + return 0; +} + +void +run_concurrent_crash(void) +{ + HANDLE threads[CRASH_THREADS]; + + for (int i = 0; i < CRASH_THREADS; i++) { + threads[i] = CreateThread(NULL, 0, crash_thread, NULL, 0, NULL); + if (!threads[i]) { + fprintf(stderr, "Failed to create thread %d\n", i); + exit(1); + } + } + + while (!all_threads_ready()) { + sleep_ms(1); + } + sleep_ms(10); + + release_barrier(); + + WaitForMultipleObjects(CRASH_THREADS, threads, TRUE, 5000); + for (int i = 0; i < CRASH_THREADS; i++) { + CloseHandle(threads[i]); + } +} +#endif diff --git a/tests/fixtures/inproc_stress/main.c b/tests/fixtures/inproc_stress/main.c new file mode 100644 index 000000000..9b842c49a --- /dev/null +++ b/tests/fixtures/inproc_stress/main.c @@ -0,0 +1,186 @@ +/** + * Main test driver for inproc backend stress tests. + * + * Usage: + * ./inproc_stress_test concurrent-crash + * ./inproc_stress_test handler-thread-crash + * ./inproc_stress_test simple-crash + * ./inproc_stress_test pipe-failure + */ + +#include "sentry.h" +#include +#include +#include +#include + +#ifndef _WIN32 +# include +# include +#endif + +// From concurrent_crash.c +extern void run_concurrent_crash(void); + +static void *invalid_mem = (void *)1; + +// on_crash callback that crashes - simulates buggy user code +static sentry_value_t +crashing_on_crash_callback( + const sentry_ucontext_t *uctx, sentry_value_t event, void *closure) +{ + (void)uctx; + (void)event; + (void)closure; + + fprintf(stderr, "on_crash callback about to crash\n"); + fflush(stderr); + + memset((char *)invalid_mem, 1, 100); + + return event; +} + +static void +print_envelope(sentry_envelope_t *envelope, void *unused_state) +{ + (void)unused_state; + size_t size_out = 0; + char *s = sentry_envelope_serialize(envelope, &size_out); + printf("%s", s); + fflush(stdout); + sentry_free(s); + sentry_envelope_free(envelope); +} + +static int +setup_sentry(const char *database_path) +{ + sentry_options_t *options = sentry_options_new(); + sentry_options_set_database_path(options, database_path); + sentry_options_set_auto_session_tracking(options, false); + sentry_options_set_dsn(options, "https://public@sentry.invalid/1"); + sentry_options_set_debug(options, 1); + sentry_options_set_transport(options, sentry_transport_new(print_envelope)); + + if (sentry_init(options) != 0) { + fprintf(stderr, "Failed to initialize sentry\n"); + return 1; + } + return 0; +} + +static int +test_concurrent_crash(const char *database_path) +{ + if (setup_sentry(database_path) != 0) { + return 1; + } + + sentry_set_tag("test", "concurrent-crash"); + + fprintf(stderr, "Starting concurrent crash test\n"); + fflush(stderr); + + run_concurrent_crash(); + + // Should not reach this + fprintf(stderr, "ERROR: Threads returned without crashing\n"); + sentry_close(); + return 1; +} + +static void +trigger_crash(void) +{ + memset((char *)invalid_mem, 1, 100); +} + +static int +setup_sentry_with_crashing_on_crash(const char *database_path) +{ + sentry_options_t *options = sentry_options_new(); + sentry_options_set_database_path(options, database_path); + sentry_options_set_auto_session_tracking(options, false); + sentry_options_set_dsn(options, "https://public@sentry.invalid/1"); + sentry_options_set_debug(options, 1); + sentry_options_set_transport(options, sentry_transport_new(print_envelope)); + + // Set the crashing on_crash callback + sentry_options_set_on_crash(options, crashing_on_crash_callback, NULL); + + if (sentry_init(options) != 0) { + fprintf(stderr, "Failed to initialize sentry\n"); + return 1; + } + return 0; +} + +static int +test_handler_thread_crash(const char *database_path) +{ + if (setup_sentry_with_crashing_on_crash(database_path) != 0) { + return 1; + } + + sentry_set_tag("test", "handler-thread-crash"); + + fprintf(stderr, "Starting handler thread crash test\n"); + fflush(stderr); + + // This will crash, trigger the handler thread, which will call + // on_crash callback, which will crash the handler thread. + // The fallback should then process in the signal handler. + trigger_crash(); + + fprintf(stderr, "ERROR: Should have crashed\n"); + sentry_close(); + return 1; +} + +static int +test_simple_crash(const char *database_path) +{ + if (setup_sentry(database_path) != 0) { + return 1; + } + + sentry_set_tag("test", "simple-crash"); + + fprintf(stderr, "Starting simple crash test\n"); + fflush(stderr); + + trigger_crash(); + + fprintf(stderr, "ERROR: Should have crashed\n"); + sentry_close(); + return 1; +} + +int +main(int argc, char **argv) +{ + if (argc < 2) { + fprintf(stderr, "Usage: %s [database-path]\n", argv[0]); + fprintf(stderr, "Tests:\n"); + fprintf(stderr, " concurrent-crash - Multiple threads crash simultaneously\n"); + fprintf(stderr, " simple-crash - Single thread crash (baseline)\n"); + fprintf(stderr, " handler-thread-crash - Handler thread crashes in on_crash\n"); + return 1; + } + + const char *test_name = argv[1]; + const char *database_path = argc > 2 ? argv[2] : ".sentry-native"; + + if (strcmp(test_name, "concurrent-crash") == 0) { + return test_concurrent_crash(database_path); + } + if (strcmp(test_name, "simple-crash") == 0) { + return test_simple_crash(database_path); + } + if (strcmp(test_name, "handler-thread-crash") == 0) { + return test_handler_thread_crash(database_path); + } + fprintf(stderr, "Unknown test: %s\n", test_name); + return 1; +} diff --git a/tests/test_inproc_stress.py b/tests/test_inproc_stress.py new file mode 100644 index 000000000..c48cff527 --- /dev/null +++ b/tests/test_inproc_stress.py @@ -0,0 +1,237 @@ +import os +import pathlib +import shutil +import subprocess +import sys + +import pytest + +from . import Envelope +from .assertions import assert_inproc_crash + +fixture_path = pathlib.Path("tests/fixtures/inproc_stress") + + +def compile_test_program(tmp_path): + build_dir = tmp_path / "inproc_stress_build" + build_dir.mkdir(exist_ok=True) + + source_dir = pathlib.Path(__file__).parent.parent.resolve() + include_dir = source_dir / "include" + + subprocess.run( + [ + "cmake", + f"-DSENTRY_LIB_DIR={tmp_path}", + f"-DSENTRY_INCLUDE_DIR={include_dir}", + str(fixture_path.resolve()), + ], + check=True, + cwd=build_dir, + ) + + subprocess.run( + ["cmake", "--build", ".", "--parallel"], + check=True, + cwd=build_dir, + ) + + if sys.platform == "win32": + return build_dir / "inproc_stress_test.exe" + else: + return build_dir / "inproc_stress_test" + + +def run_stress_test(tmp_path, test_executable, test_name, database_path=None): + if database_path is None: + database_path = tmp_path / ".sentry-native" + + env = os.environ.copy() + if sys.platform != "win32": + env["LD_LIBRARY_PATH"] = str(tmp_path) + ":" + env.get("LD_LIBRARY_PATH", "") + + proc = subprocess.Popen( + [str(test_executable), test_name, str(database_path)], + cwd=tmp_path, + env=env, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + ) + + stdout, stderr = proc.communicate(timeout=30) + return proc.returncode, stdout, stderr + + +def assert_single_crash_envelope(database_path): + run_dirs = list(database_path.glob("*.run")) + assert len(run_dirs) == 1, f"Expected 1 .run directory, found {len(run_dirs)}" + + run_dir = run_dirs[0] + envelopes = list(run_dir.glob("*.envelope")) + assert ( + len(envelopes) == 1 + ), f"Expected 1 envelope, found {len(envelopes)}: {envelopes}" + + return envelopes[0] + + +def assert_crash_marker(database_path): + assert (database_path / "last_crash").exists(), "Crash marker file missing" + + +def test_inproc_simple_crash(cmake): + tmp_path = cmake( + ["sentry"], + {"SENTRY_BACKEND": "inproc", "SENTRY_TRANSPORT": "none"}, + ) + + test_exe = compile_test_program(tmp_path) + database_path = tmp_path / ".sentry-native" + + try: + returncode, stdout, stderr = run_stress_test( + tmp_path, test_exe, "simple-crash", database_path + ) + + assert returncode != 0, f"Process should have crashed. stderr:\n{stderr}" + + assert ( + "crash has been captured" in stderr + ), f"Crash not captured. stderr:\n{stderr}" + + assert_crash_marker(database_path) + envelope_path = assert_single_crash_envelope(database_path) + + with open(envelope_path, "rb") as f: + envelope = Envelope.deserialize(f.read()) + assert_inproc_crash(envelope) + + event = envelope.get_event() + assert event is not None, "Event missing from envelope" + assert "exception" in event, "Exception missing from event" + + finally: + shutil.rmtree(database_path, ignore_errors=True) + + +def test_inproc_concurrent_crash(cmake): + """ + Stress test: multiple threads crash simultaneously. + + This test verifies that: + 1. Only ONE crash envelope is generated (not one per thread) + 2. The envelope is not corrupted by concurrent access + 3. The process terminates cleanly after handling the first crash + """ + tmp_path = cmake( + ["sentry"], + {"SENTRY_BACKEND": "inproc", "SENTRY_TRANSPORT": "none"}, + ) + + test_exe = compile_test_program(tmp_path) + database_path = tmp_path / ".sentry-native" + + try: + returncode, stdout, stderr = run_stress_test( + tmp_path, test_exe, "concurrent-crash", database_path + ) + + assert returncode != 0, f"Process should have crashed. stderr:\n{stderr}" + assert ( + "crash has been captured" in stderr + ), f"Crash not captured. stderr:\n{stderr}" + + assert_crash_marker(database_path) + envelope_path = assert_single_crash_envelope(database_path) + + with open(envelope_path, "rb") as f: + content = f.read() + envelope = Envelope.deserialize(content) + + assert_inproc_crash(envelope) + + event = envelope.get_event() + assert event is not None, "Event missing from envelope" + assert "exception" in event, "Exception missing from event" + + exc = event["exception"]["values"][0] + assert "type" in exc, "Exception type missing" + assert "stacktrace" in exc, "Stacktrace missing from exception" + + finally: + shutil.rmtree(database_path, ignore_errors=True) + + +def test_inproc_handler_thread_crash(cmake): + """ + Test fallback when handler thread crashes. + + This test verifies that when the on_crash callback crashes (which runs + on the handler thread), the signal handler detects this and falls back + to processing the crash directly in the signal handler context. + """ + tmp_path = cmake( + ["sentry"], + {"SENTRY_BACKEND": "inproc", "SENTRY_TRANSPORT": "none"}, + ) + + test_exe = compile_test_program(tmp_path) + database_path = tmp_path / ".sentry-native" + + try: + returncode, stdout, stderr = run_stress_test( + tmp_path, test_exe, "handler-thread-crash", database_path + ) + + assert returncode != 0, f"Process should have crashed. stderr:\n{stderr}" + + assert ( + "on_crash callback about to crash" in stderr + ), f"on_crash callback didn't run. stderr:\n{stderr}" + + assert ( + "FATAL crash in handler thread" in stderr + ), f"Handler thread crash not detected. stderr:\n{stderr}" + + assert ( + "crash has been captured" in stderr + ), f"Crash not captured via fallback. stderr:\n{stderr}" + + assert_crash_marker(database_path) + envelope_path = assert_single_crash_envelope(database_path) + with open(envelope_path, "rb") as f: + envelope = Envelope.deserialize(f.read()) + assert_inproc_crash(envelope) + + finally: + shutil.rmtree(database_path, ignore_errors=True) + + +@pytest.mark.parametrize("iteration", range(5)) +def test_inproc_concurrent_crash_repeated(cmake, iteration): + tmp_path = cmake( + ["sentry"], + {"SENTRY_BACKEND": "inproc", "SENTRY_TRANSPORT": "none"}, + ) + + test_exe = compile_test_program(tmp_path) + database_path = tmp_path / f".sentry-native-{iteration}" + + try: + returncode, stdout, stderr = run_stress_test( + tmp_path, test_exe, "concurrent-crash", database_path + ) + + assert returncode != 0, f"Iteration {iteration}: Process should have crashed" + assert ( + "crash has been captured" in stderr + ), f"Iteration {iteration}: Crash not captured" + + envelope_path = assert_single_crash_envelope(database_path) + with open(envelope_path, "rb") as f: + envelope = Envelope.deserialize(f.read()) + assert_inproc_crash(envelope) + + finally: + shutil.rmtree(database_path, ignore_errors=True) From 82e94e4ba85bf3cfc8ad8fa1b4e24d80e9e6b434 Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Tue, 10 Feb 2026 13:24:12 +0100 Subject: [PATCH 087/130] split out build_config.py from cmake.py so we can build and run new integration test executables with the same parametrization as the core artifacts and "example". --- tests/build_config.py | 204 ++++++++++++++++++++ tests/cmake.py | 62 ++---- tests/fixtures/inproc_stress/CMakeLists.txt | 21 ++ tests/test_inproc_stress.py | 15 +- 4 files changed, 251 insertions(+), 51 deletions(-) create mode 100644 tests/build_config.py diff --git a/tests/build_config.py b/tests/build_config.py new file mode 100644 index 000000000..4c21a4fdd --- /dev/null +++ b/tests/build_config.py @@ -0,0 +1,204 @@ +""" +Build configuration utilities for sentry-native tests. + +This module provides shared logic for reading environment variables and +preparing CMake build parameters, used by both cmake.py (for building +example, library and backend artifacts) and individual tests that need +to build additional executables (like the inproc stress test) but still +need to align with the core artifacts. + +This should make it much easier to add additional integration tests that +we no longer want to bloat example.c with. +""" + +import os +import shutil +import sys +from pathlib import Path + + +def get_android_config(): + """ + Returns Android toolchain CMake options if ANDROID_API and ANDROID_NDK + environment variables are set. + """ + if not (os.environ.get("ANDROID_API") and os.environ.get("ANDROID_NDK")): + return {} + + toolchain = "{}/ndk/{}/build/cmake/android.toolchain.cmake".format( + os.environ["ANDROID_HOME"], os.environ["ANDROID_NDK"] + ) + return { + "CMAKE_TOOLCHAIN_FILE": toolchain, + "ANDROID_ABI": os.environ.get("ANDROID_ARCH") or "x86", + "ANDROID_NATIVE_API_LEVEL": os.environ["ANDROID_API"], + } + + +def get_platform_cmake_args(): + """ + Returns a list of CMake command-line arguments based on platform and + environment configuration. + + This handles: + - 32-bit builds (TEST_X86) + - ASAN/TSAN sanitizers (RUN_ANALYZER) + - Additional CMAKE_DEFINES from environment + """ + args = [] + + if sys.platform == "win32" and os.environ.get("TEST_X86"): + args.append("-AWin32") + elif sys.platform == "linux" and os.environ.get("TEST_X86"): + args.append("-DSENTRY_BUILD_FORCE32=ON") + + if "asan" in os.environ.get("RUN_ANALYZER", ""): + args.append("-DWITH_ASAN_OPTION=ON") + + if "tsan" in os.environ.get("RUN_ANALYZER", ""): + args.append("-DWITH_TSAN_OPTION=ON") + + if "CMAKE_DEFINES" in os.environ: + args.extend(os.environ.get("CMAKE_DEFINES").split()) + + return args + + +def get_cflags(extra_cflags=None): + """ + Returns compiler flags (CFLAGS/CXXFLAGS) for the build. + + This handles: + - ERROR_ON_WARNINGS -> -Werror + - MSVC parallel builds and warnings as errors + - GCC analyzer + - llvm-cov coverage flags + + Args: + extra_cflags: Additional flags to include + """ + cflags = list(extra_cflags) if extra_cflags else [] + + if os.environ.get("ERROR_ON_WARNINGS"): + cflags.append("-Werror") + + if sys.platform == "win32" and not os.environ.get("TEST_MINGW"): + cpus = os.cpu_count() + cflags.append("/WX /MP{}".format(cpus)) + + if "gcc" in os.environ.get("RUN_ANALYZER", ""): + cflags.append("-fanalyzer") + + if "llvm-cov" in os.environ.get("RUN_ANALYZER", ""): + cflags.extend(_get_llvm_cov_flags()) + + return cflags + + +def _get_llvm_cov_flags(): + """Returns compiler flags for llvm-cov coverage.""" + flags = ["-fprofile-instr-generate", "-fcoverage-mapping"] + + # Handle macOS with llvm@15 from homebrew + if ( + sys.platform == "darwin" + and os.environ.get("CC", "") == "clang" + and shutil.which("clang") == "/usr/local/opt/llvm@15/bin/clang" + ): + flags.extend( + [ + "-L/usr/local/opt/llvm@15/lib/c++", + "-fexperimental-library", + "-Wno-unused-command-line-argument", + ] + ) + + return flags + + +def get_tsan_env(): + """ + Returns TSAN_OPTIONS environment variable value if TSAN is enabled. + + Returns None if TSAN is not enabled. + """ + if "tsan" not in os.environ.get("RUN_ANALYZER", ""): + return None + + module_dir = Path(__file__).resolve().parent + tsan_options = { + "verbosity": 2, + "detect_deadlocks": 1, + "second_deadlock_stack": 1, + "suppressions": module_dir / "tsan.supp", + } + return ":".join(f"{key}={value}" for key, value in tsan_options.items()) + + +def get_test_executable_cmake_args(sentry_lib_dir, sentry_include_dir): + """ + Returns CMake arguments for building a test executable that links against + libsentry, with all necessary platform/sanitizer flags. + + Args: + sentry_lib_dir: Path to directory containing libsentry + sentry_include_dir: Path to sentry include directory + """ + args = [ + f"-DSENTRY_LIB_DIR={sentry_lib_dir}", + f"-DSENTRY_INCLUDE_DIR={sentry_include_dir}", + ] + + android_config = get_android_config() + for key, value in android_config.items(): + args.append(f"-D{key}={value}") + + args.extend(get_platform_cmake_args()) + + return args + + +def get_test_executable_cflags(): + """ + Returns CFLAGS for building test executables. + + This is a subset of the flags used for libsentry, focusing on: + - Sanitizer instrumentation (required for linking with sanitized libsentry) + - 32-bit builds + """ + cflags = [] + + if sys.platform == "linux" and os.environ.get("TEST_X86"): + cflags.append("-m32") + + if "asan" in os.environ.get("RUN_ANALYZER", ""): + cflags.append("-fsanitize=address") + + if "tsan" in os.environ.get("RUN_ANALYZER", ""): + cflags.append("-fsanitize=thread") + + if "llvm-cov" in os.environ.get("RUN_ANALYZER", ""): + cflags.extend(["-fprofile-instr-generate", "-fcoverage-mapping"]) + + return cflags + + +def get_test_executable_env(): + """ + Returns environment variables for running/building test executables. + """ + env = dict(os.environ) + + tsan_opts = get_tsan_env() + if tsan_opts: + env["TSAN_OPTIONS"] = tsan_opts + + cflags = get_test_executable_cflags() + if cflags: + existing_cflags = env.get("CFLAGS", "") + existing_cxxflags = env.get("CXXFLAGS", "") + flags_str = " ".join(cflags) + env["CFLAGS"] = f"{existing_cflags} {flags_str}".strip() + env["CXXFLAGS"] = f"{existing_cxxflags} {flags_str}".strip() + + return env diff --git a/tests/cmake.py b/tests/cmake.py index 62484910a..6e97a273f 100644 --- a/tests/cmake.py +++ b/tests/cmake.py @@ -8,6 +8,13 @@ import pytest +from .build_config import ( + get_android_config, + get_platform_cmake_args, + get_cflags, + get_tsan_env, +) + class CMake: def __init__(self, factory): @@ -122,18 +129,8 @@ def cmake(cwd, targets, options=None, cflags=None): "CMAKE_RUNTIME_OUTPUT_DIRECTORY_RELEASE": cwd, } ) - if os.environ.get("ANDROID_API") and os.environ.get("ANDROID_NDK"): - # See: https://developer.android.com/ndk/guides/cmake - toolchain = "{}/ndk/{}/build/cmake/android.toolchain.cmake".format( - os.environ["ANDROID_HOME"], os.environ["ANDROID_NDK"] - ) - options.update( - { - "CMAKE_TOOLCHAIN_FILE": toolchain, - "ANDROID_ABI": os.environ.get("ANDROID_ARCH") or "x86", - "ANDROID_NATIVE_API_LEVEL": os.environ["ANDROID_API"], - } - ) + + options.update(get_android_config()) source_dir = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) @@ -158,25 +155,14 @@ def cmake(cwd, targets, options=None, cflags=None): for key, value in options.items(): config_cmd.append("-D{}={}".format(key, value)) - if sys.platform == "win32" and os.environ.get("TEST_X86"): - config_cmd.append("-AWin32") - elif sys.platform == "linux" and os.environ.get("TEST_X86"): - config_cmd.append("-DSENTRY_BUILD_FORCE32=ON") - if "asan" in os.environ.get("RUN_ANALYZER", ""): - config_cmd.append("-DWITH_ASAN_OPTION=ON") - if "tsan" in os.environ.get("RUN_ANALYZER", ""): - configure_tsan(config_cmd) - - # we have to set `-Werror` for this cmake invocation only, otherwise - # completely unrelated things will break - if os.environ.get("ERROR_ON_WARNINGS"): - cflags.append("-Werror") - if sys.platform == "win32" and not os.environ.get("TEST_MINGW"): - # MP = object level parallelism, WX = warnings as errors - cpus = os.cpu_count() - cflags.append("/WX /MP{}".format(cpus)) - if "gcc" in os.environ.get("RUN_ANALYZER", ""): - cflags.append("-fanalyzer") + + config_cmd.extend(get_platform_cmake_args()) + + tsan_opts = get_tsan_env() + if tsan_opts: + os.environ["TSAN_OPTIONS"] = tsan_opts + + cflags.extend(get_cflags()) if "llvm-cov" in os.environ.get("RUN_ANALYZER", ""): configure_llvm_cov(config_cmd) if "CMAKE_DEFINES" in os.environ: @@ -290,20 +276,6 @@ def configure_clang_cl(config_cmd: list[str]): config_cmd.append("-T ClangCL") -def configure_tsan(config_cmd: list[str]): - module_dir = Path(__file__).resolve().parent - tsan_options = { - "verbosity": 2, - "detect_deadlocks": 1, - "second_deadlock_stack": 1, - "suppressions": module_dir / "tsan.supp", - } - os.environ["TSAN_OPTIONS"] = ":".join( - f"{key}={value}" for key, value in tsan_options.items() - ) - config_cmd.append("-DWITH_TSAN_OPTION=ON") - - def configure_llvm_cov(config_cmd: list[str]): if False and os.environ.get("VS_GENERATOR_TOOLSET") == "ClangCL": # for clang-cl in CI we have to use `--coverage` rather than `fprofile-instr-generate` and provide an diff --git a/tests/fixtures/inproc_stress/CMakeLists.txt b/tests/fixtures/inproc_stress/CMakeLists.txt index 545b26938..a7f326bcb 100644 --- a/tests/fixtures/inproc_stress/CMakeLists.txt +++ b/tests/fixtures/inproc_stress/CMakeLists.txt @@ -2,6 +2,12 @@ cmake_minimum_required(VERSION 3.10) project(inproc_stress_test C) # SENTRY_LIB_DIR and SENTRY_INCLUDE_DIR are passed from the pytest + +# Options that match the main sentry-native build +option(SENTRY_BUILD_FORCE32 "Force a 32bit compile on a 64bit host" OFF) +option(WITH_ASAN_OPTION "Build with address sanitizer" OFF) +option(WITH_TSAN_OPTION "Build with thread sanitizer" OFF) + add_executable(inproc_stress_test main.c concurrent_crash.c @@ -18,4 +24,19 @@ set_target_properties(inproc_stress_test PROPERTIES if(NOT MSVC) target_compile_options(inproc_stress_test PRIVATE -O0 -g) target_link_libraries(inproc_stress_test PRIVATE pthread) + + if(SENTRY_BUILD_FORCE32) + target_compile_options(inproc_stress_test PRIVATE -m32) + target_link_options(inproc_stress_test PRIVATE -m32) + endif() + + if(WITH_ASAN_OPTION) + target_compile_options(inproc_stress_test PRIVATE -fsanitize=address) + target_link_options(inproc_stress_test PRIVATE -fsanitize=address) + endif() + + if(WITH_TSAN_OPTION) + target_compile_options(inproc_stress_test PRIVATE -fsanitize=thread) + target_link_options(inproc_stress_test PRIVATE -fsanitize=thread) + endif() endif() diff --git a/tests/test_inproc_stress.py b/tests/test_inproc_stress.py index c48cff527..d61301d6c 100644 --- a/tests/test_inproc_stress.py +++ b/tests/test_inproc_stress.py @@ -8,6 +8,7 @@ from . import Envelope from .assertions import assert_inproc_crash +from .build_config import get_test_executable_cmake_args, get_test_executable_env fixture_path = pathlib.Path("tests/fixtures/inproc_stress") @@ -19,21 +20,23 @@ def compile_test_program(tmp_path): source_dir = pathlib.Path(__file__).parent.parent.resolve() include_dir = source_dir / "include" + cmake_args = get_test_executable_cmake_args(tmp_path, include_dir) + cmake_args.append(str(fixture_path.resolve())) + + env = get_test_executable_env() + subprocess.run( - [ - "cmake", - f"-DSENTRY_LIB_DIR={tmp_path}", - f"-DSENTRY_INCLUDE_DIR={include_dir}", - str(fixture_path.resolve()), - ], + ["cmake"] + cmake_args, check=True, cwd=build_dir, + env=env, ) subprocess.run( ["cmake", "--build", ".", "--parallel"], check=True, cwd=build_dir, + env=env, ) if sys.platform == "win32": From 13ebb7155e2d068aa7538b270914e0b19e88616f Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Tue, 10 Feb 2026 13:49:21 +0100 Subject: [PATCH 088/130] look up pthread correctly using find_package() rather than assuming pthread exists as a separate library --- src/backends/sentry_backend_inproc.c | 2 +- tests/fixtures/inproc_stress/CMakeLists.txt | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/backends/sentry_backend_inproc.c b/src/backends/sentry_backend_inproc.c index d5c1f2393..f604d725c 100644 --- a/src/backends/sentry_backend_inproc.c +++ b/src/backends/sentry_backend_inproc.c @@ -33,7 +33,7 @@ # define SENTRY_SIGNAL_SAFE_LOG(msg) \ do { \ static const char _msg[] = "[sentry] " msg "\n"; \ - (void)write(STDERR_FILENO, _msg, sizeof(_msg) - 1); \ + (void)!write(STDERR_FILENO, _msg, sizeof(_msg) - 1); \ } while (0) #elif defined(SENTRY_PLATFORM_WINDOWS) # define SENTRY_SIGNAL_SAFE_LOG(msg) \ diff --git a/tests/fixtures/inproc_stress/CMakeLists.txt b/tests/fixtures/inproc_stress/CMakeLists.txt index a7f326bcb..edbd91a10 100644 --- a/tests/fixtures/inproc_stress/CMakeLists.txt +++ b/tests/fixtures/inproc_stress/CMakeLists.txt @@ -8,6 +8,10 @@ option(SENTRY_BUILD_FORCE32 "Force a 32bit compile on a 64bit host" OFF) option(WITH_ASAN_OPTION "Build with address sanitizer" OFF) option(WITH_TSAN_OPTION "Build with thread sanitizer" OFF) +# Use CMake's thread finding mechanism (handles Android correctly) +set(THREADS_PREFER_PTHREAD_FLAG ON) +find_package(Threads REQUIRED) + add_executable(inproc_stress_test main.c concurrent_crash.c @@ -15,7 +19,7 @@ add_executable(inproc_stress_test target_include_directories(inproc_stress_test PRIVATE ${SENTRY_INCLUDE_DIR}) target_link_directories(inproc_stress_test PRIVATE ${SENTRY_LIB_DIR}) -target_link_libraries(inproc_stress_test PRIVATE sentry) +target_link_libraries(inproc_stress_test PRIVATE sentry Threads::Threads) set_target_properties(inproc_stress_test PROPERTIES BUILD_RPATH "${SENTRY_LIB_DIR}" @@ -23,7 +27,6 @@ set_target_properties(inproc_stress_test PROPERTIES if(NOT MSVC) target_compile_options(inproc_stress_test PRIVATE -O0 -g) - target_link_libraries(inproc_stress_test PRIVATE pthread) if(SENTRY_BUILD_FORCE32) target_compile_options(inproc_stress_test PRIVATE -m32) From 0e8539bed5e70f3641f733cda66a546b849d1b4a Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Tue, 10 Feb 2026 14:31:28 +0100 Subject: [PATCH 089/130] allow the inproc stress tests to run on Android using adb shell --- tests/test_inproc_stress.py | 105 ++++++++++++++++++++++++++++++++++-- 1 file changed, 101 insertions(+), 4 deletions(-) diff --git a/tests/test_inproc_stress.py b/tests/test_inproc_stress.py index d61301d6c..01bdac920 100644 --- a/tests/test_inproc_stress.py +++ b/tests/test_inproc_stress.py @@ -12,6 +12,21 @@ fixture_path = pathlib.Path("tests/fixtures/inproc_stress") +ANDROID_TMP = "/data/local/tmp" + + +def adb(*args): + """Run an adb command.""" + return subprocess.run( + ["{}/platform-tools/adb".format(os.environ["ANDROID_HOME"]), *args], + check=True, + capture_output=True, + ) + + +def is_android(): + return bool(os.environ.get("ANDROID_API")) + def compile_test_program(tmp_path): build_dir = tmp_path / "inproc_stress_build" @@ -39,13 +54,22 @@ def compile_test_program(tmp_path): env=env, ) - if sys.platform == "win32": - return build_dir / "inproc_stress_test.exe" - else: - return build_dir / "inproc_stress_test" + exe_name = ( + "inproc_stress_test.exe" if sys.platform == "win32" else "inproc_stress_test" + ) + exe_path = build_dir / exe_name + + # Push executable to Android device + if is_android(): + adb("push", str(exe_path), ANDROID_TMP) + + return exe_path def run_stress_test(tmp_path, test_executable, test_name, database_path=None): + if is_android(): + return run_stress_test_android(test_executable, test_name, database_path) + if database_path is None: database_path = tmp_path / ".sentry-native" @@ -66,6 +90,79 @@ def run_stress_test(tmp_path, test_executable, test_name, database_path=None): return proc.returncode, stdout, stderr +def run_stress_test_android(test_executable, test_name, database_path): + """Run the stress test on Android via adb shell.""" + exe_name = test_executable.name + remote_db_path = f"{ANDROID_TMP}/{database_path.name}" + + # Clear logcat before running so we limit the capture as close to this run as possible + subprocess.run( + ["{}/platform-tools/adb".format(os.environ["ANDROID_HOME"]), "logcat", "-c"], + check=False, + ) + + # Run on device - we need to capture both stdout and stderr, and the return code + # Android shell doesn't separate stdout/stderr well, so we redirect stderr to stdout + # and parse the return code from the output (same approach as tests/__init__.py) + result = subprocess.run( + [ + "{}/platform-tools/adb".format(os.environ["ANDROID_HOME"]), + "shell", + f"cd {ANDROID_TMP} && LD_LIBRARY_PATH=. ./{exe_name} {test_name} {remote_db_path} 2>&1; echo ret:$?", + ], + capture_output=True, + text=True, + ) + + output = result.stdout + ret_marker = output.rfind("ret:") + if ret_marker != -1: + returncode = int(output[ret_marker + 4 :].strip()) + output = output[:ret_marker] + else: + returncode = result.returncode + + # Capture logcat to get our logs + logcat_result = subprocess.run( + [ + "{}/platform-tools/adb".format(os.environ["ANDROID_HOME"]), + "logcat", + "-d", + "-s", + "sentry-native:*", + ], + capture_output=True, + text=True, + ) + logcat_output = logcat_result.stdout + + # Pull the database directory back from the device + # adb pull creates a subdirectory, so we pull to the parent + if database_path.exists(): + shutil.rmtree(database_path) + + # Pull the remote database to local path (pulls to parent, creates database_path) + try: + adb("pull", f"{remote_db_path}/", str(database_path.parent)) + except subprocess.CalledProcessError: + # Database might not exist if crash wasn't captured + pass + + # Clean up remote database for next run + subprocess.run( + [ + "{}/platform-tools/adb".format(os.environ["ANDROID_HOME"]), + "shell", + f"rm -rf {remote_db_path}", + ], + check=False, + ) + + # Combine shell output with logcat output for assertion checks + combined_output = output + "\n" + logcat_output + return returncode, combined_output, combined_output + + def assert_single_crash_envelope(database_path): run_dirs = list(database_path.glob("*.run")) assert len(run_dirs) == 1, f"Expected 1 .run directory, found {len(run_dirs)}" From 0b83e30517059b6d870bef5cd1456c352c0d46c1 Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Tue, 10 Feb 2026 15:05:01 +0100 Subject: [PATCH 090/130] make the inproc output test assertions a bit more defensive --- tests/test_inproc_stress.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/tests/test_inproc_stress.py b/tests/test_inproc_stress.py index 01bdac920..72f2f61c9 100644 --- a/tests/test_inproc_stress.py +++ b/tests/test_inproc_stress.py @@ -3,6 +3,7 @@ import shutil import subprocess import sys +import time import pytest @@ -122,6 +123,9 @@ def run_stress_test_android(test_executable, test_name, database_path): else: returncode = result.returncode + # Give the system a moment to flush logcat buffers after the crash + time.sleep(0.5) + # Capture logcat to get our logs logcat_result = subprocess.run( [ @@ -290,14 +294,23 @@ def test_inproc_handler_thread_crash(cmake): "on_crash callback about to crash" in stderr ), f"on_crash callback didn't run. stderr:\n{stderr}" - assert ( - "FATAL crash in handler thread" in stderr - ), f"Handler thread crash not detected. stderr:\n{stderr}" + # The "FATAL crash in handler thread" message uses write() to stderr, + # which may not always be captured due to the racy nature of nested + # crash handling - the process might be killed before the write completes. + # We check for it but don't fail if it's missing, as long as the crash + # was still captured. + handler_crash_detected = "FATAL crash in handler thread" in stderr assert ( "crash has been captured" in stderr ), f"Crash not captured via fallback. stderr:\n{stderr}" + # Log whether we saw the handler crash message for debugging + if not handler_crash_detected: + print( + "Note: 'FATAL crash in handler thread' message not captured (timing-dependent)" + ) + assert_crash_marker(database_path) envelope_path = assert_single_crash_envelope(database_path) with open(envelope_path, "rb") as f: From 19f6391fd798a151f354e9260db6ffee0e3bc8ba Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Tue, 10 Feb 2026 15:18:24 +0100 Subject: [PATCH 091/130] deduplicate CMAKE_DEFINES from build_config refactor --- tests/cmake.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/cmake.py b/tests/cmake.py index 6e97a273f..09e25eb72 100644 --- a/tests/cmake.py +++ b/tests/cmake.py @@ -165,8 +165,6 @@ def cmake(cwd, targets, options=None, cflags=None): cflags.extend(get_cflags()) if "llvm-cov" in os.environ.get("RUN_ANALYZER", ""): configure_llvm_cov(config_cmd) - if "CMAKE_DEFINES" in os.environ: - config_cmd.extend(os.environ.get("CMAKE_DEFINES").split()) env = dict(os.environ) env["CFLAGS"] = env["CXXFLAGS"] = " ".join(cflags) From ea5599a77e76791caa5f23773480d9074e9eccda Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Tue, 10 Feb 2026 15:21:25 +0100 Subject: [PATCH 092/130] print macOS architecture in CI --- .github/workflows/ci.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3f796045b..b746d69d5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -231,6 +231,12 @@ jobs: - uses: actions/checkout@v4 with: submodules: recursive + + - name: Debug runner architecture + if: ${{ runner.os == 'macOS' }} + run: | + echo "Runner arch: $(uname -m)" + - uses: actions/setup-python@v5 with: python-version: ${{ !env['SYSTEM_PYTHON'] && '3.12' || '' }} From 548b70f0deb55ab5e5836555464868cd8a99f96d Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Tue, 10 Feb 2026 15:40:28 +0100 Subject: [PATCH 093/130] bump PAC runner to macOS-26 (since macOS-15 counter to docs is x86-64) --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b746d69d5..d8456c197 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -167,8 +167,8 @@ jobs: ERROR_ON_WARNINGS: 1 SYSTEM_VERSION_COMPAT: 0 RUN_ANALYZER: asan,llvm-cov - - name: macOS 15 (arm64e + PAC test) - os: macos-15-large + - name: macOS 26 (arm64e + PAC test) + os: macos-26 ERROR_ON_WARNINGS: 1 SYSTEM_VERSION_COMPAT: 0 CMAKE_DEFINES: -DCMAKE_OSX_ARCHITECTURES=arm64e From d512d6a1e60d1510f7d2dce88606fca6fe77b7c8 Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Tue, 10 Feb 2026 16:06:00 +0100 Subject: [PATCH 094/130] don't run the inproc stress tests via adb shell in CI, it is just too unstable... it works great locally and should remain for local testing and debugging though. --- tests/test_inproc_stress.py | 37 ++++++++++++++++++++----------------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/tests/test_inproc_stress.py b/tests/test_inproc_stress.py index 72f2f61c9..089d42f3a 100644 --- a/tests/test_inproc_stress.py +++ b/tests/test_inproc_stress.py @@ -16,6 +16,22 @@ ANDROID_TMP = "/data/local/tmp" +def is_android(): + return bool(os.environ.get("ANDROID_API")) + + +def is_ci(): + return os.environ.get("CI") == "true" or os.environ.get("GITHUB_ACTIONS") == "true" + + +# Skip Android tests in CI - adb shell execution is not stable enough +# These tests can still be run locally for development/debugging +pytestmark = pytest.mark.skipif( + is_android() and is_ci(), + reason="Android inproc stress tests disabled in CI (adb execution unreliable)", +) + + def adb(*args): """Run an adb command.""" return subprocess.run( @@ -25,10 +41,6 @@ def adb(*args): ) -def is_android(): - return bool(os.environ.get("ANDROID_API")) - - def compile_test_program(tmp_path): build_dir = tmp_path / "inproc_stress_build" build_dir.mkdir(exist_ok=True) @@ -294,23 +306,14 @@ def test_inproc_handler_thread_crash(cmake): "on_crash callback about to crash" in stderr ), f"on_crash callback didn't run. stderr:\n{stderr}" - # The "FATAL crash in handler thread" message uses write() to stderr, - # which may not always be captured due to the racy nature of nested - # crash handling - the process might be killed before the write completes. - # We check for it but don't fail if it's missing, as long as the crash - # was still captured. - handler_crash_detected = "FATAL crash in handler thread" in stderr + assert ( + "FATAL crash in handler thread" in stderr + ), f"Handler thread crash not detected. stderr:\n{stderr}" assert ( "crash has been captured" in stderr ), f"Crash not captured via fallback. stderr:\n{stderr}" - # Log whether we saw the handler crash message for debugging - if not handler_crash_detected: - print( - "Note: 'FATAL crash in handler thread' message not captured (timing-dependent)" - ) - assert_crash_marker(database_path) envelope_path = assert_single_crash_envelope(database_path) with open(envelope_path, "rb") as f: @@ -339,7 +342,7 @@ def test_inproc_concurrent_crash_repeated(cmake, iteration): assert returncode != 0, f"Iteration {iteration}: Process should have crashed" assert ( "crash has been captured" in stderr - ), f"Iteration {iteration}: Crash not captured" + ), f"Crash not captured. stderr:\n{stderr}" envelope_path = assert_single_crash_envelope(database_path) with open(envelope_path, "rb") as f: From a44d343ff2ba05b1a89bd704836450510f6bd72a Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Tue, 10 Feb 2026 16:10:19 +0100 Subject: [PATCH 095/130] mark test_inproc_handler_thread_crash flaky for now --- tests/test_inproc_stress.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_inproc_stress.py b/tests/test_inproc_stress.py index 089d42f3a..76683c702 100644 --- a/tests/test_inproc_stress.py +++ b/tests/test_inproc_stress.py @@ -6,6 +6,7 @@ import time import pytest +from flaky import flaky from . import Envelope from .assertions import assert_inproc_crash @@ -279,6 +280,7 @@ def test_inproc_concurrent_crash(cmake): shutil.rmtree(database_path, ignore_errors=True) +@flaky(max_runs=3) def test_inproc_handler_thread_crash(cmake): """ Test fallback when handler thread crashes. From 0bada1fb7fe4c18f09617eba0439a0dd2b0e8259 Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Tue, 10 Feb 2026 16:15:37 +0100 Subject: [PATCH 096/130] fix Windows build issue (noinline) --- tests/fixtures/inproc_stress/concurrent_crash.c | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/fixtures/inproc_stress/concurrent_crash.c b/tests/fixtures/inproc_stress/concurrent_crash.c index c11af2c33..ebf621b3d 100644 --- a/tests/fixtures/inproc_stress/concurrent_crash.c +++ b/tests/fixtures/inproc_stress/concurrent_crash.c @@ -32,7 +32,12 @@ static volatile LONG g_barrier = 0; static volatile LONG g_ready_count = 0; #endif -__attribute__((noinline)) void +#if defined(__GNUC__) || defined(__clang__) +__attribute__((noinline)) +#elif defined(_MSC_VER) +__declspec(noinline) +#endif +void do_crash(void) { memset((char *)invalid_mem, 1, 100); From e91da9fb0dc45d32d86bcb9bcd387e3e47b31f60 Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Tue, 10 Feb 2026 19:47:28 +0100 Subject: [PATCH 097/130] ensure that on Windows the inproc stress test executable placed directly in the build directory --- tests/fixtures/inproc_stress/CMakeLists.txt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/fixtures/inproc_stress/CMakeLists.txt b/tests/fixtures/inproc_stress/CMakeLists.txt index edbd91a10..2b664ce15 100644 --- a/tests/fixtures/inproc_stress/CMakeLists.txt +++ b/tests/fixtures/inproc_stress/CMakeLists.txt @@ -23,6 +23,13 @@ target_link_libraries(inproc_stress_test PRIVATE sentry Threads::Threads) set_target_properties(inproc_stress_test PROPERTIES BUILD_RPATH "${SENTRY_LIB_DIR}" + # Ensure the executable is placed directly in the build directory on all platforms + # (Visual Studio generators normally use Debug/Release subdirectories) + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}" + RUNTIME_OUTPUT_DIRECTORY_DEBUG "${CMAKE_BINARY_DIR}" + RUNTIME_OUTPUT_DIRECTORY_RELEASE "${CMAKE_BINARY_DIR}" + RUNTIME_OUTPUT_DIRECTORY_RELWITHDEBINFO "${CMAKE_BINARY_DIR}" + RUNTIME_OUTPUT_DIRECTORY_MINSIZEREL "${CMAKE_BINARY_DIR}" ) if(NOT MSVC) From 32b979fa6af6cc3501c6a00d21913adb108b2e38 Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Tue, 10 Feb 2026 19:59:21 +0100 Subject: [PATCH 098/130] provide a more comprehensive solution to recursive crash scenarios: * reintroduce unblocked signal handling thread to sentry__enter_signal_handler() * but also add a counter that tracks how often that thread entered the signal handler * and(!) configure all signal handlers as SA_NODEFER to allow recursive detection * add additional tests and test hook-points to both crash and abort() from within the signal handler * reintroduce the skip_hook idea into inproc, where a handler on the second recursive crash no longer calls on_crash or before_send --- src/backends/sentry_backend_breakpad.cpp | 2 +- src/backends/sentry_backend_crashpad.cpp | 2 +- src/backends/sentry_backend_inproc.c | 88 +++++++++++++++++++----- src/sentry_sync.c | 18 ++++- src/sentry_sync.h | 8 ++- tests/fixtures/inproc_stress/main.c | 66 +++++++++++++++++- tests/test_inproc_stress.py | 48 ++++++++++++- 7 files changed, 205 insertions(+), 27 deletions(-) diff --git a/src/backends/sentry_backend_breakpad.cpp b/src/backends/sentry_backend_breakpad.cpp index ede62051e..f723680f5 100644 --- a/src/backends/sentry_backend_breakpad.cpp +++ b/src/backends/sentry_backend_breakpad.cpp @@ -101,7 +101,7 @@ breakpad_backend_callback(const google_breakpad::MinidumpDescriptor &descriptor, #ifndef SENTRY_PLATFORM_WINDOWS sentry__page_allocator_enable(); - sentry__enter_signal_handler(); + (void)!sentry__enter_signal_handler(); #endif sentry_path_t *dump_path = nullptr; diff --git a/src/backends/sentry_backend_crashpad.cpp b/src/backends/sentry_backend_crashpad.cpp index 7cfa36bc7..e765b9ce6 100644 --- a/src/backends/sentry_backend_crashpad.cpp +++ b/src/backends/sentry_backend_crashpad.cpp @@ -317,7 +317,7 @@ static bool crashpad_handler(int signum, siginfo_t *info, ucontext_t *user_context) { sentry__page_allocator_enable(); - sentry__enter_signal_handler(); + (void)!sentry__enter_signal_handler(); # endif // Disable logging during crash handling if the option is set SENTRY_WITH_OPTIONS (options) { diff --git a/src/backends/sentry_backend_inproc.c b/src/backends/sentry_backend_inproc.c index f604d725c..519eb27ad 100644 --- a/src/backends/sentry_backend_inproc.c +++ b/src/backends/sentry_backend_inproc.c @@ -312,7 +312,12 @@ startup_inproc_backend( // install our own signal handler sigemptyset(&g_sigaction.sa_mask); g_sigaction.sa_sigaction = handle_signal; - g_sigaction.sa_flags = SA_SIGINFO | SA_ONSTACK; + // SA_NODEFER allows the signal to be delivered while the handler is + // running. This is needed for recursive crash detection to work - without + // it, a crash during crash handling would block the signal and leave the + // process in an undefined state. Our sentry__enter_signal_handler() + // detects recursive crashes and bails out to the previous handler. + g_sigaction.sa_flags = SA_SIGINFO | SA_ONSTACK | SA_NODEFER; for (size_t i = 0; i < SIGNAL_COUNT; ++i) { sigaction(SIGNAL_DEFINITIONS[i].signum, &g_sigaction, NULL); } @@ -856,11 +861,35 @@ make_signal_event(const struct signal_slot *sig_slot, * allocator since the program is in a crashed state. At least one thread no * longer progresses and memory can be corrupted. */ + +// Test hook for triggering crashes at specific points in the handler. +// Set via sentry__set_test_crash_callback() during testing. +#ifdef SENTRY_UNITTEST +typedef void (*sentry_test_crash_callback_t)(const char *location); +static sentry_test_crash_callback_t g_test_crash_callback = NULL; + +void +sentry__set_test_crash_callback(sentry_test_crash_callback_t callback) +{ + g_test_crash_callback = callback; +} + +# define TEST_CRASH_POINT(location) \ + do { \ + if (g_test_crash_callback) { \ + g_test_crash_callback(location); \ + } \ + } while (0) +#else +# define TEST_CRASH_POINT(location) ((void)0) +#endif + static void process_ucontext_deferred(const sentry_ucontext_t *uctx, - const struct signal_slot *sig_slot, bool skip_on_crash) + const struct signal_slot *sig_slot, bool skip_hooks) { SENTRY_INFO("entering signal handler"); + TEST_CRASH_POINT("after_enter"); SENTRY_WITH_OPTIONS (options) { sentry_handler_strategy_t strategy = @@ -872,11 +901,12 @@ process_ucontext_deferred(const sentry_ucontext_t *uctx, bool should_handle = true; sentry__write_crash_marker(options); - // TODO: before_send too - if (options->on_crash_func && !skip_on_crash) { + if (options->on_crash_func && !skip_hooks) { SENTRY_DEBUG("invoking `on_crash` hook"); event = options->on_crash_func(uctx, event, options->on_crash_data); should_handle = !sentry_value_is_null(event); + } else if (skip_hooks && options->on_crash_func) { + SENTRY_DEBUG("skipping `on_crash` hook due to recursive crash"); } // Flush logs in a crash-safe manner before crash handling @@ -886,9 +916,10 @@ process_ucontext_deferred(const sentry_ucontext_t *uctx, if (options->enable_metrics) { sentry__metrics_flush_crash_safe(); } + TEST_CRASH_POINT("before_capture"); if (should_handle) { - sentry_envelope_t *envelope = sentry__prepare_event( - options, event, NULL, !options->on_crash_func, NULL); + sentry_envelope_t *envelope = sentry__prepare_event(options, event, + NULL, !options->on_crash_func && !skip_hooks, NULL); // TODO(tracing): Revisit when investigating transaction flushing // during hard crashes. @@ -1133,19 +1164,24 @@ has_handler_thread_crashed(void) } static void -dispatch_ucontext( - const sentry_ucontext_t *uctx, const struct signal_slot *sig_slot) +dispatch_ucontext(const sentry_ucontext_t *uctx, + const struct signal_slot *sig_slot, int handler_depth) { + // skip_hooks when re-entering (depth >= 2) to avoid crashing in the same + // hook again, but still try to capture the crash + bool skip_hooks = handler_depth >= 2; + #ifdef SENTRY_WITH_UNWINDER_LIBBACKTRACE // For targets that still use `backtrace()` as the sole unwinder we must // run the signal-unsafe part in the signal handler like we did before. - process_ucontext_deferred(uctx, sig_slot, false); + process_ucontext_deferred(uctx, sig_slot, skip_hooks); return; #else if (has_handler_thread_crashed()) { // directly execute unsafe part in signal handler as a last chance to - // report an error when the handler thread has crashed. Skip the - // on_crash callback since that's likely what caused the crash. + // report an error when the handler thread has crashed. + // Always skip hooks here since the first attempt (from handler thread) + // already failed, likely due to a crashing hook. process_ucontext_deferred(uctx, sig_slot, true); return; } @@ -1173,7 +1209,7 @@ dispatch_ucontext( // We are the first handler. Check if handler thread is available. if (!sentry__atomic_fetch(&g_handler_thread_ready)) { - process_ucontext_deferred(uctx, sig_slot, false); + process_ucontext_deferred(uctx, sig_slot, skip_hooks); return; } @@ -1225,8 +1261,12 @@ dispatch_ucontext( if (!handler_signaled) { // Fall back to in-handler processing - sentry__enter_signal_handler(); - process_ucontext_deferred(uctx, sig_slot, false); + int depth = sentry__enter_signal_handler(); + if (depth >= 3) { + // Multiple recursive crashes - bail out + return; + } + process_ucontext_deferred(uctx, sig_slot, depth >= 2); return; } # elif defined(SENTRY_PLATFORM_WINDOWS) @@ -1241,7 +1281,7 @@ dispatch_ucontext( if (!handler_signaled) { // Fall back to in-handler processing - process_ucontext_deferred(uctx, sig_slot, false); + process_ucontext_deferred(uctx, sig_slot, skip_hooks); return; } # endif @@ -1262,7 +1302,8 @@ dispatch_ucontext( # endif # ifdef SENTRY_PLATFORM_UNIX - sentry__enter_signal_handler(); + // Re-acquire signal handler lock after handler thread finished. + (void)!sentry__enter_signal_handler(); # endif #endif @@ -1327,7 +1368,13 @@ process_ucontext(const sentry_ucontext_t *uctx) #endif #ifdef SENTRY_PLATFORM_UNIX - sentry__enter_signal_handler(); + int handler_depth = sentry__enter_signal_handler(); + if (handler_depth >= 3) { + // Multiple recursive crashes - bail out completely to avoid loops + SENTRY_SIGNAL_SAFE_LOG( + "multiple recursive crashes detected, bailing out"); + goto cleanup; + } #endif if (!g_backend_config.enable_logging_when_crashed) { @@ -1354,9 +1401,14 @@ process_ucontext(const sentry_ucontext_t *uctx) #endif // invoke the handler thread for signal unsafe actions - dispatch_ucontext(uctx, sig_slot); +#ifdef SENTRY_PLATFORM_UNIX + dispatch_ucontext(uctx, sig_slot, handler_depth); +#else + dispatch_ucontext(uctx, sig_slot, 1); +#endif #ifdef SENTRY_PLATFORM_UNIX +cleanup: // reset signal handlers and invoke the original ones. This will then tear // down the process. In theory someone might have some other handler here // which recovers the process but this will cause a memory leak going diff --git a/src/sentry_sync.c b/src/sentry_sync.c index 299e444a7..d370f4923 100644 --- a/src/sentry_sync.c +++ b/src/sentry_sync.c @@ -533,12 +533,25 @@ sentry__block_for_signal_handler(void) } } -void +// Tracks recursive entry depth for the signal handling thread. +// 0 = not in handler, 1 = first entry, 2 = re-entry (skip hooks), 3+ = bail out +static volatile sig_atomic_t g_signal_handler_depth = 0; + +int sentry__enter_signal_handler(void) { for (;;) { // entering a signal handler while another runs, should block us + // unless we are the signal handling thread (recursive crash) while (__atomic_load_n(&g_in_signal_handler, __ATOMIC_RELAXED)) { + if (sentry__threadid_equal(sentry__current_thread(), + __atomic_load_n( + &g_signal_handling_thread, __ATOMIC_ACQUIRE))) { + // same thread re-entering via recursive crash + int depth = __atomic_add_fetch( + &g_signal_handler_depth, 1, __ATOMIC_ACQ_REL); + return depth; + } sentry__cpu_relax(); } @@ -548,7 +561,8 @@ sentry__enter_signal_handler(void) // once we have, publish the handling thread too sentry_threadid_t me = sentry__current_thread(); __atomic_store_n(&g_signal_handling_thread, me, __ATOMIC_RELEASE); - return; + __atomic_store_n(&g_signal_handler_depth, 1, __ATOMIC_RELEASE); + return 1; // first entry } // otherwise we've been raced, spin diff --git a/src/sentry_sync.h b/src/sentry_sync.h index 964822e6f..6505dce59 100644 --- a/src/sentry_sync.h +++ b/src/sentry_sync.h @@ -237,7 +237,13 @@ sentry__threadid_equal(sentry_threadid_t a, sentry_threadid_t b) us crash under concurrent modifications. The mutexes we're likely going to hit are the options and scope lock. */ bool sentry__block_for_signal_handler(void); -void sentry__enter_signal_handler(void); +/** + * Enter signal handler context. Returns the recursion depth: + * 1 = first entry, normal processing + * 2 = re-entry (crash during handling), skip hooks but try to capture + * 3+ = multiple re-entries, bail out to previous handler + */ +int sentry__enter_signal_handler(void); void sentry__leave_signal_handler(void); typedef pthread_t sentry_threadid_t; diff --git a/tests/fixtures/inproc_stress/main.c b/tests/fixtures/inproc_stress/main.c index 9b842c49a..40dd0ea7c 100644 --- a/tests/fixtures/inproc_stress/main.c +++ b/tests/fixtures/inproc_stress/main.c @@ -24,7 +24,7 @@ extern void run_concurrent_crash(void); static void *invalid_mem = (void *)1; -// on_crash callback that crashes - simulates buggy user code +// on_crash callback that crashes via SIGSEGV: simulates buggy user code static sentry_value_t crashing_on_crash_callback( const sentry_ucontext_t *uctx, sentry_value_t event, void *closure) @@ -41,6 +41,23 @@ crashing_on_crash_callback( return event; } +// on_crash callback that crashes via abort(): tests signal mask reset behavior +static sentry_value_t +aborting_on_crash_callback( + const sentry_ucontext_t *uctx, sentry_value_t event, void *closure) +{ + (void)uctx; + (void)event; + (void)closure; + + fprintf(stderr, "on_crash callback about to abort\n"); + fflush(stderr); + + abort(); + + return event; +} + static void print_envelope(sentry_envelope_t *envelope, void *unused_state) { @@ -116,6 +133,25 @@ setup_sentry_with_crashing_on_crash(const char *database_path) return 0; } +static int +setup_sentry_with_aborting_on_crash(const char *database_path) +{ + sentry_options_t *options = sentry_options_new(); + sentry_options_set_database_path(options, database_path); + sentry_options_set_auto_session_tracking(options, false); + sentry_options_set_dsn(options, "https://public@sentry.invalid/1"); + sentry_options_set_debug(options, 1); + sentry_options_set_transport(options, sentry_transport_new(print_envelope)); + + sentry_options_set_on_crash(options, aborting_on_crash_callback, NULL); + + if (sentry_init(options) != 0) { + fprintf(stderr, "Failed to initialize sentry\n"); + return 1; + } + return 0; +} + static int test_handler_thread_crash(const char *database_path) { @@ -138,6 +174,28 @@ test_handler_thread_crash(const char *database_path) return 1; } +static int +test_handler_abort_crash(const char *database_path) +{ + if (setup_sentry_with_aborting_on_crash(database_path) != 0) { + return 1; + } + + sentry_set_tag("test", "handler-abort-crash"); + + fprintf(stderr, "Starting handler abort crash test\n"); + fflush(stderr); + + // This will crash, trigger the handler thread, which will call + // on_crash callback, which will call abort(). abort() resets the + // signal mask, so this tests a different code path. + trigger_crash(); + + fprintf(stderr, "ERROR: Should have crashed\n"); + sentry_close(); + return 1; +} + static int test_simple_crash(const char *database_path) { @@ -165,7 +223,8 @@ main(int argc, char **argv) fprintf(stderr, "Tests:\n"); fprintf(stderr, " concurrent-crash - Multiple threads crash simultaneously\n"); fprintf(stderr, " simple-crash - Single thread crash (baseline)\n"); - fprintf(stderr, " handler-thread-crash - Handler thread crashes in on_crash\n"); + fprintf(stderr, " handler-thread-crash - Handler thread crashes in on_crash (SIGSEGV)\n"); + fprintf(stderr, " handler-abort-crash - Handler thread crashes in on_crash (abort)\n"); return 1; } @@ -181,6 +240,9 @@ main(int argc, char **argv) if (strcmp(test_name, "handler-thread-crash") == 0) { return test_handler_thread_crash(database_path); } + if (strcmp(test_name, "handler-abort-crash") == 0) { + return test_handler_abort_crash(database_path); + } fprintf(stderr, "Unknown test: %s\n", test_name); return 1; } diff --git a/tests/test_inproc_stress.py b/tests/test_inproc_stress.py index 76683c702..b492d6b87 100644 --- a/tests/test_inproc_stress.py +++ b/tests/test_inproc_stress.py @@ -280,10 +280,9 @@ def test_inproc_concurrent_crash(cmake): shutil.rmtree(database_path, ignore_errors=True) -@flaky(max_runs=3) def test_inproc_handler_thread_crash(cmake): """ - Test fallback when handler thread crashes. + Test fallback when handler thread crashes via SIGSEGV. This test verifies that when the on_crash callback crashes (which runs on the handler thread), the signal handler detects this and falls back @@ -326,6 +325,51 @@ def test_inproc_handler_thread_crash(cmake): shutil.rmtree(database_path, ignore_errors=True) +def test_inproc_handler_abort_crash(cmake): + """ + Test fallback when handler thread crashes via abort(). + + abort() has special behavior - it resets the signal mask before raising + SIGABRT. This tests that the recursive crash detection works even when + the signal mask is reset. + """ + tmp_path = cmake( + ["sentry"], + {"SENTRY_BACKEND": "inproc", "SENTRY_TRANSPORT": "none"}, + ) + + test_exe = compile_test_program(tmp_path) + database_path = tmp_path / ".sentry-native" + + try: + returncode, stdout, stderr = run_stress_test( + tmp_path, test_exe, "handler-abort-crash", database_path + ) + + assert returncode != 0, f"Process should have crashed. stderr:\n{stderr}" + + assert ( + "on_crash callback about to abort" in stderr + ), f"on_crash callback didn't run. stderr:\n{stderr}" + + assert ( + "FATAL crash in handler thread" in stderr + ), f"Handler thread crash not detected. stderr:\n{stderr}" + + assert ( + "crash has been captured" in stderr + ), f"Crash not captured via fallback. stderr:\n{stderr}" + + assert_crash_marker(database_path) + envelope_path = assert_single_crash_envelope(database_path) + with open(envelope_path, "rb") as f: + envelope = Envelope.deserialize(f.read()) + assert_inproc_crash(envelope) + + finally: + shutil.rmtree(database_path, ignore_errors=True) + + @pytest.mark.parametrize("iteration", range(5)) def test_inproc_concurrent_crash_repeated(cmake, iteration): tmp_path = cmake( From 2bd4551d4c79e4ab28dc910ed2d11d472ee22b11 Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Tue, 10 Feb 2026 21:32:32 +0100 Subject: [PATCH 099/130] make the inproc stress tests widechar path friendly --- tests/fixtures/inproc_stress/main.c | 97 ++++++++++++++++++++++++----- 1 file changed, 81 insertions(+), 16 deletions(-) diff --git a/tests/fixtures/inproc_stress/main.c b/tests/fixtures/inproc_stress/main.c index 40dd0ea7c..8db77127f 100644 --- a/tests/fixtures/inproc_stress/main.c +++ b/tests/fixtures/inproc_stress/main.c @@ -8,6 +8,11 @@ * ./inproc_stress_test pipe-failure */ +#ifdef _WIN32 +# define WIN32_LEAN_AND_MEAN +# include +#endif + #include "sentry.h" #include #include @@ -70,11 +75,22 @@ print_envelope(sentry_envelope_t *envelope, void *unused_state) sentry_envelope_free(envelope); } +// Use wide-char paths on Windows, narrow paths elsewhere +#if defined(_WIN32) && defined(_MSC_VER) +# define PATH_TYPE const wchar_t * +# define SET_DATABASE_PATH sentry_options_set_database_pathw +# define DEFAULT_DATABASE_PATH L".sentry-native" +#else +# define PATH_TYPE const char * +# define SET_DATABASE_PATH sentry_options_set_database_path +# define DEFAULT_DATABASE_PATH ".sentry-native" +#endif + static int -setup_sentry(const char *database_path) +setup_sentry(PATH_TYPE database_path) { sentry_options_t *options = sentry_options_new(); - sentry_options_set_database_path(options, database_path); + SET_DATABASE_PATH(options, database_path); sentry_options_set_auto_session_tracking(options, false); sentry_options_set_dsn(options, "https://public@sentry.invalid/1"); sentry_options_set_debug(options, 1); @@ -88,7 +104,7 @@ setup_sentry(const char *database_path) } static int -test_concurrent_crash(const char *database_path) +test_concurrent_crash(PATH_TYPE database_path) { if (setup_sentry(database_path) != 0) { return 1; @@ -114,10 +130,10 @@ trigger_crash(void) } static int -setup_sentry_with_crashing_on_crash(const char *database_path) +setup_sentry_with_crashing_on_crash(PATH_TYPE database_path) { sentry_options_t *options = sentry_options_new(); - sentry_options_set_database_path(options, database_path); + SET_DATABASE_PATH(options, database_path); sentry_options_set_auto_session_tracking(options, false); sentry_options_set_dsn(options, "https://public@sentry.invalid/1"); sentry_options_set_debug(options, 1); @@ -134,10 +150,10 @@ setup_sentry_with_crashing_on_crash(const char *database_path) } static int -setup_sentry_with_aborting_on_crash(const char *database_path) +setup_sentry_with_aborting_on_crash(PATH_TYPE database_path) { sentry_options_t *options = sentry_options_new(); - sentry_options_set_database_path(options, database_path); + SET_DATABASE_PATH(options, database_path); sentry_options_set_auto_session_tracking(options, false); sentry_options_set_dsn(options, "https://public@sentry.invalid/1"); sentry_options_set_debug(options, 1); @@ -153,7 +169,7 @@ setup_sentry_with_aborting_on_crash(const char *database_path) } static int -test_handler_thread_crash(const char *database_path) +test_handler_thread_crash(PATH_TYPE database_path) { if (setup_sentry_with_crashing_on_crash(database_path) != 0) { return 1; @@ -175,7 +191,7 @@ test_handler_thread_crash(const char *database_path) } static int -test_handler_abort_crash(const char *database_path) +test_handler_abort_crash(PATH_TYPE database_path) { if (setup_sentry_with_aborting_on_crash(database_path) != 0) { return 1; @@ -197,7 +213,7 @@ test_handler_abort_crash(const char *database_path) } static int -test_simple_crash(const char *database_path) +test_simple_crash(PATH_TYPE database_path) { if (setup_sentry(database_path) != 0) { return 1; @@ -215,21 +231,69 @@ test_simple_crash(const char *database_path) return 1; } +#if defined(_WIN32) && defined(_MSC_VER) +int +wmain(int argc, wchar_t *argv[]) +{ + if (argc < 2) { + fwprintf(stderr, L"Usage: %ls [database-path]\n", argv[0]); + fwprintf(stderr, L"Tests:\n"); + fwprintf(stderr, + L" concurrent-crash - Multiple threads crash " + L"simultaneously\n"); + fwprintf(stderr, + L" simple-crash - Single thread crash (baseline)\n"); + fwprintf(stderr, + L" handler-thread-crash - Handler thread crashes in on_crash " + L"(SIGSEGV)\n"); + fwprintf(stderr, + L" handler-abort-crash - Handler thread crashes in on_crash " + L"(abort)\n"); + return 1; + } + + const wchar_t *test_name = argv[1]; + const wchar_t *database_path + = argc > 2 ? argv[2] : DEFAULT_DATABASE_PATH; + + if (wcscmp(test_name, L"concurrent-crash") == 0) { + return test_concurrent_crash(database_path); + } + if (wcscmp(test_name, L"simple-crash") == 0) { + return test_simple_crash(database_path); + } + if (wcscmp(test_name, L"handler-thread-crash") == 0) { + return test_handler_thread_crash(database_path); + } + if (wcscmp(test_name, L"handler-abort-crash") == 0) { + return test_handler_abort_crash(database_path); + } + fwprintf(stderr, L"Unknown test: %ls\n", test_name); + return 1; +} +#else int -main(int argc, char **argv) +main(int argc, char *argv[]) { if (argc < 2) { fprintf(stderr, "Usage: %s [database-path]\n", argv[0]); fprintf(stderr, "Tests:\n"); - fprintf(stderr, " concurrent-crash - Multiple threads crash simultaneously\n"); - fprintf(stderr, " simple-crash - Single thread crash (baseline)\n"); - fprintf(stderr, " handler-thread-crash - Handler thread crashes in on_crash (SIGSEGV)\n"); - fprintf(stderr, " handler-abort-crash - Handler thread crashes in on_crash (abort)\n"); + fprintf(stderr, + " concurrent-crash - Multiple threads crash " + "simultaneously\n"); + fprintf(stderr, + " simple-crash - Single thread crash (baseline)\n"); + fprintf(stderr, + " handler-thread-crash - Handler thread crashes in on_crash " + "(SIGSEGV)\n"); + fprintf(stderr, + " handler-abort-crash - Handler thread crashes in on_crash " + "(abort)\n"); return 1; } const char *test_name = argv[1]; - const char *database_path = argc > 2 ? argv[2] : ".sentry-native"; + const char *database_path = argc > 2 ? argv[2] : DEFAULT_DATABASE_PATH; if (strcmp(test_name, "concurrent-crash") == 0) { return test_concurrent_crash(database_path); @@ -246,3 +310,4 @@ main(int argc, char **argv) fprintf(stderr, "Unknown test: %s\n", test_name); return 1; } +#endif From b2594e53dc0ed09f2d349af264cb059333e6cd30 Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Tue, 10 Feb 2026 21:32:57 +0100 Subject: [PATCH 100/130] do not set SA_NODEFER for SIGABRT --- src/backends/sentry_backend_inproc.c | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/backends/sentry_backend_inproc.c b/src/backends/sentry_backend_inproc.c index 519eb27ad..370a34e87 100644 --- a/src/backends/sentry_backend_inproc.c +++ b/src/backends/sentry_backend_inproc.c @@ -312,13 +312,19 @@ startup_inproc_backend( // install our own signal handler sigemptyset(&g_sigaction.sa_mask); g_sigaction.sa_sigaction = handle_signal; - // SA_NODEFER allows the signal to be delivered while the handler is - // running. This is needed for recursive crash detection to work - without - // it, a crash during crash handling would block the signal and leave the - // process in an undefined state. Our sentry__enter_signal_handler() - // detects recursive crashes and bails out to the previous handler. - g_sigaction.sa_flags = SA_SIGINFO | SA_ONSTACK | SA_NODEFER; for (size_t i = 0; i < SIGNAL_COUNT; ++i) { + // SA_NODEFER allows the signal to be delivered while the handler is + // running. This is needed for recursive crash detection to work - + // without it, a crash during crash handling would block the signal + // and leave the process in an undefined state. + // However, SA_NODEFER should NOT be used for SIGABRT because abort() + // does its own signal mask manipulation and having SA_NODEFER can + // cause unexpected interactions with different libc implementations. + if (SIGNAL_DEFINITIONS[i].signum == SIGABRT) { + g_sigaction.sa_flags = SA_SIGINFO | SA_ONSTACK; + } else { + g_sigaction.sa_flags = SA_SIGINFO | SA_ONSTACK | SA_NODEFER; + } sigaction(SIGNAL_DEFINITIONS[i].signum, &g_sigaction, NULL); } return 0; From 50f34d300f83994086ca3dba365eda1e16df9379 Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Tue, 10 Feb 2026 21:42:12 +0100 Subject: [PATCH 101/130] introduce sigaltstack also to the handler thread and set up a sigabrt handler for Windows, so inproc can handle abort() --- src/backends/sentry_backend_inproc.c | 131 ++++++++++++++++++++++----- 1 file changed, 107 insertions(+), 24 deletions(-) diff --git a/src/backends/sentry_backend_inproc.c b/src/backends/sentry_backend_inproc.c index 370a34e87..2e8ff0211 100644 --- a/src/backends/sentry_backend_inproc.c +++ b/src/backends/sentry_backend_inproc.c @@ -13,6 +13,7 @@ #include "sentry_options.h" #if defined(SENTRY_PLATFORM_WINDOWS) # include "sentry_os.h" +# include #endif #include "sentry_scope.h" #include "sentry_screenshot.h" @@ -229,6 +230,50 @@ static const struct signal_slot SIGNAL_DEFINITIONS[SIGNAL_COUNT] = { static void handle_signal(int signum, siginfo_t *info, void *user_context); +/** + * Sets up an alternate signal stack for the current thread if one isn't + * already configured. The allocated stack is stored in `out_stack` so it + * can be freed later via `teardown_sigaltstack()`. + */ +static void +setup_sigaltstack(stack_t *out_stack) +{ + memset(out_stack, 0, sizeof(*out_stack)); + + stack_t old_sig_stack; + int ret = sigaltstack(NULL, &old_sig_stack); + if (ret == 0 && old_sig_stack.ss_flags == SS_DISABLE) { + SENTRY_DEBUGF("installing signal stack (size: %d)", SIGNAL_STACK_SIZE); + out_stack->ss_sp = sentry_malloc(SIGNAL_STACK_SIZE); + if (!out_stack->ss_sp) { + SENTRY_WARN("failed to allocate signal stack"); + return; + } + out_stack->ss_size = SIGNAL_STACK_SIZE; + out_stack->ss_flags = 0; + sigaltstack(out_stack, 0); + } else if (ret == 0) { + SENTRY_DEBUGF("using existing signal stack (size: %d, flags: %d)", + old_sig_stack.ss_size, old_sig_stack.ss_flags); + } else if (ret == -1) { + SENTRY_WARNF("failed to query signal stack: %s", strerror(errno)); + } +} + +/** + * Tears down a signal stack previously set up via `setup_sigaltstack()`. + */ +static void +teardown_sigaltstack(stack_t *sig_stack) +{ + if (sig_stack->ss_sp) { + sig_stack->ss_flags = SS_DISABLE; + sigaltstack(sig_stack, 0); + sentry_free(sig_stack->ss_sp); + sig_stack->ss_sp = NULL; + } +} + static void reset_signal_handlers(void) { @@ -290,24 +335,7 @@ startup_inproc_backend( } } - // set up an alternate signal stack if noone defined one before - stack_t old_sig_stack; - int ret = sigaltstack(NULL, &old_sig_stack); - if (ret == 0 && old_sig_stack.ss_flags == SS_DISABLE) { - SENTRY_DEBUGF("installing signal stack (size: %d)", SIGNAL_STACK_SIZE); - g_signal_stack.ss_sp = sentry_malloc(SIGNAL_STACK_SIZE); - if (!g_signal_stack.ss_sp) { - return 1; - } - g_signal_stack.ss_size = SIGNAL_STACK_SIZE; - g_signal_stack.ss_flags = 0; - sigaltstack(&g_signal_stack, 0); - } else if (ret == 0) { - SENTRY_DEBUGF("using existing signal stack (size: %d, flags: %d)", - old_sig_stack.ss_size, old_sig_stack.ss_flags); - } else if (ret == -1) { - SENTRY_WARNF("Failed to query signal stack size: %s", strerror(errno)); - } + setup_sigaltstack(&g_signal_stack); // install our own signal handler sigemptyset(&g_sigaction.sa_mask); @@ -335,12 +363,7 @@ shutdown_inproc_backend(sentry_backend_t *backend) { stop_handler_thread(); - if (g_signal_stack.ss_sp) { - g_signal_stack.ss_flags = SS_DISABLE; - sigaltstack(&g_signal_stack, 0); - sentry_free(g_signal_stack.ss_sp); - g_signal_stack.ss_sp = NULL; - } + teardown_sigaltstack(&g_signal_stack); reset_signal_handlers(); if (backend) { @@ -388,6 +411,46 @@ static const struct signal_slot SIGNAL_DEFINITIONS[SIGNAL_COUNT] = { static LONG WINAPI handle_exception(EXCEPTION_POINTERS *); +// SIGABRT handling on Windows: abort() calls the signal handler but doesn't +// go through the unhandled exception filter. We register a SIGABRT handler +// that captures context and calls into our exception handler. +static void (*g_previous_sigabrt_handler)(int) = NULL; + +static void +handle_sigabrt(int signum) +{ + (void)signum; + + // Capture the current CPU context + CONTEXT context; + RtlCaptureContext(&context); + + // Create a synthetic exception record for abort + EXCEPTION_RECORD record; + memset(&record, 0, sizeof(record)); + record.ExceptionCode = STATUS_FATAL_APP_EXIT; + record.ExceptionFlags = EXCEPTION_NONCONTINUABLE; +# if defined(_M_AMD64) + record.ExceptionAddress = (PVOID)context.Rip; +# elif defined(_M_IX86) + record.ExceptionAddress = (PVOID)context.Eip; +# elif defined(_M_ARM64) + record.ExceptionAddress = (PVOID)context.Pc; +# endif + + EXCEPTION_POINTERS exception_pointers; + exception_pointers.ContextRecord = &context; + exception_pointers.ExceptionRecord = &record; + + handle_exception(&exception_pointers); + + // If we get here, call the previous handler or terminate + if (g_previous_sigabrt_handler && g_previous_sigabrt_handler != SIG_DFL + && g_previous_sigabrt_handler != SIG_IGN) { + g_previous_sigabrt_handler(signum); + } +} + static int startup_inproc_backend( sentry_backend_t *backend, const sentry_options_t *options) @@ -408,6 +471,10 @@ startup_inproc_backend( } g_previous_handler = SetUnhandledExceptionFilter(&handle_exception); SetErrorMode(SEM_FAILCRITICALERRORS); + + // Register SIGABRT handler - abort() doesn't go through the UEF + g_previous_sigabrt_handler = signal(SIGABRT, handle_sigabrt); + return 0; } @@ -422,6 +489,12 @@ shutdown_inproc_backend(sentry_backend_t *backend) SetUnhandledExceptionFilter(current_handler); } + // Restore previous SIGABRT handler + if (g_previous_sigabrt_handler) { + signal(SIGABRT, g_previous_sigabrt_handler); + g_previous_sigabrt_handler = NULL; + } + if (backend) { backend->data = NULL; } @@ -966,6 +1039,12 @@ process_ucontext_deferred(const sentry_ucontext_t *uctx, SENTRY_THREAD_FN handler_thread_main(void *UNUSED(data)) { +#ifdef SENTRY_PLATFORM_UNIX + // Set up an alternate signal stack for the handler thread + stack_t handler_thread_stack; + setup_sigaltstack(&handler_thread_stack); +#endif + sentry__atomic_store(&g_handler_thread_ready, 1); for (;;) { @@ -1023,6 +1102,10 @@ handler_thread_main(void *UNUSED(data)) } } +#ifdef SENTRY_PLATFORM_UNIX + teardown_sigaltstack(&handler_thread_stack); +#endif + #ifdef SENTRY_PLATFORM_WINDOWS return 0; #else From 18eb05c1c0d9b1a3416e9889eb6e3a43f5e6baf2 Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Tue, 10 Feb 2026 22:57:18 +0100 Subject: [PATCH 102/130] disable abort pop up on windows which blocks the test in CI indefinitely --- tests/fixtures/inproc_stress/main.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/fixtures/inproc_stress/main.c b/tests/fixtures/inproc_stress/main.c index 8db77127f..b3fc3238c 100644 --- a/tests/fixtures/inproc_stress/main.c +++ b/tests/fixtures/inproc_stress/main.c @@ -193,6 +193,11 @@ test_handler_thread_crash(PATH_TYPE database_path) static int test_handler_abort_crash(PATH_TYPE database_path) { +#ifdef _WIN32 + // Suppress the Windows abort dialog that would block CI + _set_abort_behavior(0, _WRITE_ABORT_MSG | _CALL_REPORTFAULT); +#endif + if (setup_sentry_with_aborting_on_crash(database_path) != 0) { return 1; } From fc5c057afef13e05c8143ec1cd9382baa973eca3 Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Wed, 11 Feb 2026 08:49:33 +0100 Subject: [PATCH 103/130] disable any stdio logging in inproc signal handler fallback paths. --- src/backends/sentry_backend_inproc.c | 35 +++++++++++++++++----------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/src/backends/sentry_backend_inproc.c b/src/backends/sentry_backend_inproc.c index 2e8ff0211..17f16141f 100644 --- a/src/backends/sentry_backend_inproc.c +++ b/src/backends/sentry_backend_inproc.c @@ -340,19 +340,12 @@ startup_inproc_backend( // install our own signal handler sigemptyset(&g_sigaction.sa_mask); g_sigaction.sa_sigaction = handle_signal; + // SA_NODEFER allows the signal to be delivered while the handler is + // running. This is needed for recursive crash detection to work - + // without it, a crash during crash handling would block the signal + // and leave the process in an undefined state. + g_sigaction.sa_flags = SA_SIGINFO | SA_ONSTACK | SA_NODEFER; for (size_t i = 0; i < SIGNAL_COUNT; ++i) { - // SA_NODEFER allows the signal to be delivered while the handler is - // running. This is needed for recursive crash detection to work - - // without it, a crash during crash handling would block the signal - // and leave the process in an undefined state. - // However, SA_NODEFER should NOT be used for SIGABRT because abort() - // does its own signal mask manipulation and having SA_NODEFER can - // cause unexpected interactions with different libc implementations. - if (SIGNAL_DEFINITIONS[i].signum == SIGABRT) { - g_sigaction.sa_flags = SA_SIGINFO | SA_ONSTACK; - } else { - g_sigaction.sa_flags = SA_SIGINFO | SA_ONSTACK | SA_NODEFER; - } sigaction(SIGNAL_DEFINITIONS[i].signum, &g_sigaction, NULL); } return 0; @@ -1032,7 +1025,9 @@ process_ucontext_deferred(const sentry_ucontext_t *uctx, // after capturing the crash event, dump all the envelopes to disk sentry__transport_dump_queue(options->transport, options->run); - SENTRY_INFO("crash has been captured"); + // Use signal-safe logging here since this may run in signal handler + // context (fallback path) where stdio functions are not safe. + SENTRY_SIGNAL_SAFE_LOG("crash has been captured"); } } @@ -1263,10 +1258,18 @@ dispatch_ucontext(const sentry_ucontext_t *uctx, #ifdef SENTRY_WITH_UNWINDER_LIBBACKTRACE // For targets that still use `backtrace()` as the sole unwinder we must // run the signal-unsafe part in the signal handler like we did before. + // Disable stdio-based logging - not safe in signal handler context. + sentry__logger_disable(); process_ucontext_deferred(uctx, sig_slot, skip_hooks); return; #else if (has_handler_thread_crashed()) { + // Disable stdio-based logging since we're now in signal handler context + // where stdio functions are not safe. This is critical for abort() + // which may hold stdio locks when raising SIGABRT - using fprintf would + // deadlock. + sentry__logger_disable(); + // directly execute unsafe part in signal handler as a last chance to // report an error when the handler thread has crashed. // Always skip hooks here since the first attempt (from handler thread) @@ -1298,6 +1301,8 @@ dispatch_ucontext(const sentry_ucontext_t *uctx, // We are the first handler. Check if handler thread is available. if (!sentry__atomic_fetch(&g_handler_thread_ready)) { + // Disable stdio-based logging - not safe in signal handler context. + sentry__logger_disable(); process_ucontext_deferred(uctx, sig_slot, skip_hooks); return; } @@ -1355,6 +1360,8 @@ dispatch_ucontext(const sentry_ucontext_t *uctx, // Multiple recursive crashes - bail out return; } + // Disable stdio-based logging - not safe in signal handler context. + sentry__logger_disable(); process_ucontext_deferred(uctx, sig_slot, depth >= 2); return; } @@ -1370,6 +1377,8 @@ dispatch_ucontext(const sentry_ucontext_t *uctx, if (!handler_signaled) { // Fall back to in-handler processing + // Disable stdio-based logging - not safe in signal handler context. + sentry__logger_disable(); process_ucontext_deferred(uctx, sig_slot, skip_hooks); return; } From a45b32d624de180d5e1ad44eed8efdb1813896c2 Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Wed, 11 Feb 2026 09:45:33 +0100 Subject: [PATCH 104/130] don't allow recursive abort(), allowing a crash handler to handle a crash in the handler that is coming from an abort is extremely unstable and why might extend that to other signals as well, but those typically come from the kernel as hard-fault translations. However abort() comes from libc infra and is far from safe to keep running. We always assumed reports for crashed handlers to be a best effort topic, now we have way to detect those in the recursion and can act on particularly bad crashes in the handler to just skip. --- src/backends/sentry_backend_inproc.c | 20 +++++++++++++++--- tests/test_inproc_stress.py | 31 +++++++++++++++++++--------- tests/test_integration_stdout.py | 18 ++++++++++++++++ 3 files changed, 56 insertions(+), 13 deletions(-) diff --git a/src/backends/sentry_backend_inproc.c b/src/backends/sentry_backend_inproc.c index 17f16141f..a9256f5d4 100644 --- a/src/backends/sentry_backend_inproc.c +++ b/src/backends/sentry_backend_inproc.c @@ -1255,6 +1255,14 @@ dispatch_ucontext(const sentry_ucontext_t *uctx, // hook again, but still try to capture the crash bool skip_hooks = handler_depth >= 2; + // If SIGABRT occurs during recursive signal handling (depth >= 2), don't + // attempt to capture it. abort() holds stdio/libc internal locks that our + // crash capture code may need, which can lead to deadlocks or recursive + // aborts. + if (sig_slot && sig_slot->signum == SIGABRT && handler_depth >= 2) { + return; + } + #ifdef SENTRY_WITH_UNWINDER_LIBBACKTRACE // For targets that still use `backtrace()` as the sole unwinder we must // run the signal-unsafe part in the signal handler like we did before. @@ -1264,10 +1272,16 @@ dispatch_ucontext(const sentry_ucontext_t *uctx, return; #else if (has_handler_thread_crashed()) { + // If SIGABRT occurs on the handler thread, don't attempt to capture it. + // abort() holds stdio/libc internal locks that our crash capture code + // may need (snprintf for addresses, malloc, etc.), which can lead to + // deadlocks or recursive aborts. + if (sig_slot && sig_slot->signum == SIGABRT) { + return; + } + // Disable stdio-based logging since we're now in signal handler context - // where stdio functions are not safe. This is critical for abort() - // which may hold stdio locks when raising SIGABRT - using fprintf would - // deadlock. + // where stdio functions are not safe. sentry__logger_disable(); // directly execute unsafe part in signal handler as a last chance to diff --git a/tests/test_inproc_stress.py b/tests/test_inproc_stress.py index b492d6b87..6f7551dc7 100644 --- a/tests/test_inproc_stress.py +++ b/tests/test_inproc_stress.py @@ -327,11 +327,15 @@ def test_inproc_handler_thread_crash(cmake): def test_inproc_handler_abort_crash(cmake): """ - Test fallback when handler thread crashes via abort(). + Test behavior when handler thread crashes via abort(). - abort() has special behavior - it resets the signal mask before raising - SIGABRT. This tests that the recursive crash detection works even when - the signal mask is reset. + When abort() is called from the on_crash callback (running on the handler + thread), we intentionally do NOT attempt to capture the SIGABRT. This is + because abort() holds stdio/libc internal locks that our crash capture + code needs, which can lead to deadlocks or recursive aborts. + + The original crash processing is interrupted, so no envelope is written. + The crash marker should exist (written before on_crash is called). """ tmp_path = cmake( ["sentry"], @@ -356,15 +360,22 @@ def test_inproc_handler_abort_crash(cmake): "FATAL crash in handler thread" in stderr ), f"Handler thread crash not detected. stderr:\n{stderr}" + # We intentionally do NOT capture SIGABRT from abort() on handler thread + # to avoid deadlocks. The original crash processing was interrupted. assert ( - "crash has been captured" in stderr - ), f"Crash not captured via fallback. stderr:\n{stderr}" + "crash has been captured" not in stderr + ), f"Should not have attempted to capture abort(). stderr:\n{stderr}" + # Crash marker should exist (written before on_crash callback) assert_crash_marker(database_path) - envelope_path = assert_single_crash_envelope(database_path) - with open(envelope_path, "rb") as f: - envelope = Envelope.deserialize(f.read()) - assert_inproc_crash(envelope) + + # No envelope expected - processing was interrupted by abort() + run_dirs = list(database_path.glob("*.run")) + if run_dirs: + envelopes = list(run_dirs[0].glob("*.envelope")) + assert ( + len(envelopes) == 0 + ), f"Should not have envelope after abort(), found: {envelopes}" finally: shutil.rmtree(database_path, ignore_errors=True) diff --git a/tests/test_integration_stdout.py b/tests/test_integration_stdout.py index d7ed20ccb..ecbdac1a5 100644 --- a/tests/test_integration_stdout.py +++ b/tests/test_integration_stdout.py @@ -185,6 +185,24 @@ def test_inproc_crash_stdout(cmake): assert_inproc_crash(envelope) +def test_inproc_abort_stdout(cmake): + """Test that a normal abort() call is captured by inproc backend. + + This verifies that our SIGABRT handling changes (which bail out early + for abort() on the handler thread or during recursion) don't break + normal abort() capture from user code. + """ + tmp_path, output = run_stdout_for("inproc", cmake, ["attachment", "abort"]) + + envelope = Envelope.deserialize(output) + + assert_crash_timestamp(has_files, tmp_path) + assert_meta(envelope, integration="inproc") + assert_breadcrumb(envelope) + assert_attachment(envelope) + assert_inproc_crash(envelope) + + def test_inproc_crash_stdout_before_send(cmake): tmp_path, output = run_crash_stdout_for("inproc", cmake, ["before-send"]) From 6412e89b280055248a7f958ae8649dbace7ee83a Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Wed, 11 Feb 2026 10:52:27 +0100 Subject: [PATCH 105/130] guard the chain at start strategy against SIGABRT --- src/backends/sentry_backend_inproc.c | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/backends/sentry_backend_inproc.c b/src/backends/sentry_backend_inproc.c index a9256f5d4..5c41aeef1 100644 --- a/src/backends/sentry_backend_inproc.c +++ b/src/backends/sentry_backend_inproc.c @@ -1450,7 +1450,13 @@ process_ucontext(const sentry_ucontext_t *uctx) { #ifdef SENTRY_PLATFORM_LINUX if (g_backend_config.handler_strategy - == SENTRY_HANDLER_STRATEGY_CHAIN_AT_START) { + == SENTRY_HANDLER_STRATEGY_CHAIN_AT_START + && uctx->signum != SIGABRT) { + // SIGABRT is excluded: CLR/Mono never uses it for managed exception + // translation. Chaining SIGABRT to a SIG_DFL previous handler calls + // raise(SIGABRT), and with SA_NODEFER our handler re-enters + // immediately causing an infinite loop. + // // On Linux (and thus Android) CLR/Mono converts signals provoked by // AOT/JIT-generated native code into managed code exceptions. In these // cases, we shouldn't react to the signal at all and let their handler From 6170a11bed47ccc15de093db3a04a24d2f88c63a Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Wed, 11 Feb 2026 12:29:09 +0100 Subject: [PATCH 106/130] disable the abort dialog for Win32 in the example when abort() is triggered --- examples/example.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/examples/example.c b/examples/example.c index 33573c25a..59edef2f8 100644 --- a/examples/example.c +++ b/examples/example.c @@ -837,6 +837,10 @@ main(int argc, char **argv) assert(0); } if (has_arg(argc, argv, "abort")) { +#ifdef _WIN32 + // Suppress the Windows abort dialog that would block CI + _set_abort_behavior(0, _WRITE_ABORT_MSG | _CALL_REPORTFAULT); +#endif abort(); } #ifdef SENTRY_PLATFORM_UNIX From 5cc3a34dad585695fd85d1923754335a1d929965 Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Wed, 11 Feb 2026 13:01:08 +0100 Subject: [PATCH 107/130] extract abort check into a function so we can handle UNIX/Windows differences there --- src/backends/sentry_backend_inproc.c | 34 ++++++++++++++++++++++++---- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/src/backends/sentry_backend_inproc.c b/src/backends/sentry_backend_inproc.c index 5c41aeef1..6b7350182 100644 --- a/src/backends/sentry_backend_inproc.c +++ b/src/backends/sentry_backend_inproc.c @@ -1247,6 +1247,32 @@ has_handler_thread_crashed(void) return false; } +/** + * Returns true if the crash context represents an abort(). + * + * On UNIX, abort() delivers SIGABRT which populates sig_slot normally. + * On Windows, our handle_sigabrt() creates a synthetic exception with + * STATUS_FATAL_APP_EXIT, which is not in the SIGNAL_DEFINITIONS table and + * thus leaves sig_slot == NULL. We must check the exception code directly. + */ +static bool +is_abort(const sentry_ucontext_t *uctx, const struct signal_slot *sig_slot) +{ +#ifdef SENTRY_PLATFORM_UNIX + (void)uctx; + return sig_slot && sig_slot->signum == SIGABRT; +#elif defined(SENTRY_PLATFORM_WINDOWS) + (void)sig_slot; + return uctx->exception_ptrs.ExceptionRecord + && uctx->exception_ptrs.ExceptionRecord->ExceptionCode + == STATUS_FATAL_APP_EXIT; +#else + (void)uctx; + (void)sig_slot; + return false; +#endif +} + static void dispatch_ucontext(const sentry_ucontext_t *uctx, const struct signal_slot *sig_slot, int handler_depth) @@ -1255,11 +1281,11 @@ dispatch_ucontext(const sentry_ucontext_t *uctx, // hook again, but still try to capture the crash bool skip_hooks = handler_depth >= 2; - // If SIGABRT occurs during recursive signal handling (depth >= 2), don't + // If abort() occurs during recursive signal handling (depth >= 2), don't // attempt to capture it. abort() holds stdio/libc internal locks that our // crash capture code may need, which can lead to deadlocks or recursive // aborts. - if (sig_slot && sig_slot->signum == SIGABRT && handler_depth >= 2) { + if (is_abort(uctx, sig_slot) && handler_depth >= 2) { return; } @@ -1272,11 +1298,11 @@ dispatch_ucontext(const sentry_ucontext_t *uctx, return; #else if (has_handler_thread_crashed()) { - // If SIGABRT occurs on the handler thread, don't attempt to capture it. + // If abort() occurs on the handler thread, don't attempt to capture it. // abort() holds stdio/libc internal locks that our crash capture code // may need (snprintf for addresses, malloc, etc.), which can lead to // deadlocks or recursive aborts. - if (sig_slot && sig_slot->signum == SIGABRT) { + if (is_abort(uctx, sig_slot)) { return; } From 8c134352559ef7a22133ac1895a752af30321bf9 Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Wed, 11 Feb 2026 20:32:30 +0100 Subject: [PATCH 108/130] ensure handler thread started before we crash --- src/backends/sentry_backend_inproc.c | 30 ++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/backends/sentry_backend_inproc.c b/src/backends/sentry_backend_inproc.c index 6b7350182..3be29d974 100644 --- a/src/backends/sentry_backend_inproc.c +++ b/src/backends/sentry_backend_inproc.c @@ -1168,6 +1168,36 @@ start_handler_thread(void) return 1; } + // Wait for handler thread to be ready before returning. This eliminates + // the race where a crash could occur before g_handler_thread_ready is set, + // which would cause in-handler processing with unexpected behavior for + // callbacks that crash (e.g., calling abort()). + int timeout_counter = 1000000; + while ( + !sentry__atomic_fetch(&g_handler_thread_ready) && timeout_counter > 0) { + sentry__cpu_relax(); + timeout_counter--; + } + + if (!sentry__atomic_fetch(&g_handler_thread_ready)) { + SENTRY_WARN("handler thread failed to start in time"); + // Thread was spawned but didn't become ready - try to clean up. + // Set exit flag and hope the thread eventually sees it. + sentry__atomic_store(&g_handler_should_exit, 1); +#ifdef SENTRY_PLATFORM_UNIX + // Signal the pipe to unblock any pending read + if (g_handler_pipe[1] >= 0) { + close(g_handler_pipe[1]); + g_handler_pipe[1] = -1; + } +#elif defined(SENTRY_PLATFORM_WINDOWS) + if (g_handler_semaphore) { + ReleaseSemaphore(g_handler_semaphore, 1, NULL); + } +#endif + return 1; + } + return 0; } From b811e9ed237b5aff2ed34e3f64c76560b7fc9350 Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Wed, 11 Feb 2026 21:00:21 +0100 Subject: [PATCH 109/130] assume that inproc stress tests output UTF-8 on Windows and enable replace error handling --- tests/test_inproc_stress.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_inproc_stress.py b/tests/test_inproc_stress.py index 6f7551dc7..9f83f807c 100644 --- a/tests/test_inproc_stress.py +++ b/tests/test_inproc_stress.py @@ -97,7 +97,8 @@ def run_stress_test(tmp_path, test_executable, test_name, database_path=None): env=env, stdout=subprocess.PIPE, stderr=subprocess.PIPE, - text=True, + encoding="utf-8", + errors="replace", ) stdout, stderr = proc.communicate(timeout=30) From 64b8f37c703270e549f3dd376d98cdc76d59536c Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Thu, 12 Feb 2026 13:33:46 +0100 Subject: [PATCH 110/130] temporarily disable CI gate for inproc stress on android --- tests/test_inproc_stress.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_inproc_stress.py b/tests/test_inproc_stress.py index 9f83f807c..a5e0b7139 100644 --- a/tests/test_inproc_stress.py +++ b/tests/test_inproc_stress.py @@ -27,10 +27,10 @@ def is_ci(): # Skip Android tests in CI - adb shell execution is not stable enough # These tests can still be run locally for development/debugging -pytestmark = pytest.mark.skipif( - is_android() and is_ci(), - reason="Android inproc stress tests disabled in CI (adb execution unreliable)", -) +# pytestmark = pytest.mark.skipif( +# is_android() and is_ci(), +# reason="Android inproc stress tests disabled in CI (adb execution unreliable)", +# ) def adb(*args): From 5e9d739566f27696c87e2348286a5b43bd65f13b Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Thu, 12 Feb 2026 13:59:53 +0100 Subject: [PATCH 111/130] use a time-based timeout for the init wait on inproc handler_thread_ready --- src/backends/sentry_backend_inproc.c | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/src/backends/sentry_backend_inproc.c b/src/backends/sentry_backend_inproc.c index 3be29d974..0a7fd51bf 100644 --- a/src/backends/sentry_backend_inproc.c +++ b/src/backends/sentry_backend_inproc.c @@ -1168,15 +1168,20 @@ start_handler_thread(void) return 1; } - // Wait for handler thread to be ready before returning. This eliminates - // the race where a crash could occur before g_handler_thread_ready is set, - // which would cause in-handler processing with unexpected behavior for - // callbacks that crash (e.g., calling abort()). - int timeout_counter = 1000000; - while ( - !sentry__atomic_fetch(&g_handler_thread_ready) && timeout_counter > 0) { - sentry__cpu_relax(); - timeout_counter--; + // Wait for handler thread to be ready before returning. Use a time-based + // timeout (5 seconds) with periodic sleeps to ensure we give the handler + // thread enough time even on slow systems (e.g., Android emulators). + const int timeout_ms = 5000; + const int sleep_interval_ms = 10; + int elapsed_ms = 0; + while (!sentry__atomic_fetch(&g_handler_thread_ready) + && elapsed_ms < timeout_ms) { +#ifdef SENTRY_PLATFORM_WINDOWS + Sleep(sleep_interval_ms); +#else + usleep(sleep_interval_ms * 1000); +#endif + elapsed_ms += sleep_interval_ms; } if (!sentry__atomic_fetch(&g_handler_thread_ready)) { From 59a8ab04489318e6a92a56a3e814c61b3288498f Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Thu, 12 Feb 2026 14:00:11 +0100 Subject: [PATCH 112/130] permanently disable CI gate for inproc stress on android --- tests/test_inproc_stress.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/tests/test_inproc_stress.py b/tests/test_inproc_stress.py index a5e0b7139..2f455ac3b 100644 --- a/tests/test_inproc_stress.py +++ b/tests/test_inproc_stress.py @@ -21,18 +21,6 @@ def is_android(): return bool(os.environ.get("ANDROID_API")) -def is_ci(): - return os.environ.get("CI") == "true" or os.environ.get("GITHUB_ACTIONS") == "true" - - -# Skip Android tests in CI - adb shell execution is not stable enough -# These tests can still be run locally for development/debugging -# pytestmark = pytest.mark.skipif( -# is_android() and is_ci(), -# reason="Android inproc stress tests disabled in CI (adb execution unreliable)", -# ) - - def adb(*args): """Run an adb command.""" return subprocess.run( From 086c57e6a7c52f6fad702e0e6d859bb6fe12d4e5 Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Thu, 12 Feb 2026 14:23:41 +0100 Subject: [PATCH 113/130] clean up changelog --- CHANGELOG.md | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 042a1c0c0..24180c041 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,12 +2,20 @@ ## Unreleased +**Breaking**: + +- inproc(Linux): the `inproc` backend on Linux now depends on "nognu" `libunwind`. ([#1446](https://github.com/getsentry/sentry-native/pull/1446)) +- inproc: since we split `inproc` into signal-handler/UEF part and a separate handler thread, `before_send` and `on_crash` could be called from other threads than the one that crashed. While this was never part of the contract, if your code relies on this, it will no longer work. ([#1446](https://github.com/getsentry/sentry-native/pull/1446)) + **Features**: - Add new offline caching options to persist envelopes locally: `sentry_options_set_cache_keep`, `sentry_options_set_cache_max_items`, `sentry_options_set_cache_max_size`, and `sentry_options_set_cache_max_age`. ([#1490](https://github.com/getsentry/sentry-native/pull/1490), [#1493](https://github.com/getsentry/sentry-native/pull/1493)) **Fixes**: +- Make the signal-handler synchronization fully atomic to fix rare race scenarios. ([#1446](https://github.com/getsentry/sentry-native/pull/1446)) +- Reintroduce an FP-based stack-walker for macOS that can start from a user context. This also makes `inproc` backend functional again on macOS 13+. ([#1446](https://github.com/getsentry/sentry-native/pull/1446)) +- Split the `inproc` signal handler (and UEF on Windows) into a safe handler part and an "unsafe" handler thread. This minimizes exposure to undefined behavior inside the signal handler. ([#1446](https://github.com/getsentry/sentry-native/pull/1446)) - Remove spurious decref in `sentry_capture_user_feedback()` ([#1510](https://github.com/getsentry/sentry-native/pull/1510)) - Prevent double-decref of event in envelope add functions ([#1511](https://github.com/getsentry/sentry-native/pull/1511)) @@ -36,11 +44,6 @@ ## 0.12.3 -**Breaking**: - -- inproc(Linux): the `inproc` backend on Linux now depends on "nognu" `libunwind`. ([#1446](https://github.com/getsentry/sentry-native/pull/1446)) -- inproc: since we split `inproc` into signal-handler/UEF part and a separate handler thread, `before_send` and `on_crash` could be called from other threads than the one that crashed. While this was never part of the contract, if your code relies on this, it will no longer work. ([#1446](https://github.com/getsentry/sentry-native/pull/1446)) - **Fixes**: - Removed the 10-item limit per envelope for non-session data. Sessions are now limited to 100 per envelope, while other items (e.g., attachments) have no limit in amount. ([#1347](https://github.com/getsentry/sentry-native/pull/1347)) @@ -56,11 +59,6 @@ - Add logs flush on `sentry_flush()`. ([#1434](https://github.com/getsentry/sentry-native/pull/1434)) - Add global attributes API. These are added to all `sentry_log_X` calls. ([#1450](https://github.com/getsentry/sentry-native/pull/1450)) -**Fixes**: - -- Make the signal-handler synchronization fully atomic to fix rare race scenarios. ([#1446](https://github.com/getsentry/sentry-native/pull/1446)) -- Reintroduce an FP-based stack-walker for macOS that can start from a user context. This also makes `inproc` backend functional again on macOS 13+. ([#1446](https://github.com/getsentry/sentry-native/pull/1446)) -- Split the `inproc` signal handler (and UEF on Windows) into a safe handler part and an "unsafe" handler thread. This minimizes exposure to undefined behavior inside the signal handler. ([#1446](https://github.com/getsentry/sentry-native/pull/1446)) ## 0.12.1 From 5b525a4b6599a57ffff22579bbd6c9b3fe1494cd Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Thu, 12 Feb 2026 15:27:40 +0100 Subject: [PATCH 114/130] temporarily remove SIGABRT recursion gate --- src/backends/sentry_backend_inproc.c | 5 +++++ tests/test_inproc_stress.py | 29 ++++++++++++++-------------- 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/src/backends/sentry_backend_inproc.c b/src/backends/sentry_backend_inproc.c index 0a7fd51bf..8f2e1af23 100644 --- a/src/backends/sentry_backend_inproc.c +++ b/src/backends/sentry_backend_inproc.c @@ -1316,6 +1316,7 @@ dispatch_ucontext(const sentry_ucontext_t *uctx, // hook again, but still try to capture the crash bool skip_hooks = handler_depth >= 2; +#if 0 // If abort() occurs during recursive signal handling (depth >= 2), don't // attempt to capture it. abort() holds stdio/libc internal locks that our // crash capture code may need, which can lead to deadlocks or recursive @@ -1323,6 +1324,8 @@ dispatch_ucontext(const sentry_ucontext_t *uctx, if (is_abort(uctx, sig_slot) && handler_depth >= 2) { return; } +#else +#endif #ifdef SENTRY_WITH_UNWINDER_LIBBACKTRACE // For targets that still use `backtrace()` as the sole unwinder we must @@ -1333,6 +1336,7 @@ dispatch_ucontext(const sentry_ucontext_t *uctx, return; #else if (has_handler_thread_crashed()) { +# if 0 // If abort() occurs on the handler thread, don't attempt to capture it. // abort() holds stdio/libc internal locks that our crash capture code // may need (snprintf for addresses, malloc, etc.), which can lead to @@ -1340,6 +1344,7 @@ dispatch_ucontext(const sentry_ucontext_t *uctx, if (is_abort(uctx, sig_slot)) { return; } +# endif // Disable stdio-based logging since we're now in signal handler context // where stdio functions are not safe. diff --git a/tests/test_inproc_stress.py b/tests/test_inproc_stress.py index 2f455ac3b..2418e0b37 100644 --- a/tests/test_inproc_stress.py +++ b/tests/test_inproc_stress.py @@ -319,12 +319,12 @@ def test_inproc_handler_abort_crash(cmake): Test behavior when handler thread crashes via abort(). When abort() is called from the on_crash callback (running on the handler - thread), we intentionally do NOT attempt to capture the SIGABRT. This is - because abort() holds stdio/libc internal locks that our crash capture - code needs, which can lead to deadlocks or recursive aborts. + thread), we detect this is a handler thread crash and fall back to + processing in the signal handler context (with hooks skipped to avoid + calling the crashing hook again). - The original crash processing is interrupted, so no envelope is written. - The crash marker should exist (written before on_crash is called). + The crash marker should exist (written before on_crash callback), and + an envelope should be written for the abort crash. """ tmp_path = cmake( ["sentry"], @@ -349,22 +349,21 @@ def test_inproc_handler_abort_crash(cmake): "FATAL crash in handler thread" in stderr ), f"Handler thread crash not detected. stderr:\n{stderr}" - # We intentionally do NOT capture SIGABRT from abort() on handler thread - # to avoid deadlocks. The original crash processing was interrupted. + # The abort crash should be captured via the fallback path assert ( - "crash has been captured" not in stderr - ), f"Should not have attempted to capture abort(). stderr:\n{stderr}" + "crash has been captured" in stderr + ), f"Crash should have been captured. stderr:\n{stderr}" # Crash marker should exist (written before on_crash callback) assert_crash_marker(database_path) - # No envelope expected - processing was interrupted by abort() + # An envelope should be written for the abort crash run_dirs = list(database_path.glob("*.run")) - if run_dirs: - envelopes = list(run_dirs[0].glob("*.envelope")) - assert ( - len(envelopes) == 0 - ), f"Should not have envelope after abort(), found: {envelopes}" + assert len(run_dirs) == 1, f"Expected 1 run dir, found: {run_dirs}" + envelopes = list(run_dirs[0].glob("*.envelope")) + assert ( + len(envelopes) == 1 + ), f"Expected 1 envelope after abort(), found: {envelopes}" finally: shutil.rmtree(database_path, ignore_errors=True) From d488c9acaaa0fdb693bbdb773e4e76a6b3cebe29 Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Thu, 12 Feb 2026 15:33:14 +0100 Subject: [PATCH 115/130] update changelog --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 24180c041..3b1d05acf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ **Features**: - Add new offline caching options to persist envelopes locally: `sentry_options_set_cache_keep`, `sentry_options_set_cache_max_items`, `sentry_options_set_cache_max_size`, and `sentry_options_set_cache_max_age`. ([#1490](https://github.com/getsentry/sentry-native/pull/1490), [#1493](https://github.com/getsentry/sentry-native/pull/1493)) +- Add support for `abort()` in the `inproc` backend on Windows. ([#1446](https://github.com/getsentry/sentry-native/pull/1446)) **Fixes**: @@ -19,6 +20,10 @@ - Remove spurious decref in `sentry_capture_user_feedback()` ([#1510](https://github.com/getsentry/sentry-native/pull/1510)) - Prevent double-decref of event in envelope add functions ([#1511](https://github.com/getsentry/sentry-native/pull/1511)) +**Internal**: + +- Introduce PAC tests for `arm64e` on macOS. ([#1446](https://github.com/getsentry/sentry-native/pull/1446)) + ## 0.12.6 **Features**: From b85b23d9fe3c663ecc2857e372bb1b7987ddb973 Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Thu, 12 Feb 2026 15:39:06 +0100 Subject: [PATCH 116/130] also hide is_abort() for all Werror builds --- src/backends/sentry_backend_inproc.c | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/backends/sentry_backend_inproc.c b/src/backends/sentry_backend_inproc.c index 8f2e1af23..4fe6ca140 100644 --- a/src/backends/sentry_backend_inproc.c +++ b/src/backends/sentry_backend_inproc.c @@ -1290,23 +1290,25 @@ has_handler_thread_crashed(void) * STATUS_FATAL_APP_EXIT, which is not in the SIGNAL_DEFINITIONS table and * thus leaves sig_slot == NULL. We must check the exception code directly. */ +#if 0 static bool is_abort(const sentry_ucontext_t *uctx, const struct signal_slot *sig_slot) { -#ifdef SENTRY_PLATFORM_UNIX +# ifdef SENTRY_PLATFORM_UNIX (void)uctx; return sig_slot && sig_slot->signum == SIGABRT; -#elif defined(SENTRY_PLATFORM_WINDOWS) +# elif defined(SENTRY_PLATFORM_WINDOWS) (void)sig_slot; return uctx->exception_ptrs.ExceptionRecord && uctx->exception_ptrs.ExceptionRecord->ExceptionCode == STATUS_FATAL_APP_EXIT; -#else +# else (void)uctx; (void)sig_slot; return false; -#endif +# endif } +#endif static void dispatch_ucontext(const sentry_ucontext_t *uctx, @@ -1324,7 +1326,6 @@ dispatch_ucontext(const sentry_ucontext_t *uctx, if (is_abort(uctx, sig_slot) && handler_depth >= 2) { return; } -#else #endif #ifdef SENTRY_WITH_UNWINDER_LIBBACKTRACE From e628e03461d793572056d21dc0e472d30754fe53 Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Thu, 12 Feb 2026 17:05:33 +0100 Subject: [PATCH 117/130] delete data in crashpad_backend_free() rather than sentry_free since it was allocated with new. --- src/backends/sentry_backend_crashpad.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backends/sentry_backend_crashpad.cpp b/src/backends/sentry_backend_crashpad.cpp index 1f60e6205..9815bdb42 100644 --- a/src/backends/sentry_backend_crashpad.cpp +++ b/src/backends/sentry_backend_crashpad.cpp @@ -855,7 +855,7 @@ crashpad_backend_free(sentry_backend_t *backend) sentry__path_free(data->breadcrumb1_path); sentry__path_free(data->breadcrumb2_path); sentry__path_free(data->external_report_path); - sentry_free(data); + delete data; } static void From a0dc74913271098151fc158250697de649adab7c Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Thu, 12 Feb 2026 17:08:07 +0100 Subject: [PATCH 118/130] add signal-safe address formatter --- CHANGELOG.md | 5 ++- src/modulefinder/sentry_modulefinder_apple.c | 3 +- src/sentry_string.c | 40 ++++++++++++++++++++ src/sentry_string.h | 9 +++++ src/sentry_value.c | 6 +-- tests/unit/CMakeLists.txt | 1 + tests/unit/test_string.c | 30 +++++++++++++++ tests/unit/tests.inc | 1 + 8 files changed, 87 insertions(+), 8 deletions(-) create mode 100644 tests/unit/test_string.c diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b1d05acf..0f5cbe508 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,11 +14,12 @@ **Fixes**: +- Remove spurious decref in `sentry_capture_user_feedback()` ([#1510](https://github.com/getsentry/sentry-native/pull/1510)) +- Prevent double-decref of event in envelope add functions ([#1511](https://github.com/getsentry/sentry-native/pull/1511)) - Make the signal-handler synchronization fully atomic to fix rare race scenarios. ([#1446](https://github.com/getsentry/sentry-native/pull/1446)) - Reintroduce an FP-based stack-walker for macOS that can start from a user context. This also makes `inproc` backend functional again on macOS 13+. ([#1446](https://github.com/getsentry/sentry-native/pull/1446)) - Split the `inproc` signal handler (and UEF on Windows) into a safe handler part and an "unsafe" handler thread. This minimizes exposure to undefined behavior inside the signal handler. ([#1446](https://github.com/getsentry/sentry-native/pull/1446)) -- Remove spurious decref in `sentry_capture_user_feedback()` ([#1510](https://github.com/getsentry/sentry-native/pull/1510)) -- Prevent double-decref of event in envelope add functions ([#1511](https://github.com/getsentry/sentry-native/pull/1511)) +- Use a signal-safe address formatter instead of `snprintf()`. ([#1446](https://github.com/getsentry/sentry-native/pull/1446)) **Internal**: diff --git a/src/modulefinder/sentry_modulefinder_apple.c b/src/modulefinder/sentry_modulefinder_apple.c index 26ef5088e..b7c8d7266 100644 --- a/src/modulefinder/sentry_modulefinder_apple.c +++ b/src/modulefinder/sentry_modulefinder_apple.c @@ -106,7 +106,8 @@ remove_image(const struct mach_header *mh, intptr_t UNUSED(vmaddr_slide)) } char ref_addr[32]; - snprintf(ref_addr, sizeof(ref_addr), "0x%llx", (long long)info.dli_fbase); + sentry__addr_to_string( + ref_addr, sizeof(ref_addr), (uint64_t)info.dli_fbase); sentry_value_t new_modules = sentry_value_new_list(); for (size_t i = 0; i < sentry_value_get_length(g_modules); i++) { diff --git a/src/sentry_string.c b/src/sentry_string.c index 5952888dd..76f4ce0be 100644 --- a/src/sentry_string.c +++ b/src/sentry_string.c @@ -7,6 +7,46 @@ # include #endif +// "0x" + 16 nibbles + NUL +#define SENTRY_ADDR_MIN_BUFFER_SIZE 19 +/** + * We collect hex digits into a small stack scratch buffer (in reverse order) + * and then copy them forward. This avoids reverse-writing into the destination + * and keeps the code simple. + */ +bool +sentry__addr_to_string(char *buf, size_t buf_len, uint64_t addr) +{ + static const char hex[] = "0123456789abcdef"; + + if (!buf || buf_len < SENTRY_ADDR_MIN_BUFFER_SIZE) { + return false; + } + + size_t buf_idx = 0; + buf[buf_idx++] = '0'; + buf[buf_idx++] = 'x'; + + // fill a reverse buffer from each nibble + char rev[2 * sizeof(uint64_t)]; + size_t rev_idx = 0; + if (addr == 0) { + rev[rev_idx++] = '0'; + } else { + while (addr && rev_idx < sizeof(rev)) { + rev[rev_idx++] = hex[addr & 0xF]; + addr >>= 4; + } + } + + // read rev into buf from its end + while (rev_idx && buf_idx + 1 < buf_len) { + buf[buf_idx++] = rev[--rev_idx]; + } + buf[buf_idx] = '\0'; + return true; +} + #define INITIAL_BUFFER_SIZE 128 void diff --git a/src/sentry_string.h b/src/sentry_string.h index 710924e7f..74dba6812 100644 --- a/src/sentry_string.h +++ b/src/sentry_string.h @@ -204,6 +204,15 @@ sentry__uint64_to_string(uint64_t val) return sentry__string_clone(buf); } +/** + * Formats an address as "0x" + lowercase hex into a caller-provided buffer. + * This is a replacement for `snprintf` in signal handlers: + * - signal-safe: uses no stdio, malloc, locks, or thread-local state. + * - reentrant: only stack locals; no writable globals. + * Returns 0 on success. + */ +bool sentry__addr_to_string(char *buf, size_t buf_len, uint64_t addr); + #ifdef SENTRY_PLATFORM_WINDOWS /** * Create a utf-8 string from a Wide String. diff --git a/src/sentry_value.c b/src/sentry_value.c index f58793646..978251e3b 100644 --- a/src/sentry_value.c +++ b/src/sentry_value.c @@ -1281,11 +1281,7 @@ sentry_value_t sentry__value_new_addr(uint64_t addr) { char buf[32]; - size_t written = (size_t)snprintf(buf, sizeof(buf), "0x%" PRIx64, addr); - if (written >= sizeof(buf)) { - return sentry_value_new_null(); - } - buf[written] = '\0'; + sentry__addr_to_string(buf, sizeof(buf), addr); return sentry_value_new_string(buf); } diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt index 444f54b0b..359ec43f9 100644 --- a/tests/unit/CMakeLists.txt +++ b/tests/unit/CMakeLists.txt @@ -47,6 +47,7 @@ add_executable(sentry_test_unit test_scope.c test_session.c test_slice.c + test_string.c test_symbolizer.c test_sync.c test_tracing.c diff --git a/tests/unit/test_string.c b/tests/unit/test_string.c new file mode 100644 index 000000000..317084906 --- /dev/null +++ b/tests/unit/test_string.c @@ -0,0 +1,30 @@ +#include "sentry_random.h" +#include "sentry_string.h" +#include "sentry_testsupport.h" + +void +assert_addr_value_equals_format(uint64_t addr) +{ + char our_buf[32]; + char stdio_buf[32]; + sentry__addr_to_string(our_buf, sizeof(our_buf), addr); + snprintf(stdio_buf, sizeof(stdio_buf), "0x%" PRIx64, addr); + TEST_CHECK_STRING_EQUAL(our_buf, stdio_buf); +} + +SENTRY_TEST(string_address_format) +{ + assert_addr_value_equals_format(0); + assert_addr_value_equals_format(0xf000000000000000); + assert_addr_value_equals_format(0x000000000000000f); + assert_addr_value_equals_format(0x0000ffff0000ffff); + assert_addr_value_equals_format(0x0000ffffffff0000); + assert_addr_value_equals_format(0xf0f0f0f0f0f0f0f0); + assert_addr_value_equals_format(0x0f0f0f0f0f0f0f0f); + uint64_t rnd; + for (int i = 0; i < 1000000; ++i) { + TEST_ASSERT(!sentry__getrandom(&rnd, sizeof(rnd))); + assert_addr_value_equals_format(rnd); + } + assert_addr_value_equals_format(0xffffffffffffffff); +} diff --git a/tests/unit/tests.inc b/tests/unit/tests.inc index 0a5cb2e9d..6686e78d2 100644 --- a/tests/unit/tests.inc +++ b/tests/unit/tests.inc @@ -190,6 +190,7 @@ XX(span_tagging_n) XX(spans_on_scope) XX(stack_guarantee) XX(stack_guarantee_auto_init) +XX(string_address_format) XX(symbolizer) XX(task_queue) XX(thread_without_name_still_valid) From bbf2857830dbf32b2f4fed75e3efe959f7bae51a Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Thu, 12 Feb 2026 17:10:38 +0100 Subject: [PATCH 119/130] remove `abort()` recursion-gate completely --- src/backends/sentry_backend_inproc.c | 48 ---------------------------- 1 file changed, 48 deletions(-) diff --git a/src/backends/sentry_backend_inproc.c b/src/backends/sentry_backend_inproc.c index 4fe6ca140..8adc0d2d0 100644 --- a/src/backends/sentry_backend_inproc.c +++ b/src/backends/sentry_backend_inproc.c @@ -1282,34 +1282,6 @@ has_handler_thread_crashed(void) return false; } -/** - * Returns true if the crash context represents an abort(). - * - * On UNIX, abort() delivers SIGABRT which populates sig_slot normally. - * On Windows, our handle_sigabrt() creates a synthetic exception with - * STATUS_FATAL_APP_EXIT, which is not in the SIGNAL_DEFINITIONS table and - * thus leaves sig_slot == NULL. We must check the exception code directly. - */ -#if 0 -static bool -is_abort(const sentry_ucontext_t *uctx, const struct signal_slot *sig_slot) -{ -# ifdef SENTRY_PLATFORM_UNIX - (void)uctx; - return sig_slot && sig_slot->signum == SIGABRT; -# elif defined(SENTRY_PLATFORM_WINDOWS) - (void)sig_slot; - return uctx->exception_ptrs.ExceptionRecord - && uctx->exception_ptrs.ExceptionRecord->ExceptionCode - == STATUS_FATAL_APP_EXIT; -# else - (void)uctx; - (void)sig_slot; - return false; -# endif -} -#endif - static void dispatch_ucontext(const sentry_ucontext_t *uctx, const struct signal_slot *sig_slot, int handler_depth) @@ -1318,16 +1290,6 @@ dispatch_ucontext(const sentry_ucontext_t *uctx, // hook again, but still try to capture the crash bool skip_hooks = handler_depth >= 2; -#if 0 - // If abort() occurs during recursive signal handling (depth >= 2), don't - // attempt to capture it. abort() holds stdio/libc internal locks that our - // crash capture code may need, which can lead to deadlocks or recursive - // aborts. - if (is_abort(uctx, sig_slot) && handler_depth >= 2) { - return; - } -#endif - #ifdef SENTRY_WITH_UNWINDER_LIBBACKTRACE // For targets that still use `backtrace()` as the sole unwinder we must // run the signal-unsafe part in the signal handler like we did before. @@ -1337,16 +1299,6 @@ dispatch_ucontext(const sentry_ucontext_t *uctx, return; #else if (has_handler_thread_crashed()) { -# if 0 - // If abort() occurs on the handler thread, don't attempt to capture it. - // abort() holds stdio/libc internal locks that our crash capture code - // may need (snprintf for addresses, malloc, etc.), which can lead to - // deadlocks or recursive aborts. - if (is_abort(uctx, sig_slot)) { - return; - } -# endif - // Disable stdio-based logging since we're now in signal handler context // where stdio functions are not safe. sentry__logger_disable(); From 69834121d5e3492cfad8e728a12b3077807fe71a Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Thu, 12 Feb 2026 17:51:12 +0100 Subject: [PATCH 120/130] update README.md to remove inproc exceptions. --- README.md | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 0522200c4..c0d0dbcb4 100644 --- a/README.md +++ b/README.md @@ -301,27 +301,26 @@ using `cmake -D BUILD_SHARED_LIBS=OFF ..`. ### Support Matrix -| Feature | Windows | macOS | Linux | Android | iOS | -|------------|---------|-------|-------|---------|-------| -| Transports | | | | | | -| - curl | | ☑ | ☑ | (✓)*** | | -| - winhttp | ☑ | | | | | -| - none | ✓ | ✓ | ✓ | ☑ | ☑ | -| | | | | | | -| Backends | | | | | | -| - crashpad | ☑ | ☑ | ☑ | | | -| - breakpad | ✓ | ✓ | ✓ | (✓)** | (✓)** | -| - inproc | ✓ | (✓)* | ✓ | ☑ | | -| - none | ✓ | ✓ | ✓ | ✓ | | +| Feature | Windows | macOS | Linux | Android | iOS | +|------------|---------|-------|-------|---------|------| +| Transports | | | | | | +| - curl | | ☑ | ☑ | (✓)** | | +| - winhttp | ☑ | | | | | +| - none | ✓ | ✓ | ✓ | ☑ | ☑ | +| | | | | | | +| Backends | | | | | | +| - crashpad | ☑ | ☑ | ☑ | | | +| - breakpad | ✓ | ✓ | ✓ | (✓)* | (✓)* | +| - inproc | ✓ | ✓ | ✓ | ☑ | | +| - none | ✓ | ✓ | ✓ | ✓ | | Legend: - ☑ default - ✓ supported - (✓) supported with limitations -- `*`: `inproc` has not produced valid stack traces on macOS since version 13 ("Ventura"). Tracking: https://github.com/getsentry/sentry-native/issues/906 -- `**`: `breakpad` on Android and iOS builds and should work according to upstream but is untested. -- `***`: `curl` as a transport works on Android but isn't used in any supported configuration to reduce the size of our artifacts. +- `*`: `breakpad` on Android and iOS builds and should work according to upstream but is untested. +- `**`: `curl` as a transport works on Android but isn't used in any supported configuration to reduce the size of our artifacts. In addition to platform support, the "Advanced Usage" section of the SDK docs now [describes the tradeoffs](https://docs.sentry.io/platforms/native/advanced-usage/backend-tradeoffs/) involved in choosing a suitable backend for a particular use case. From fd7858ba2e8365b7bee655c35aee750946b09079 Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Thu, 12 Feb 2026 17:57:07 +0100 Subject: [PATCH 121/130] ensure top-frames are put verbatim into the stack-trace --- src/unwinder/sentry_unwinder_libunwind_mac.c | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/unwinder/sentry_unwinder_libunwind_mac.c b/src/unwinder/sentry_unwinder_libunwind_mac.c index becf59b0a..30639701c 100644 --- a/src/unwinder/sentry_unwinder_libunwind_mac.c +++ b/src/unwinder/sentry_unwinder_libunwind_mac.c @@ -126,12 +126,12 @@ fp_walk_from_uctx(const sentry_ucontext_t *uctx, void **ptrs, size_t max_frames) lr = STRIP_PAC((uintptr_t)mctx->__ss.__lr); # endif - // top frame: adjust pc−1 so it symbolizes inside the function + // top frame: no adjustment if (pc && n < max_frames) { - ptrs[n++] = (void *)(pc - 1); + ptrs[n++] = (void *)pc; } - // next frame is from saved LR at current FP record + // next frame is from saved LR at current FP record (adjust -1) if (lr && n < max_frames) { ptrs[n++] = (void *)(lr - 1); } @@ -141,9 +141,9 @@ fp_walk_from_uctx(const sentry_ucontext_t *uctx, void **ptrs, size_t max_frames) uintptr_t ip = (uintptr_t)mctx->__ss.__rip; uintptr_t bp = (uintptr_t)mctx->__ss.__rbp; - // top frame: adjust ip−1 so it symbolizes inside the function + // top frame: no adjustment if (ip && n < max_frames) { - ptrs[n++] = (void *)(ip - 1); + ptrs[n++] = (void *)ip; } fp_walk(bp, &n, ptrs, max_frames); From 3369db2154a6dc0167cfb72af82ceae29a3a9aa6 Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Thu, 12 Feb 2026 18:05:03 +0100 Subject: [PATCH 122/130] add libunwind as build dependency for inproc to the CONTRIBUTING.md --- CONTRIBUTING.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d70145828..82dcca29d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -12,7 +12,8 @@ Building and testing `sentry-native` currently requires the following tools: - **CMake** and a supported C/C++ compiler, to actually build the code. - **python** and **pytest**, to run integration tests. - **clang-format** and **black**, to format the C/C++ and python code respectively. -- **curl** and **zlib** libraries (e.g. on Ubuntu: libcurl4-openssl-dev, libz-dev) +- **curl** and **zlib** libraries (e.g. on Ubuntu: `libcurl4-openssl-dev`, `libz-dev`) +- if you run `inproc` on Linux you need `libunwind` (from `nognu`, not `llvm`) (e.g. on Ubuntu: `libunwind-dev`) `pytest`, `clang-format` and `black` are installed as virtualenv dependencies automatically. From ec525a3ba59054af3fc446716d83a8f180246525 Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Thu, 12 Feb 2026 18:31:03 +0100 Subject: [PATCH 123/130] Terminate abort() if only SIG_DFL or SIG_IGN are left. --- src/backends/sentry_backend_inproc.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/backends/sentry_backend_inproc.c b/src/backends/sentry_backend_inproc.c index 8adc0d2d0..3302d95de 100644 --- a/src/backends/sentry_backend_inproc.c +++ b/src/backends/sentry_backend_inproc.c @@ -442,6 +442,9 @@ handle_sigabrt(int signum) && g_previous_sigabrt_handler != SIG_IGN) { g_previous_sigabrt_handler(signum); } + + // Terminate the process - abort() must not return + TerminateProcess(GetCurrentProcess(), 3); } static int From 66103ee91f60b2a6054b9fed7c101aebe5704df7 Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Thu, 12 Feb 2026 18:46:22 +0100 Subject: [PATCH 124/130] improve report by explicitly providing a string for STATUS_FATAL_APP_EXIT --- src/backends/sentry_backend_inproc.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/backends/sentry_backend_inproc.c b/src/backends/sentry_backend_inproc.c index 3302d95de..065c84fde 100644 --- a/src/backends/sentry_backend_inproc.c +++ b/src/backends/sentry_backend_inproc.c @@ -375,7 +375,7 @@ struct signal_slot { const char *sigdesc; }; -# define SIGNAL_COUNT 20 +# define SIGNAL_COUNT 21 static LPTOP_LEVEL_EXCEPTION_FILTER g_previous_handler = NULL; @@ -399,7 +399,8 @@ static const struct signal_slot SIGNAL_DEFINITIONS[SIGNAL_COUNT] = { SIGNAL_DEF(EXCEPTION_NONCONTINUABLE_EXCEPTION, "NonContinuableException"), SIGNAL_DEF(EXCEPTION_PRIV_INSTRUCTION, "PrivilgedInstruction"), SIGNAL_DEF(EXCEPTION_SINGLE_STEP, "SingleStep"), - SIGNAL_DEF(EXCEPTION_STACK_OVERFLOW, "StackOverflow") + SIGNAL_DEF(EXCEPTION_STACK_OVERFLOW, "StackOverflow"), + SIGNAL_DEF(STATUS_FATAL_APP_EXIT, "FatalAppExit"), }; static LONG WINAPI handle_exception(EXCEPTION_POINTERS *); From abad6bc9720ec7646b0395e65e362cc32feddf0d Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Thu, 12 Feb 2026 18:49:59 +0100 Subject: [PATCH 125/130] fully clean up after a handler thread start timed out --- src/backends/sentry_backend_inproc.c | 38 +++++++++++++++++++++++++--- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/src/backends/sentry_backend_inproc.c b/src/backends/sentry_backend_inproc.c index 065c84fde..7a72175f6 100644 --- a/src/backends/sentry_backend_inproc.c +++ b/src/backends/sentry_backend_inproc.c @@ -1190,11 +1190,12 @@ start_handler_thread(void) if (!sentry__atomic_fetch(&g_handler_thread_ready)) { SENTRY_WARN("handler thread failed to start in time"); - // Thread was spawned but didn't become ready - try to clean up. - // Set exit flag and hope the thread eventually sees it. + // Thread was spawned but didn't become ready. Signal it to exit, + // join it, and clean up all resources. sentry__atomic_store(&g_handler_should_exit, 1); #ifdef SENTRY_PLATFORM_UNIX - // Signal the pipe to unblock any pending read + // Close the write end of the pipe to unblock any pending read() + // in the handler thread, causing it to see EOF and exit. if (g_handler_pipe[1] >= 0) { close(g_handler_pipe[1]); g_handler_pipe[1] = -1; @@ -1203,6 +1204,37 @@ start_handler_thread(void) if (g_handler_semaphore) { ReleaseSemaphore(g_handler_semaphore, 1, NULL); } +#endif + sentry__thread_join(g_handler_thread); + sentry__thread_free(&g_handler_thread); + + // The thread may have set g_handler_thread_ready before exiting; + // ensure it's cleared so we don't appear "started". + sentry__atomic_store(&g_handler_thread_ready, 0); + + // Clean up remaining resources +#ifdef SENTRY_PLATFORM_UNIX + if (g_handler_pipe[0] >= 0) { + close(g_handler_pipe[0]); + g_handler_pipe[0] = -1; + } + if (g_handler_ack_pipe[0] >= 0) { + close(g_handler_ack_pipe[0]); + g_handler_ack_pipe[0] = -1; + } + if (g_handler_ack_pipe[1] >= 0) { + close(g_handler_ack_pipe[1]); + g_handler_ack_pipe[1] = -1; + } +#elif defined(SENTRY_PLATFORM_WINDOWS) + if (g_handler_semaphore) { + CloseHandle(g_handler_semaphore); + g_handler_semaphore = NULL; + } + if (g_handler_ack_semaphore) { + CloseHandle(g_handler_ack_semaphore); + g_handler_ack_semaphore = NULL; + } #endif return 1; } From 64752356efaee974366ad31d27e28a261a66ccb3 Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Thu, 12 Feb 2026 22:19:26 +0100 Subject: [PATCH 126/130] reset previous SIGABRT handler on Windows unconditionally --- src/backends/sentry_backend_inproc.c | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/backends/sentry_backend_inproc.c b/src/backends/sentry_backend_inproc.c index 7a72175f6..6cb266887 100644 --- a/src/backends/sentry_backend_inproc.c +++ b/src/backends/sentry_backend_inproc.c @@ -486,11 +486,10 @@ shutdown_inproc_backend(sentry_backend_t *backend) SetUnhandledExceptionFilter(current_handler); } - // Restore previous SIGABRT handler - if (g_previous_sigabrt_handler) { - signal(SIGABRT, g_previous_sigabrt_handler); - g_previous_sigabrt_handler = NULL; - } + // Restore previous SIGABRT handler (unconditionally, since SIG_DFL is + // typically NULL on MSVC and a conditional check would skip restoration) + signal(SIGABRT, g_previous_sigabrt_handler); + g_previous_sigabrt_handler = NULL; if (backend) { backend->data = NULL; From ce5b2a1305f7546b31815517edd860d2e5be2a3f Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Thu, 12 Feb 2026 22:44:02 +0100 Subject: [PATCH 127/130] handle frame duplication when LR matches the LR in current frame record --- src/unwinder/sentry_unwinder_libunwind_mac.c | 28 ++- tests/fixtures/inproc_stress/main.c | 174 +++++++++++++++++++ tests/test_inproc_stress.py | 104 +++++++++++ 3 files changed, 303 insertions(+), 3 deletions(-) diff --git a/src/unwinder/sentry_unwinder_libunwind_mac.c b/src/unwinder/sentry_unwinder_libunwind_mac.c index 30639701c..39bd1a4c6 100644 --- a/src/unwinder/sentry_unwinder_libunwind_mac.c +++ b/src/unwinder/sentry_unwinder_libunwind_mac.c @@ -126,17 +126,39 @@ fp_walk_from_uctx(const sentry_ucontext_t *uctx, void **ptrs, size_t max_frames) lr = STRIP_PAC((uintptr_t)mctx->__ss.__lr); # endif - // top frame: no adjustment + // top frame: crash PC points to the faulting instruction, no adjustment if (pc && n < max_frames) { ptrs[n++] = (void *)pc; } - // next frame is from saved LR at current FP record (adjust -1) + // Emit LR as the next frame (return address, so adjust -1). if (lr && n < max_frames) { ptrs[n++] = (void *)(lr - 1); } - fp_walk(fp, &n, ptrs, max_frames); + // Determine where to start the frame pointer walk: + // If LR matches the saved LR in the current frame record, + // the crashing function has a frame and hasn't made sub-calls since its + // prologue + // -> walking from fp would produce a duplicate. + // -> Skip to the caller's frame pointer. + // + // When they differ, + // either the function has no frame record (fp belongs to the caller, and LR + // is the only reference to the caller frame) + // or + // it sub-calls (LR is a stale return within the crashing function). + // + // In both cases, walking from fp captures the correct remaining frames. + if (valid_ptr(fp)) { + const uintptr_t *record = (uintptr_t *)fp; + uintptr_t saved_lr = STRIP_PAC(record[1]); + if (lr == saved_lr) { + fp_walk(record[0], &n, ptrs, max_frames); + } else { + fp_walk(fp, &n, ptrs, max_frames); + } + } #elif defined(__x86_64__) uintptr_t ip = (uintptr_t)mctx->__ss.__rip; uintptr_t bp = (uintptr_t)mctx->__ss.__rbp; diff --git a/tests/fixtures/inproc_stress/main.c b/tests/fixtures/inproc_stress/main.c index b3fc3238c..693aabc77 100644 --- a/tests/fixtures/inproc_stress/main.c +++ b/tests/fixtures/inproc_stress/main.c @@ -29,6 +29,117 @@ extern void run_concurrent_crash(void); static void *invalid_mem = (void *)1; +// Prevent inlining and optimization to ensure predictable frame records. +#define NOINLINE __attribute__((noinline, optnone)) + +// Stack trace test functions (used on macOS because that uses a handwritten stack-walker, can compile on Linux/Unix). +// The naming convention encodes the expected call chain so the Python test +// can verify frames by function name. +// +// Scenario 1: crash in function with frame record, no sub-calls since prologue +// stacktest_A_calls_B_no_subcalls -> stacktest_B_crash_no_subcalls +// +// Scenario 2: crash in function with frame record, made sub-calls before crash +// stacktest_A_calls_B_with_subcalls -> stacktest_B_crash_after_subcall +// (B calls stacktest_C_helper which returns, then B crashes) +// +// Scenario 3: crash in function without a frame record +// stacktest_A_calls_B_no_frame_record -> stacktest_B_crash_no_frame_record +#ifndef _WIN32 + +static volatile int g_side_effect = 0; + +// Has a frame record (explicit prologue) but makes no calls, so LR still +// holds the return address from the caller's bl, matching [FP+8]. +# if defined(__aarch64__) +__attribute__((naked, noinline)) static void +stacktest_B_crash_no_subcalls(void) +{ + __asm__ volatile( + "stp x29, x30, [sp, #-16]!\n\t" // prologue: save fp, lr + "mov x29, sp\n\t" // set up frame pointer + "mov x8, #1\n\t" + "str wzr, [x8]\n\t" // store to address 0x1 -> SIGSEGV + ); +} +# elif defined(__x86_64__) +__attribute__((naked, noinline)) static void +stacktest_B_crash_no_subcalls(void) +{ + __asm__ volatile( + "pushq %%rbp\n\t" // prologue: save bp + "movq %%rsp, %%rbp\n\t" // set up frame pointer + "movl $0, 0x1\n\t" // store to address 0x1 -> SIGSEGV + ::: "memory" + ); +} +# endif + +NOINLINE static void +stacktest_A_calls_B_no_subcalls(void) +{ + fprintf(stderr, "stacktest_A_calls_B_no_subcalls\n"); + fflush(stderr); + stacktest_B_crash_no_subcalls(); +} + +NOINLINE static void +stacktest_C_helper(void) +{ + // A function that returns normally, just to modify LR in the caller + g_side_effect = 42; +} + +NOINLINE static void +stacktest_B_crash_after_subcall(void) +{ + fprintf(stderr, "stacktest_B_crash_after_subcall\n"); + fflush(stderr); + // Call a helper (modifies LR), then crash without another call + stacktest_C_helper(); + *(volatile int *)invalid_mem = 0xdead; +} + +NOINLINE static void +stacktest_A_calls_B_with_subcalls(void) +{ + fprintf(stderr, "stacktest_A_calls_B_with_subcalls\n"); + fflush(stderr); + stacktest_B_crash_after_subcall(); +} + +// This function must not have a frame record. We use __attribute__((naked)) +// to suppress the prologue/epilogue entirely, and write the crashing store +// in assembly. This ensures FP still points to the caller's frame. +# if defined(__aarch64__) +__attribute__((naked, noinline)) static void +stacktest_B_crash_no_frame_record(void) +{ + __asm__ volatile( + "mov x8, #1\n\t" + "str wzr, [x8]\n\t" // store to address 0x1 -> SIGSEGV + ); +} +# elif defined(__x86_64__) +__attribute__((naked, noinline)) static void +stacktest_B_crash_no_frame_record(void) +{ + __asm__ volatile( + "movl $0, 0x1\n\t" // store to address 0x1 -> SIGSEGV + ); +} +# endif + +NOINLINE static void +stacktest_A_calls_B_no_frame_record(void) +{ + fprintf(stderr, "stacktest_A_calls_B_no_frame_record\n"); + fflush(stderr); + stacktest_B_crash_no_frame_record(); +} + +#endif /* !_WIN32 */ + // on_crash callback that crashes via SIGSEGV: simulates buggy user code static sentry_value_t crashing_on_crash_callback( @@ -94,6 +205,7 @@ setup_sentry(PATH_TYPE database_path) sentry_options_set_auto_session_tracking(options, false); sentry_options_set_dsn(options, "https://public@sentry.invalid/1"); sentry_options_set_debug(options, 1); + sentry_options_set_symbolize_stacktraces(options, 1); sentry_options_set_transport(options, sentry_transport_new(print_envelope)); if (sentry_init(options) != 0) { @@ -217,6 +329,53 @@ test_handler_abort_crash(PATH_TYPE database_path) return 1; } +#ifndef _WIN32 +static int +test_stack_no_subcalls(PATH_TYPE database_path) +{ + if (setup_sentry(database_path) != 0) { + return 1; + } + sentry_set_tag("test", "stack-no-subcalls"); + fprintf(stderr, "Starting stack-no-subcalls test\n"); + fflush(stderr); + stacktest_A_calls_B_no_subcalls(); + fprintf(stderr, "ERROR: Should have crashed\n"); + sentry_close(); + return 1; +} + +static int +test_stack_with_subcalls(PATH_TYPE database_path) +{ + if (setup_sentry(database_path) != 0) { + return 1; + } + sentry_set_tag("test", "stack-with-subcalls"); + fprintf(stderr, "Starting stack-with-subcalls test\n"); + fflush(stderr); + stacktest_A_calls_B_with_subcalls(); + fprintf(stderr, "ERROR: Should have crashed\n"); + sentry_close(); + return 1; +} + +static int +test_stack_no_frame_record(PATH_TYPE database_path) +{ + if (setup_sentry(database_path) != 0) { + return 1; + } + sentry_set_tag("test", "stack-no-frame-record"); + fprintf(stderr, "Starting stack-no-frame-record test\n"); + fflush(stderr); + stacktest_A_calls_B_no_frame_record(); + fprintf(stderr, "ERROR: Should have crashed\n"); + sentry_close(); + return 1; +} +#endif /* !_WIN32 */ + static int test_simple_crash(PATH_TYPE database_path) { @@ -294,6 +453,12 @@ main(int argc, char *argv[]) fprintf(stderr, " handler-abort-crash - Handler thread crashes in on_crash " "(abort)\n"); + fprintf(stderr, + " stack-no-subcalls - Stack trace: no sub-calls\n"); + fprintf(stderr, + " stack-with-subcalls - Stack trace: with sub-calls\n"); + fprintf(stderr, + " stack-no-frame-record - Stack trace: no frame record\n"); return 1; } @@ -312,6 +477,15 @@ main(int argc, char *argv[]) if (strcmp(test_name, "handler-abort-crash") == 0) { return test_handler_abort_crash(database_path); } + if (strcmp(test_name, "stack-no-subcalls") == 0) { + return test_stack_no_subcalls(database_path); + } + if (strcmp(test_name, "stack-with-subcalls") == 0) { + return test_stack_with_subcalls(database_path); + } + if (strcmp(test_name, "stack-no-frame-record") == 0) { + return test_stack_no_frame_record(database_path); + } fprintf(stderr, "Unknown test: %s\n", test_name); return 1; } diff --git a/tests/test_inproc_stress.py b/tests/test_inproc_stress.py index 2418e0b37..2d2a3d91b 100644 --- a/tests/test_inproc_stress.py +++ b/tests/test_inproc_stress.py @@ -369,6 +369,110 @@ def test_inproc_handler_abort_crash(cmake): shutil.rmtree(database_path, ignore_errors=True) +@pytest.mark.skipif(sys.platform != "darwin", reason="Stack trace tests are macOS-only") +@pytest.mark.parametrize( + "test_name,expected_functions,expect_no_duplicates", + [ + ( + "stack-no-subcalls", + [ + "stacktest_B_crash_no_subcalls", + "stacktest_A_calls_B_no_subcalls", + ], + True, + ), + ( + "stack-with-subcalls", + [ + "stacktest_B_crash_after_subcall", + "stacktest_A_calls_B_with_subcalls", + ], + # LR holds a stale return into the crashing function (from the + # last bl before the crash). We accept this spurious extra frame + # because skipping LR would lose a real frame in the + # no-frame-record case, which we can't distinguish at walk time. + False, + ), + ( + "stack-no-frame-record", + [ + "stacktest_B_crash_no_frame_record", + "stacktest_A_calls_B_no_frame_record", + ], + True, + ), + ], + ids=["no-subcalls", "with-subcalls", "no-frame-record"], +) +def test_inproc_stack_trace(cmake, test_name, expected_functions, expect_no_duplicates): + """ + Test that the fp-based unwinder produces correct stack traces on macOS. + + Verifies: + - No duplicate frames where avoidable (arm64 LR vs saved LR dedup) + - No missing frames (functions without frame records need LR) + - Expected function names appear in the correct order + """ + tmp_path = cmake( + ["sentry"], + {"SENTRY_BACKEND": "inproc", "SENTRY_TRANSPORT": "none"}, + ) + + test_exe = compile_test_program(tmp_path) + database_path = tmp_path / ".sentry-native" + + try: + returncode, stdout, stderr = run_stress_test( + tmp_path, test_exe, test_name, database_path + ) + + assert returncode != 0, f"Process should have crashed. stderr:\n{stderr}" + assert ( + "crash has been captured" in stderr + ), f"Crash not captured. stderr:\n{stderr}" + + envelope_path = assert_single_crash_envelope(database_path) + with open(envelope_path, "rb") as f: + envelope = Envelope.deserialize(f.read()) + + event = envelope.get_event() + assert event is not None, "Event missing from envelope" + assert "exception" in event, "Exception missing from event" + + exc = event["exception"]["values"][0] + assert "stacktrace" in exc, "Stacktrace missing from exception" + + frames = exc["stacktrace"]["frames"] + # Sentry convention: frames are bottom-to-top, so reverse for + # top-to-bottom (crash frame first) + func_names = [f.get("function", "") for f in reversed(frames)] + + # Check no consecutive duplicate function names (where expected) + if expect_no_duplicates: + for i in range(len(func_names) - 1): + assert func_names[i] != func_names[i + 1] or not func_names[i], ( + f"Duplicate consecutive frame: {func_names[i]} at positions" + f" {i} and {i+1}.\nAll frames: {func_names}" + ) + + # Check expected functions appear in order + search_start = 0 + for expected in expected_functions: + found = False + for i in range(search_start, len(func_names)): + if expected in (func_names[i] or ""): + found = True + search_start = i + 1 + break + assert found, ( + f"Expected function '{expected}' not found (after position" + f" {search_start}) in stack trace.\nAll frames: {func_names}" + ) + + finally: + shutil.rmtree(database_path, ignore_errors=True) + + @pytest.mark.parametrize("iteration", range(5)) def test_inproc_concurrent_crash_repeated(cmake, iteration): tmp_path = cmake( From 8c8bba0b95b8822a9e1754520332d543b065bc55 Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Thu, 12 Feb 2026 22:51:59 +0100 Subject: [PATCH 128/130] switch compile guard to only __APPLE__ because that is how we use it --- tests/fixtures/inproc_stress/main.c | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/tests/fixtures/inproc_stress/main.c b/tests/fixtures/inproc_stress/main.c index 693aabc77..9be0c62b9 100644 --- a/tests/fixtures/inproc_stress/main.c +++ b/tests/fixtures/inproc_stress/main.c @@ -30,7 +30,13 @@ extern void run_concurrent_crash(void); static void *invalid_mem = (void *)1; // Prevent inlining and optimization to ensure predictable frame records. -#define NOINLINE __attribute__((noinline, optnone)) +#if defined(__clang__) +# define NOINLINE __attribute__((noinline, optnone)) +#elif defined(__GNUC__) +# define NOINLINE __attribute__((noinline, optimize("O0"))) +#else +# define NOINLINE __attribute__((noinline)) +#endif // Stack trace test functions (used on macOS because that uses a handwritten stack-walker, can compile on Linux/Unix). // The naming convention encodes the expected call chain so the Python test @@ -45,7 +51,7 @@ static void *invalid_mem = (void *)1; // // Scenario 3: crash in function without a frame record // stacktest_A_calls_B_no_frame_record -> stacktest_B_crash_no_frame_record -#ifndef _WIN32 +#ifdef __APPLE__ static volatile int g_side_effect = 0; @@ -138,7 +144,7 @@ stacktest_A_calls_B_no_frame_record(void) stacktest_B_crash_no_frame_record(); } -#endif /* !_WIN32 */ +#endif /* __APPLE__ */ // on_crash callback that crashes via SIGSEGV: simulates buggy user code static sentry_value_t @@ -329,7 +335,7 @@ test_handler_abort_crash(PATH_TYPE database_path) return 1; } -#ifndef _WIN32 +#ifdef __APPLE__ static int test_stack_no_subcalls(PATH_TYPE database_path) { @@ -374,7 +380,7 @@ test_stack_no_frame_record(PATH_TYPE database_path) sentry_close(); return 1; } -#endif /* !_WIN32 */ +#endif /* __APPLE__ */ static int test_simple_crash(PATH_TYPE database_path) @@ -453,12 +459,14 @@ main(int argc, char *argv[]) fprintf(stderr, " handler-abort-crash - Handler thread crashes in on_crash " "(abort)\n"); +#ifdef __APPLE__ fprintf(stderr, " stack-no-subcalls - Stack trace: no sub-calls\n"); fprintf(stderr, " stack-with-subcalls - Stack trace: with sub-calls\n"); fprintf(stderr, " stack-no-frame-record - Stack trace: no frame record\n"); +#endif return 1; } @@ -477,6 +485,7 @@ main(int argc, char *argv[]) if (strcmp(test_name, "handler-abort-crash") == 0) { return test_handler_abort_crash(database_path); } +#ifdef __APPLE__ if (strcmp(test_name, "stack-no-subcalls") == 0) { return test_stack_no_subcalls(database_path); } @@ -486,6 +495,7 @@ main(int argc, char *argv[]) if (strcmp(test_name, "stack-no-frame-record") == 0) { return test_stack_no_frame_record(database_path); } +#endif fprintf(stderr, "Unknown test: %s\n", test_name); return 1; } From aceeac0d01fe74f54dd47bfc07d82b30d186edc6 Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Fri, 13 Feb 2026 08:33:08 +0100 Subject: [PATCH 129/130] don't let the stack trace tests run on android (check for sys.platform is not enough, because the android tests run on macOS runners) --- tests/test_inproc_stress.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/test_inproc_stress.py b/tests/test_inproc_stress.py index 2d2a3d91b..c3758f48c 100644 --- a/tests/test_inproc_stress.py +++ b/tests/test_inproc_stress.py @@ -369,7 +369,10 @@ def test_inproc_handler_abort_crash(cmake): shutil.rmtree(database_path, ignore_errors=True) -@pytest.mark.skipif(sys.platform != "darwin", reason="Stack trace tests are macOS-only") +@pytest.mark.skipif( + sys.platform != "darwin" or is_android(), + reason="Stack trace tests are macOS-only", +) @pytest.mark.parametrize( "test_name,expected_functions,expect_no_duplicates", [ From 69e16194e87508a12c192a3b3e64fe6e2f40dd8a Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Fri, 13 Feb 2026 08:41:13 +0100 Subject: [PATCH 130/130] On x86_64, without an LR register, there's no mechanism for the FP-based unwinder to recover a frame when the callee has no frame record. So, lets limit that particular test-case to aarch64. --- tests/test_inproc_stress.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/test_inproc_stress.py b/tests/test_inproc_stress.py index c3758f48c..e9b6d4ee7 100644 --- a/tests/test_inproc_stress.py +++ b/tests/test_inproc_stress.py @@ -1,5 +1,6 @@ import os import pathlib +import platform import shutil import subprocess import sys @@ -396,13 +397,17 @@ def test_inproc_handler_abort_crash(cmake): # no-frame-record case, which we can't distinguish at walk time. False, ), - ( + pytest.param( "stack-no-frame-record", [ "stacktest_B_crash_no_frame_record", "stacktest_A_calls_B_no_frame_record", ], True, + marks=pytest.mark.skipif( + platform.machine() not in ("arm64", "aarch64"), + reason="no-frame-record recovery requires LR (arm64 only)", + ), ), ], ids=["no-subcalls", "with-subcalls", "no-frame-record"],