From 2ef936b99a67e8d342a70dd29c199df85b955847 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Tue, 24 Mar 2026 13:33:47 +0100 Subject: [PATCH 01/13] feat: preload signal handlers and ignore signal API for managed runtimes Add SENTRY_CRASH_MANAGED_RUNTIME compile-time flag that: - Preloads signal handlers via __attribute__((constructor)) before the managed runtime starts, ensuring correct handler chain order - Excludes EXC_MASK_BAD_ACCESS and EXC_MASK_ARITHMETIC from Mach exception monitoring (handled by the managed runtime via signals) Add ignoreNextSignal: API on PrivateSentrySDKOnly to let hybrid SDKs tell SentryCrash to skip the next occurrence of a signal on the calling thread (thread-local, one-shot). NULL-guard g_onExceptionEvent callback in handleException to prevent crash if a signal fires between preload and full sentrycrash_install. Co-Authored-By: Claude Opus 4.6 (1M context) --- CHANGELOG.md | 1 + Sources/Sentry/PrivateSentrySDKOnly.m | 6 +++++ Sources/Sentry/Public/PrivateSentrySDKOnly.h | 8 ++++++ Sources/Sentry/SentrySDKInternal.m | 4 ++- Sources/Sentry/include/SentryCrashC.h | 9 +++++++ .../include/SentryCrashMonitor_Signal.h | 5 ++++ .../Recording/Monitors/SentryCrashMonitor.c | 4 ++- .../SentryCrashMonitor_MachException.c | 7 +++++ .../Monitors/SentryCrashMonitor_Signal.c | 14 ++++++++++ Sources/SentryCrash/Recording/SentryCrashC.c | 27 +++++++++++++++++++ 10 files changed, 83 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 964becf2777..a6be9955e78 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - Per-instance unmaskView propagates to child views (#7733) - **Warning:** If you relied on children of an unmasked view still being individually redacted, verify your Session Replay redaction after updating. An explicit `maskView(_:)` on a descendant still takes precedence. +- Fix AOT interop with managed .NET runtimes (#6193) ## 9.8.0 diff --git a/Sources/Sentry/PrivateSentrySDKOnly.m b/Sources/Sentry/PrivateSentrySDKOnly.m index 8acebbd6775..74c01fdac9e 100644 --- a/Sources/Sentry/PrivateSentrySDKOnly.m +++ b/Sources/Sentry/PrivateSentrySDKOnly.m @@ -2,6 +2,7 @@ #import "SentryAppStartMeasurement.h" #import "SentryBreadcrumb+Private.h" #import "SentryClient.h" +#import "SentryCrashC.h" #import "SentryHub+Private.h" #import "SentryInternalDefines.h" #import "SentryMeta.h" @@ -386,4 +387,9 @@ + (void)setReplayTags:(NSDictionary *)tags #endif ++ (void)ignoreNextSignal:(int)signum +{ + sentrycrash_ignore_next_signal(signum); +} + @end diff --git a/Sources/Sentry/Public/PrivateSentrySDKOnly.h b/Sources/Sentry/Public/PrivateSentrySDKOnly.h index 3879df03b73..61331057cc0 100644 --- a/Sources/Sentry/Public/PrivateSentrySDKOnly.h +++ b/Sources/Sentry/Public/PrivateSentrySDKOnly.h @@ -200,6 +200,14 @@ typedef void (^SentryOnAppStartMeasurementAvailable)( */ + (void)setLogOutput:(void (^)(NSString *))output; +/** + * Tell the crash reporter to ignore the next occurrence of the given signal on + * the calling thread. Used by hybrid SDKs to prevent duplicate crash reports + * when the host runtime is about to raise a signal (e.g. SIGABRT) that has + * already been captured as a managed exception. + */ ++ (void)ignoreNextSignal:(int)signum; + @end NS_ASSUME_NONNULL_END diff --git a/Sources/Sentry/SentrySDKInternal.m b/Sources/Sentry/SentrySDKInternal.m index 61af97cd079..f3d737c71ce 100644 --- a/Sources/Sentry/SentrySDKInternal.m +++ b/Sources/Sentry/SentrySDKInternal.m @@ -608,7 +608,9 @@ + (void)close // Code not to be analyzed + (void)crash { - int *p = 0; + // volatile forces an actual null dereference (SIGSEGV) instead of letting + // the compiler optimize the undefined behavior into a trap (SIGTRAP). + volatile int *p = 0; *p = 0; } #endif diff --git a/Sources/Sentry/include/SentryCrashC.h b/Sources/Sentry/include/SentryCrashC.h index 45654a2c991..fd0b6b09bea 100644 --- a/Sources/Sentry/include/SentryCrashC.h +++ b/Sources/Sentry/include/SentryCrashC.h @@ -51,6 +51,15 @@ SentryCrashMonitorType sentrycrash_install(const char *appName, const char *cons void sentrycrash_uninstall(void); +/** Tell SentryCrash to ignore the next occurrence of the given signal on the + * calling thread. Used to prevent duplicate crash reports when the host runtime + * is about to raise a signal (e.g. SIGABRT) that has already been captured as + * a managed exception. + * + * @param signum The signal number to ignore (e.g. SIGABRT). + */ +void sentrycrash_ignore_next_signal(int signum); + /** Set the crash types that will be handled. * Some crash types may not be enabled depending on circumstances (e.g. running * in a debugger). diff --git a/Sources/Sentry/include/SentryCrashMonitor_Signal.h b/Sources/Sentry/include/SentryCrashMonitor_Signal.h index 2b34438349a..f78c373f332 100644 --- a/Sources/Sentry/include/SentryCrashMonitor_Signal.h +++ b/Sources/Sentry/include/SentryCrashMonitor_Signal.h @@ -41,6 +41,11 @@ extern "C" { */ void sentrycrashcm_setEnableSigtermReporting(bool enabled); +/** Tell the signal monitor to ignore the next occurrence of the given signal + * on the calling thread. + */ +void sentrycrashcm_signal_ignore_next(int signum); + /** Access the Monitor API. */ SentryCrashMonitorAPI *sentrycrashcm_signal_getAPI(void); diff --git a/Sources/SentryCrash/Recording/Monitors/SentryCrashMonitor.c b/Sources/SentryCrash/Recording/Monitors/SentryCrashMonitor.c index 163193332c7..11a4f6cfb10 100644 --- a/Sources/SentryCrash/Recording/Monitors/SentryCrashMonitor.c +++ b/Sources/SentryCrash/Recording/Monitors/SentryCrashMonitor.c @@ -283,7 +283,9 @@ sentrycrashcm_handleException(struct SentryCrash_MonitorContext *context) } } - g_onExceptionEvent(context); + if (g_onExceptionEvent != NULL) { + g_onExceptionEvent(context); + } if (g_isHandlingFatalException && !g_crashedDuringExceptionHandling) { SENTRY_ASYNC_SAFE_LOG_DEBUG("Exception is fatal. Restoring original handlers."); diff --git a/Sources/SentryCrash/Recording/Monitors/SentryCrashMonitor_MachException.c b/Sources/SentryCrash/Recording/Monitors/SentryCrashMonitor_MachException.c index 4c094704793..1b6d70f587b 100644 --- a/Sources/SentryCrash/Recording/Monitors/SentryCrashMonitor_MachException.c +++ b/Sources/SentryCrash/Recording/Monitors/SentryCrashMonitor_MachException.c @@ -463,6 +463,13 @@ installExceptionHandler(void) exception_mask_t mask = EXC_MASK_BAD_ACCESS | EXC_MASK_BAD_INSTRUCTION | EXC_MASK_ARITHMETIC | EXC_MASK_SOFTWARE | EXC_MASK_BREAKPOINT; +# ifdef SENTRY_CRASH_MANAGED_RUNTIME + // Exclude Mach exceptions that the managed (.NET/Mono) runtime handles via + // signal handlers (EXC_BAD_ACCESS for NullReferenceException, EXC_ARITHMETIC + // for DivideByZeroException). + mask &= ~(EXC_MASK_BAD_ACCESS | EXC_MASK_ARITHMETIC); +# endif + SENTRY_ASYNC_SAFE_LOG_DEBUG("Backing up original exception ports."); kr = task_get_exception_ports(thisTask, mask, g_previousExceptionPorts.masks, &g_previousExceptionPorts.count, g_previousExceptionPorts.ports, diff --git a/Sources/SentryCrash/Recording/Monitors/SentryCrashMonitor_Signal.c b/Sources/SentryCrash/Recording/Monitors/SentryCrashMonitor_Signal.c index f3d2ef30207..ec6dd3f0c13 100644 --- a/Sources/SentryCrash/Recording/Monitors/SentryCrashMonitor_Signal.c +++ b/Sources/SentryCrash/Recording/Monitors/SentryCrashMonitor_Signal.c @@ -49,6 +49,7 @@ static volatile bool g_isEnabled = false; static bool g_isSigtermReportingEnabled = false; +static _Thread_local int tl_ignore_signum = 0; static SentryCrash_MonitorContext g_monitorContext; static SentryCrashStackCursor g_stackCursor; @@ -82,6 +83,11 @@ static char g_eventID[37]; static void handleSignal(int sigNum, siginfo_t *signalInfo, void *userContext) { + if (sigNum == tl_ignore_signum) { + SENTRY_ASYNC_SAFE_LOG_DEBUG("Ignored signal %d", sigNum); + tl_ignore_signum = 0; + return; + } SENTRY_ASYNC_SAFE_LOG_DEBUG("Trapped signal %d", sigNum); if (g_isEnabled) { thread_act_array_t threads = NULL; @@ -284,6 +290,14 @@ sentrycrashcm_setEnableSigtermReporting(bool enabled) #endif } +void +sentrycrashcm_signal_ignore_next(int signum) +{ +#if SENTRY_HAS_SIGNAL + tl_ignore_signum = signum; +#endif +} + SentryCrashMonitorAPI * sentrycrashcm_signal_getAPI(void) { diff --git a/Sources/SentryCrash/Recording/SentryCrashC.c b/Sources/SentryCrash/Recording/SentryCrashC.c index 72dfc6d77f1..90bdece5773 100644 --- a/Sources/SentryCrash/Recording/SentryCrashC.c +++ b/Sources/SentryCrash/Recording/SentryCrashC.c @@ -31,6 +31,7 @@ #include "SentryCrashFileUtils.h" #include "SentryCrashMonitorContext.h" #include "SentryCrashMonitor_AppState.h" +#include "SentryCrashMonitor_Signal.h" #include "SentryCrashMonitor_System.h" #include "SentryCrashObjC.h" #include "SentryCrashReport.h" @@ -54,6 +55,9 @@ /** True if SentryCrash has been installed. */ static volatile bool g_installed = 0; +/** True if SentryCrash signal handlers have been preloaded. */ +static volatile bool g_preloaded = 0; + static SentryCrashMonitorType g_monitoring = SentryCrashMonitorTypeProductionSafeMinimal; static char g_lastCrashReportFilePath[SentryCrashFU_MAX_PATH_LENGTH]; static void (*g_saveScreenShot)(const char *) = 0; @@ -63,6 +67,22 @@ static void (*g_saveTransaction)(void) = 0; #pragma mark - Utility - // ============================================================================ +#ifdef SENTRY_CRASH_MANAGED_RUNTIME +/** Preload signal handlers before the managed (.NET/Mono) runtime installs its + * own, to ensure the correct handler chain order: + * managed runtime -> SentryCrash -> system. + */ +__attribute__((constructor)) static void +onPreload(void) +{ + if (g_installed) { + return; + } + g_preloaded = 1; + sentrycrashcm_setActiveMonitors(SentryCrashMonitorTypeSignal); +} +#endif + // ============================================================================ #pragma mark - Callbacks - // ============================================================================ @@ -155,6 +175,7 @@ sentrycrash_uninstall(void) sentrycrashcm_setEventCallback(NULL); sentrycrashcm_resetState(); g_installed = 0; + g_preloaded = 0; sentrycrashccd_close(); } @@ -171,6 +192,12 @@ sentrycrash_setMonitoring(SentryCrashMonitorType monitors) return g_monitoring; } +void +sentrycrash_ignore_next_signal(int signum) +{ + sentrycrashcm_signal_ignore_next(signum); +} + void sentrycrash_setUserInfoJSON(const char *const userInfoJSON) { From 86c05be2e9a44ea4086cf36d00543542a1fa5aa2 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Tue, 24 Mar 2026 14:34:17 +0100 Subject: [PATCH 02/13] ref: remove unused g_preloaded flag The flag was originally needed to prevent sentrycrash_setMonitoring() from re-enabling Mach exceptions after preload. Now that the Mach exception mask is controlled at compile time via SENTRY_CRASH_MANAGED_RUNTIME in SentryCrashMonitor_MachException.c, there is no runtime state to guard. Co-Authored-By: Claude Opus 4.6 (1M context) --- Sources/SentryCrash/Recording/SentryCrashC.c | 5 ----- 1 file changed, 5 deletions(-) diff --git a/Sources/SentryCrash/Recording/SentryCrashC.c b/Sources/SentryCrash/Recording/SentryCrashC.c index 90bdece5773..b47701b90f7 100644 --- a/Sources/SentryCrash/Recording/SentryCrashC.c +++ b/Sources/SentryCrash/Recording/SentryCrashC.c @@ -55,9 +55,6 @@ /** True if SentryCrash has been installed. */ static volatile bool g_installed = 0; -/** True if SentryCrash signal handlers have been preloaded. */ -static volatile bool g_preloaded = 0; - static SentryCrashMonitorType g_monitoring = SentryCrashMonitorTypeProductionSafeMinimal; static char g_lastCrashReportFilePath[SentryCrashFU_MAX_PATH_LENGTH]; static void (*g_saveScreenShot)(const char *) = 0; @@ -78,7 +75,6 @@ onPreload(void) if (g_installed) { return; } - g_preloaded = 1; sentrycrashcm_setActiveMonitors(SentryCrashMonitorTypeSignal); } #endif @@ -175,7 +171,6 @@ sentrycrash_uninstall(void) sentrycrashcm_setEventCallback(NULL); sentrycrashcm_resetState(); g_installed = 0; - g_preloaded = 0; sentrycrashccd_close(); } From 7f6b05927f51ac3dc3c43a3ae5188117b221d250 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Fri, 27 Mar 2026 15:06:09 +0100 Subject: [PATCH 03/13] fix: managed runtime signal handler lifecycle Preserve the managed runtime's signal handler chain across SDK lifecycle transitions (close/start) by skipping uninstall when not handling a crash. On the crash path, uninstall proceeds normally since the process is terminating. --- .../Monitors/SentryCrashMonitor_Signal.c | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/Sources/SentryCrash/Recording/Monitors/SentryCrashMonitor_Signal.c b/Sources/SentryCrash/Recording/Monitors/SentryCrashMonitor_Signal.c index ec6dd3f0c13..a6ce7a50428 100644 --- a/Sources/SentryCrash/Recording/Monitors/SentryCrashMonitor_Signal.c +++ b/Sources/SentryCrash/Recording/Monitors/SentryCrashMonitor_Signal.c @@ -49,6 +49,7 @@ static volatile bool g_isEnabled = false; static bool g_isSigtermReportingEnabled = false; +static volatile bool g_isHandlingCrash = false; static _Thread_local int tl_ignore_signum = 0; static SentryCrash_MonitorContext g_monitorContext; @@ -92,6 +93,7 @@ handleSignal(int sigNum, siginfo_t *signalInfo, void *userContext) if (g_isEnabled) { thread_act_array_t threads = NULL; mach_msg_type_number_t numThreads = 0; + g_isHandlingCrash = true; // Signal handlers preempt the crashing thread, so reentrancy can // occur from the same thread (handler crashes) or other threads. sentrycrashcm_notifyFatalException(false, &threads, &numThreads); @@ -114,6 +116,7 @@ handleSignal(int sigNum, siginfo_t *signalInfo, void *userContext) crashContext->stackCursor = &g_stackCursor; sentrycrashcm_handleException(crashContext); + g_isHandlingCrash = false; sentrycrashmc_resumeEnvironment(threads, numThreads); } @@ -129,6 +132,15 @@ handleSignal(int sigNum, siginfo_t *signalInfo, void *userContext) static bool installSignalHandler(void) { +# ifdef SENTRY_CRASH_MANAGED_RUNTIME + // Already installed by onPreload(). Reinstalling would overwrite + // g_previousSignalHandlers with the managed runtime's handler instead + // of the original system handler. + if (g_previousSignalHandlers != NULL) { + return true; + } +# endif + SENTRY_ASYNC_SAFE_LOG_DEBUG("Installing signal handler."); # if SENTRY_HAS_SIGNAL_STACK @@ -228,6 +240,14 @@ installSignalHandler(void) static void uninstallSignalHandler(void) { +# ifdef SENTRY_CRASH_MANAGED_RUNTIME + if (!g_isHandlingCrash) { + // Keep the handlers installed to preserve the managed runtime's signal + // chain. The handler becomes a pass-through when g_isEnabled is false. + return; + } +# endif + SENTRY_ASYNC_SAFE_LOG_DEBUG("Uninstalling signal handlers."); const int *fatalSignals = sentrycrashsignal_fatalSignals(); From c12a651d397178e90aca55cab29475741490c11f Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Fri, 27 Mar 2026 15:09:46 +0100 Subject: [PATCH 04/13] style: rename tl_ignore_signum to tl_ignoreSignum --- .../Recording/Monitors/SentryCrashMonitor_Signal.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/SentryCrash/Recording/Monitors/SentryCrashMonitor_Signal.c b/Sources/SentryCrash/Recording/Monitors/SentryCrashMonitor_Signal.c index a6ce7a50428..965e238f24c 100644 --- a/Sources/SentryCrash/Recording/Monitors/SentryCrashMonitor_Signal.c +++ b/Sources/SentryCrash/Recording/Monitors/SentryCrashMonitor_Signal.c @@ -50,7 +50,7 @@ static volatile bool g_isEnabled = false; static bool g_isSigtermReportingEnabled = false; static volatile bool g_isHandlingCrash = false; -static _Thread_local int tl_ignore_signum = 0; +static _Thread_local int tl_ignoreSignum = 0; static SentryCrash_MonitorContext g_monitorContext; static SentryCrashStackCursor g_stackCursor; @@ -84,9 +84,9 @@ static char g_eventID[37]; static void handleSignal(int sigNum, siginfo_t *signalInfo, void *userContext) { - if (sigNum == tl_ignore_signum) { + if (sigNum == tl_ignoreSignum) { SENTRY_ASYNC_SAFE_LOG_DEBUG("Ignored signal %d", sigNum); - tl_ignore_signum = 0; + tl_ignoreSignum = 0; return; } SENTRY_ASYNC_SAFE_LOG_DEBUG("Trapped signal %d", sigNum); @@ -314,7 +314,7 @@ void sentrycrashcm_signal_ignore_next(int signum) { #if SENTRY_HAS_SIGNAL - tl_ignore_signum = signum; + tl_ignoreSignum = signum; #endif } From cf85296274cc0abefb5f20571f90ad21f8d7b15d Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Fri, 27 Mar 2026 15:31:49 +0100 Subject: [PATCH 05/13] fix: restore previous signal handler before re-raising When the signal monitor is disabled under managed runtime, restore the previous handler for the signal before re-raising to prevent an infinite loop. Without SA_NODEFER, the raised signal would be re-delivered to the same handler after it returns. --- .../Monitors/SentryCrashMonitor_Signal.c | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/Sources/SentryCrash/Recording/Monitors/SentryCrashMonitor_Signal.c b/Sources/SentryCrash/Recording/Monitors/SentryCrashMonitor_Signal.c index 965e238f24c..4c6f2572761 100644 --- a/Sources/SentryCrash/Recording/Monitors/SentryCrashMonitor_Signal.c +++ b/Sources/SentryCrash/Recording/Monitors/SentryCrashMonitor_Signal.c @@ -65,6 +65,25 @@ static struct sigaction *g_previousSignalHandlers = NULL; static char g_eventID[37]; +// ============================================================================ +# pragma mark - Utility - +// ============================================================================ + +# ifdef SENTRY_CRASH_MANAGED_RUNTIME +static void +restorePreviousSignalHandler(int sigNum) +{ + const int *fatalSignals = sentrycrashsignal_fatalSignals(); + int count = sentrycrashsignal_numFatalSignals(); + for (int i = 0; i < count; i++) { + if (fatalSignals[i] == sigNum) { + sigaction(sigNum, &g_previousSignalHandlers[i], NULL); + return; + } + } +} +# endif + // ============================================================================ # pragma mark - Callbacks - // ============================================================================ @@ -121,6 +140,11 @@ handleSignal(int sigNum, siginfo_t *signalInfo, void *userContext) } SENTRY_ASYNC_SAFE_LOG_DEBUG("Re-raising signal for regular handlers to catch."); +# ifdef SENTRY_CRASH_MANAGED_RUNTIME + if (!g_isEnabled) { + restorePreviousSignalHandler(sigNum); + } +# endif // This is technically not allowed, but it works in OSX and iOS. raise(sigNum); } @@ -243,7 +267,7 @@ uninstallSignalHandler(void) # ifdef SENTRY_CRASH_MANAGED_RUNTIME if (!g_isHandlingCrash) { // Keep the handlers installed to preserve the managed runtime's signal - // chain. The handler becomes a pass-through when g_isEnabled is false. + // chain. handleSignal() restores individual handlers before re-raising. return; } # endif From 7f1b71c47c5002167d5970ebbce6417042cf5e49 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Fri, 27 Mar 2026 15:37:51 +0100 Subject: [PATCH 06/13] ref: remove g_isHandlingCrash flag The flag is redundant now that handleSignal() restores individual handlers before re-raising. uninstallSignalHandler() can be a blanket no-op under SENTRY_CRASH_MANAGED_RUNTIME. --- .../Recording/Monitors/SentryCrashMonitor_Signal.c | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/Sources/SentryCrash/Recording/Monitors/SentryCrashMonitor_Signal.c b/Sources/SentryCrash/Recording/Monitors/SentryCrashMonitor_Signal.c index 4c6f2572761..d416597001f 100644 --- a/Sources/SentryCrash/Recording/Monitors/SentryCrashMonitor_Signal.c +++ b/Sources/SentryCrash/Recording/Monitors/SentryCrashMonitor_Signal.c @@ -49,7 +49,6 @@ static volatile bool g_isEnabled = false; static bool g_isSigtermReportingEnabled = false; -static volatile bool g_isHandlingCrash = false; static _Thread_local int tl_ignoreSignum = 0; static SentryCrash_MonitorContext g_monitorContext; @@ -112,7 +111,6 @@ handleSignal(int sigNum, siginfo_t *signalInfo, void *userContext) if (g_isEnabled) { thread_act_array_t threads = NULL; mach_msg_type_number_t numThreads = 0; - g_isHandlingCrash = true; // Signal handlers preempt the crashing thread, so reentrancy can // occur from the same thread (handler crashes) or other threads. sentrycrashcm_notifyFatalException(false, &threads, &numThreads); @@ -135,7 +133,6 @@ handleSignal(int sigNum, siginfo_t *signalInfo, void *userContext) crashContext->stackCursor = &g_stackCursor; sentrycrashcm_handleException(crashContext); - g_isHandlingCrash = false; sentrycrashmc_resumeEnvironment(threads, numThreads); } @@ -265,13 +262,9 @@ static void uninstallSignalHandler(void) { # ifdef SENTRY_CRASH_MANAGED_RUNTIME - if (!g_isHandlingCrash) { - // Keep the handlers installed to preserve the managed runtime's signal - // chain. handleSignal() restores individual handlers before re-raising. - return; - } -# endif - + // Keep the handlers installed to preserve the managed runtime's signal + // chain. handleSignal() restores individual handlers before re-raising. +# else SENTRY_ASYNC_SAFE_LOG_DEBUG("Uninstalling signal handlers."); const int *fatalSignals = sentrycrashsignal_fatalSignals(); @@ -291,6 +284,7 @@ uninstallSignalHandler(void) g_signalStack = (stack_t) { 0 }; # endif SENTRY_ASYNC_SAFE_LOG_DEBUG("Signal handlers uninstalled."); +# endif } static void From 8b08cbf30bf656b25ef94a5995a7edfbdefa063f Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Fri, 27 Mar 2026 15:46:06 +0100 Subject: [PATCH 07/13] fix lint --- .../Recording/Monitors/SentryCrashMonitor_Signal.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/SentryCrash/Recording/Monitors/SentryCrashMonitor_Signal.c b/Sources/SentryCrash/Recording/Monitors/SentryCrashMonitor_Signal.c index d416597001f..ae3be273a2c 100644 --- a/Sources/SentryCrash/Recording/Monitors/SentryCrashMonitor_Signal.c +++ b/Sources/SentryCrash/Recording/Monitors/SentryCrashMonitor_Signal.c @@ -280,9 +280,9 @@ uninstallSignalHandler(void) sigaction(fatalSignals[i], &g_previousSignalHandlers[i], NULL); } -# if SENTRY_HAS_SIGNAL_STACK +# if SENTRY_HAS_SIGNAL_STACK g_signalStack = (stack_t) { 0 }; -# endif +# endif SENTRY_ASYNC_SAFE_LOG_DEBUG("Signal handlers uninstalled."); # endif } From bb21b73fc2fa9d479debe93ece1f0b57a389833d Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 2 Apr 2026 10:04:02 +0200 Subject: [PATCH 08/13] Document managed runtime interop in develop-docs/SENTRYCRASH.md --- develop-docs/SENTRYCRASH.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/develop-docs/SENTRYCRASH.md b/develop-docs/SENTRYCRASH.md index 086afc65abc..442d1e01f53 100644 --- a/develop-docs/SENTRYCRASH.md +++ b/develop-docs/SENTRYCRASH.md @@ -22,6 +22,7 @@ This document was generated with Claude using the prompt below. To refresh it, r - [Monitor System In Depth](#monitor-system-in-depth) - [Thread Model](#thread-model) - [KSCrash Divergence Analysis](#kscrash-divergence-analysis) +- [Managed Runtime Interop](#managed-runtime-interop) - [Known Issues & Risks](#known-issues--risks) - [Developer Workflow](#developer-workflow) - [Appendix: Key File Reference](#appendix-key-file-reference) @@ -374,6 +375,9 @@ These features were added by Sentry and do not exist in upstream KSCrash: 8. **C++ Exception Swapper** (`SentryCrashCxaThrowSwapper.c/.h`) - Runtime `__cxa_throw` hooking (inspired by fishhook + Yandex approach) +9. **Managed Runtime Interop** (`SENTRY_CRASH_MANAGED_RUNTIME`) + - Signal handler preloading and lifecycle changes for .NET/Mono embedding. See [Managed Runtime Interop](#managed-runtime-interop). + ### Recent Upstream Alignment (2026) Sentry maintains a watching approach, selectively adopting KSCrash improvements: @@ -438,6 +442,28 @@ The tracking issue [sentry-cocoa #5619](https://github.com/getsentry/sentry-coco --- +## Managed Runtime Interop + +When sentry-cocoa is embedded in a managed runtime (e.g. .NET/Mono via sentry-dotnet), SentryCrash must install signal handlers **before** the managed runtime to ensure the correct chain order: + +```mermaid +flowchart LR + Signal --> Runtime["Managed runtime"] --> SentryCrash --> System["System default"] +``` + +This order allows the managed runtime to convert certain signals (e.g. `SIGSEGV` for null reference) into managed exceptions, while real native crashes are chained to SentryCrash. + +The `SENTRY_CRASH_MANAGED_RUNTIME` compile flag, set by downstream SDKs, enables this behavior. The `onPreload()` constructor (`__attribute__((constructor))`) in `SentryCrashC.c` runs before `main()`, ensuring signal handlers are in place before the managed runtime initializes. Normal SDK initialization from managed code would be too late, as the runtime's handlers are already installed by that point. With this flag, the signal handler lifecycle changes: + +- `installSignalHandler()` is a no-op if already installed, preventing `start()` from overwriting `g_previousSignalHandlers` with the managed runtime's handler. +- `uninstallSignalHandler()` is a no-op, preserving the chain across `close()`/`start()` cycles. `g_isEnabled` controls whether crashes are processed or passed through. +- When disabled, `handleSignal()` restores the previous handler for that signal before re-raising to avoid looping. +- The constructor does not set `g_isEnabled`, so `enableCrashHandler = false` is respected. + +Related: [#6193](https://github.com/getsentry/sentry-cocoa/pull/6193), [sentry-dotnet#3954](https://github.com/getsentry/sentry-dotnet/issues/3954) + +--- + ## Known Issues & Risks ### HIGH Priority From 224425f9b66d9b9d34eaaf1d877b51bf3068c05f Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 2 Apr 2026 17:13:17 +0200 Subject: [PATCH 09/13] Consume ignore-next-signal flag on next delivery Reset the flag on any signal delivery, not just the matching one. Prevents a stale flag from silently suppressing a later unrelated signal. --- Sources/Sentry/Public/PrivateSentrySDKOnly.h | 6 ++++-- Sources/Sentry/include/SentryCrashMonitor_Signal.h | 3 ++- .../Recording/Monitors/SentryCrashMonitor_Signal.c | 6 ++++-- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/Sources/Sentry/Public/PrivateSentrySDKOnly.h b/Sources/Sentry/Public/PrivateSentrySDKOnly.h index 61331057cc0..70560ffe69d 100644 --- a/Sources/Sentry/Public/PrivateSentrySDKOnly.h +++ b/Sources/Sentry/Public/PrivateSentrySDKOnly.h @@ -203,8 +203,10 @@ typedef void (^SentryOnAppStartMeasurementAvailable)( /** * Tell the crash reporter to ignore the next occurrence of the given signal on * the calling thread. Used by hybrid SDKs to prevent duplicate crash reports - * when the host runtime is about to raise a signal (e.g. SIGABRT) that has - * already been captured as a managed exception. + * when the host runtime is about to raise a signal that has already been + * captured as a managed exception. The ignore is consumed by the next signal + * delivery on that thread, regardless of whether it matches. + * @param signum The signal number to ignore (e.g. SIGABRT). */ + (void)ignoreNextSignal:(int)signum; diff --git a/Sources/Sentry/include/SentryCrashMonitor_Signal.h b/Sources/Sentry/include/SentryCrashMonitor_Signal.h index f78c373f332..24aa0bef1e5 100644 --- a/Sources/Sentry/include/SentryCrashMonitor_Signal.h +++ b/Sources/Sentry/include/SentryCrashMonitor_Signal.h @@ -42,7 +42,8 @@ extern "C" { void sentrycrashcm_setEnableSigtermReporting(bool enabled); /** Tell the signal monitor to ignore the next occurrence of the given signal - * on the calling thread. + * on the calling thread. Consumed by the next signal delivery, even if it + * doesn't match. */ void sentrycrashcm_signal_ignore_next(int signum); diff --git a/Sources/SentryCrash/Recording/Monitors/SentryCrashMonitor_Signal.c b/Sources/SentryCrash/Recording/Monitors/SentryCrashMonitor_Signal.c index ae3be273a2c..c89e615fce9 100644 --- a/Sources/SentryCrash/Recording/Monitors/SentryCrashMonitor_Signal.c +++ b/Sources/SentryCrash/Recording/Monitors/SentryCrashMonitor_Signal.c @@ -102,11 +102,13 @@ restorePreviousSignalHandler(int sigNum) static void handleSignal(int sigNum, siginfo_t *signalInfo, void *userContext) { - if (sigNum == tl_ignoreSignum) { + int ignoreSignum = tl_ignoreSignum; + tl_ignoreSignum = 0; + if (sigNum == ignoreSignum) { SENTRY_ASYNC_SAFE_LOG_DEBUG("Ignored signal %d", sigNum); - tl_ignoreSignum = 0; return; } + SENTRY_ASYNC_SAFE_LOG_DEBUG("Trapped signal %d", sigNum); if (g_isEnabled) { thread_act_array_t threads = NULL; From 1d3a9576a8b7ff326114f7ffcbc54627f9c0a3e8 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 2 Apr 2026 17:44:41 +0200 Subject: [PATCH 10/13] Re-raise ignored signals instead of returning from handler Instead of returning early from the signal handler when a signal is ignored, skip crash processing and fall through to the existing restore + raise path. This avoids undefined behavior when the ignored signal originates from abort(). --- .../Recording/Monitors/SentryCrashMonitor_Signal.c | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/Sources/SentryCrash/Recording/Monitors/SentryCrashMonitor_Signal.c b/Sources/SentryCrash/Recording/Monitors/SentryCrashMonitor_Signal.c index c89e615fce9..2ecdf2be52e 100644 --- a/Sources/SentryCrash/Recording/Monitors/SentryCrashMonitor_Signal.c +++ b/Sources/SentryCrash/Recording/Monitors/SentryCrashMonitor_Signal.c @@ -104,13 +104,9 @@ handleSignal(int sigNum, siginfo_t *signalInfo, void *userContext) { int ignoreSignum = tl_ignoreSignum; tl_ignoreSignum = 0; - if (sigNum == ignoreSignum) { - SENTRY_ASYNC_SAFE_LOG_DEBUG("Ignored signal %d", sigNum); - return; - } SENTRY_ASYNC_SAFE_LOG_DEBUG("Trapped signal %d", sigNum); - if (g_isEnabled) { + if (g_isEnabled && sigNum != ignoreSignum) { thread_act_array_t threads = NULL; mach_msg_type_number_t numThreads = 0; // Signal handlers preempt the crashing thread, so reentrancy can @@ -140,7 +136,7 @@ handleSignal(int sigNum, siginfo_t *signalInfo, void *userContext) SENTRY_ASYNC_SAFE_LOG_DEBUG("Re-raising signal for regular handlers to catch."); # ifdef SENTRY_CRASH_MANAGED_RUNTIME - if (!g_isEnabled) { + if (!g_isEnabled || sigNum == ignoreSignum) { restorePreviousSignalHandler(sigNum); } # endif From 355c4ae6d36beabf0bc8ce3f1de8175a60fbf90f Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 2 Apr 2026 18:22:51 +0200 Subject: [PATCH 11/13] Make restorePreviousSignalHandler unconditional Remove the SENTRY_CRASH_MANAGED_RUNTIME guard so ignored signals are properly re-raised regardless of the compile flag. Harmless for the non-managed case where uninstall already restored them. --- .../Recording/Monitors/SentryCrashMonitor_Signal.c | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Sources/SentryCrash/Recording/Monitors/SentryCrashMonitor_Signal.c b/Sources/SentryCrash/Recording/Monitors/SentryCrashMonitor_Signal.c index 2ecdf2be52e..0c86a424a0a 100644 --- a/Sources/SentryCrash/Recording/Monitors/SentryCrashMonitor_Signal.c +++ b/Sources/SentryCrash/Recording/Monitors/SentryCrashMonitor_Signal.c @@ -68,7 +68,6 @@ static char g_eventID[37]; # pragma mark - Utility - // ============================================================================ -# ifdef SENTRY_CRASH_MANAGED_RUNTIME static void restorePreviousSignalHandler(int sigNum) { @@ -81,7 +80,6 @@ restorePreviousSignalHandler(int sigNum) } } } -# endif // ============================================================================ # pragma mark - Callbacks - @@ -135,11 +133,10 @@ handleSignal(int sigNum, siginfo_t *signalInfo, void *userContext) } SENTRY_ASYNC_SAFE_LOG_DEBUG("Re-raising signal for regular handlers to catch."); -# ifdef SENTRY_CRASH_MANAGED_RUNTIME if (!g_isEnabled || sigNum == ignoreSignum) { + // Avoid re-entering this handler on raise(). restorePreviousSignalHandler(sigNum); } -# endif // This is technically not allowed, but it works in OSX and iOS. raise(sigNum); } From da1aea9df704e9d80e6552b098247fd07d39c21e Mon Sep 17 00:00:00 2001 From: Itay Brenner Date: Wed, 8 Apr 2026 17:56:13 -0300 Subject: [PATCH 12/13] chore: run make generate-public-api --- sdk_api.json | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/sdk_api.json b/sdk_api.json index c0dd686b261..7ffb674775b 100644 --- a/sdk_api.json +++ b/sdk_api.json @@ -28227,6 +28227,42 @@ "static": true, "usr": "c:objc(cs)PrivateSentrySDKOnly(cm)getSdkVersionString" }, + { + "children": [ + { + "children": [ + { + "kind": "TypeNominal", + "name": "Void", + "printedName": "()" + } + ], + "kind": "TypeNameAlias", + "name": "Void", + "printedName": "Swift.Void" + }, + { + "kind": "TypeNominal", + "name": "Int32", + "printedName": "Swift.Int32", + "usr": "s:s5Int32V" + } + ], + "declAttributes": [ + "Dynamic", + "ObjC" + ], + "declKind": "Func", + "funcSelfKind": "NonMutating", + "isOpen": true, + "kind": "Function", + "moduleName": "Sentry", + "name": "ignoreNextSignal", + "objc_name": "ignoreNextSignal:", + "printedName": "ignoreNextSignal(_:)", + "static": true, + "usr": "c:objc(cs)PrivateSentrySDKOnly(cm)ignoreNextSignal:" + }, { "children": [ { From 27df7b7938a6e2fc50ad08d729f5a031342fa097 Mon Sep 17 00:00:00 2001 From: Itay Brenner Date: Thu, 9 Apr 2026 00:00:37 -0300 Subject: [PATCH 13/13] Fix changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e0a885c0f60..cf1cd7aac18 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ ### Fixes - Scope clipOut masking to active clip bounds (#7780) +- Fix AOT interop with managed .NET runtimes (#6193) ## 9.9.0 @@ -35,7 +36,6 @@ - Copy incoming tags dict to prevent crash (#7763) - Per-instance unmaskView propagates to child views (#7733) - **Warning:** If you relied on children of an unmasked view still being individually redacted, verify your Session Replay redaction after updating. An explicit `maskView(_:)` on a descendant still takes precedence. -- Fix AOT interop with managed .NET runtimes (#6193) - Move SessionTracker file I/O off the main thread ([#7704](https://github.com/getsentry/sentry-cocoa/pull/7704)) ## 9.8.0