diff --git a/CHANGELOG.md b/CHANGELOG.md index 24f082bbcf7..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 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..70560ffe69d 100644 --- a/Sources/Sentry/Public/PrivateSentrySDKOnly.h +++ b/Sources/Sentry/Public/PrivateSentrySDKOnly.h @@ -200,6 +200,16 @@ 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 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; + @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..24aa0bef1e5 100644 --- a/Sources/Sentry/include/SentryCrashMonitor_Signal.h +++ b/Sources/Sentry/include/SentryCrashMonitor_Signal.h @@ -41,6 +41,12 @@ extern "C" { */ void sentrycrashcm_setEnableSigtermReporting(bool enabled); +/** Tell the signal monitor to ignore the next occurrence of the given signal + * on the calling thread. Consumed by the next signal delivery, even if it + * doesn't match. + */ +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..0c86a424a0a 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_ignoreSignum = 0; static SentryCrash_MonitorContext g_monitorContext; static SentryCrashStackCursor g_stackCursor; @@ -63,6 +64,23 @@ static struct sigaction *g_previousSignalHandlers = NULL; static char g_eventID[37]; +// ============================================================================ +# pragma mark - Utility - +// ============================================================================ + +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; + } + } +} + // ============================================================================ # pragma mark - Callbacks - // ============================================================================ @@ -82,8 +100,11 @@ static char g_eventID[37]; static void handleSignal(int sigNum, siginfo_t *signalInfo, void *userContext) { + int ignoreSignum = tl_ignoreSignum; + tl_ignoreSignum = 0; + 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 @@ -112,6 +133,10 @@ handleSignal(int sigNum, siginfo_t *signalInfo, void *userContext) } SENTRY_ASYNC_SAFE_LOG_DEBUG("Re-raising signal for regular handlers to catch."); + if (!g_isEnabled || sigNum == ignoreSignum) { + // Avoid re-entering this handler on raise(). + restorePreviousSignalHandler(sigNum); + } // This is technically not allowed, but it works in OSX and iOS. raise(sigNum); } @@ -123,6 +148,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 @@ -222,6 +256,10 @@ installSignalHandler(void) static void uninstallSignalHandler(void) { +# ifdef SENTRY_CRASH_MANAGED_RUNTIME + // 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(); @@ -237,10 +275,11 @@ 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 } static void @@ -284,6 +323,14 @@ sentrycrashcm_setEnableSigtermReporting(bool enabled) #endif } +void +sentrycrashcm_signal_ignore_next(int signum) +{ +#if SENTRY_HAS_SIGNAL + tl_ignoreSignum = signum; +#endif +} + SentryCrashMonitorAPI * sentrycrashcm_signal_getAPI(void) { diff --git a/Sources/SentryCrash/Recording/SentryCrashC.c b/Sources/SentryCrash/Recording/SentryCrashC.c index 72dfc6d77f1..b47701b90f7 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" @@ -63,6 +64,21 @@ 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; + } + sentrycrashcm_setActiveMonitors(SentryCrashMonitorTypeSignal); +} +#endif + // ============================================================================ #pragma mark - Callbacks - // ============================================================================ @@ -171,6 +187,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) { 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 diff --git a/sdk_api.json b/sdk_api.json index edabb3d7d0b..0614ce100ac 100644 --- a/sdk_api.json +++ b/sdk_api.json @@ -28614,6 +28614,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": [ {