Skip to content

Commit 9f9a858

Browse files
jpnurmiclaudeitaybre
authored
fix: AOT interop with managed .NET runtimes (#6193)
* 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) <noreply@anthropic.com> * 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) <noreply@anthropic.com> * 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. * style: rename tl_ignore_signum to tl_ignoreSignum * 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. * 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. * fix lint * Document managed runtime interop in develop-docs/SENTRYCRASH.md * 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. * 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(). * 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. * chore: run make generate-public-api --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Itay Brenner <itay.brenner@sentry.io>
1 parent bab8d5e commit 9f9a858

12 files changed

Lines changed: 179 additions & 5 deletions

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
### Fixes
1313

1414
- Scope clipOut masking to active clip bounds (#7780)
15+
- Fix AOT interop with managed .NET runtimes (#6193)
1516

1617
## 9.9.0
1718

Sources/Sentry/PrivateSentrySDKOnly.m

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
#import "SentryAppStartMeasurement.h"
33
#import "SentryBreadcrumb+Private.h"
44
#import "SentryClient.h"
5+
#import "SentryCrashC.h"
56
#import "SentryHub+Private.h"
67
#import "SentryInternalDefines.h"
78
#import "SentryMeta.h"
@@ -386,4 +387,9 @@ + (void)setReplayTags:(NSDictionary<NSString *, id> *)tags
386387

387388
#endif
388389

390+
+ (void)ignoreNextSignal:(int)signum
391+
{
392+
sentrycrash_ignore_next_signal(signum);
393+
}
394+
389395
@end

Sources/Sentry/Public/PrivateSentrySDKOnly.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,16 @@ typedef void (^SentryOnAppStartMeasurementAvailable)(
200200
*/
201201
+ (void)setLogOutput:(void (^)(NSString *))output;
202202

203+
/**
204+
* Tell the crash reporter to ignore the next occurrence of the given signal on
205+
* the calling thread. Used by hybrid SDKs to prevent duplicate crash reports
206+
* when the host runtime is about to raise a signal that has already been
207+
* captured as a managed exception. The ignore is consumed by the next signal
208+
* delivery on that thread, regardless of whether it matches.
209+
* @param signum The signal number to ignore (e.g. SIGABRT).
210+
*/
211+
+ (void)ignoreNextSignal:(int)signum;
212+
203213
@end
204214

205215
NS_ASSUME_NONNULL_END

Sources/Sentry/SentrySDKInternal.m

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -608,7 +608,9 @@ + (void)close
608608
// Code not to be analyzed
609609
+ (void)crash
610610
{
611-
int *p = 0;
611+
// volatile forces an actual null dereference (SIGSEGV) instead of letting
612+
// the compiler optimize the undefined behavior into a trap (SIGTRAP).
613+
volatile int *p = 0;
612614
*p = 0;
613615
}
614616
#endif

Sources/Sentry/include/SentryCrashC.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,15 @@ SentryCrashMonitorType sentrycrash_install(const char *appName, const char *cons
5151

5252
void sentrycrash_uninstall(void);
5353

54+
/** Tell SentryCrash to ignore the next occurrence of the given signal on the
55+
* calling thread. Used to prevent duplicate crash reports when the host runtime
56+
* is about to raise a signal (e.g. SIGABRT) that has already been captured as
57+
* a managed exception.
58+
*
59+
* @param signum The signal number to ignore (e.g. SIGABRT).
60+
*/
61+
void sentrycrash_ignore_next_signal(int signum);
62+
5463
/** Set the crash types that will be handled.
5564
* Some crash types may not be enabled depending on circumstances (e.g. running
5665
* in a debugger).

Sources/Sentry/include/SentryCrashMonitor_Signal.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,12 @@ extern "C" {
4141
*/
4242
void sentrycrashcm_setEnableSigtermReporting(bool enabled);
4343

44+
/** Tell the signal monitor to ignore the next occurrence of the given signal
45+
* on the calling thread. Consumed by the next signal delivery, even if it
46+
* doesn't match.
47+
*/
48+
void sentrycrashcm_signal_ignore_next(int signum);
49+
4450
/** Access the Monitor API.
4551
*/
4652
SentryCrashMonitorAPI *sentrycrashcm_signal_getAPI(void);

Sources/SentryCrash/Recording/Monitors/SentryCrashMonitor.c

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -283,7 +283,9 @@ sentrycrashcm_handleException(struct SentryCrash_MonitorContext *context)
283283
}
284284
}
285285

286-
g_onExceptionEvent(context);
286+
if (g_onExceptionEvent != NULL) {
287+
g_onExceptionEvent(context);
288+
}
287289

288290
if (g_isHandlingFatalException && !g_crashedDuringExceptionHandling) {
289291
SENTRY_ASYNC_SAFE_LOG_DEBUG("Exception is fatal. Restoring original handlers.");

Sources/SentryCrash/Recording/Monitors/SentryCrashMonitor_MachException.c

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -463,6 +463,13 @@ installExceptionHandler(void)
463463
exception_mask_t mask = EXC_MASK_BAD_ACCESS | EXC_MASK_BAD_INSTRUCTION | EXC_MASK_ARITHMETIC
464464
| EXC_MASK_SOFTWARE | EXC_MASK_BREAKPOINT;
465465

466+
# ifdef SENTRY_CRASH_MANAGED_RUNTIME
467+
// Exclude Mach exceptions that the managed (.NET/Mono) runtime handles via
468+
// signal handlers (EXC_BAD_ACCESS for NullReferenceException, EXC_ARITHMETIC
469+
// for DivideByZeroException).
470+
mask &= ~(EXC_MASK_BAD_ACCESS | EXC_MASK_ARITHMETIC);
471+
# endif
472+
466473
SENTRY_ASYNC_SAFE_LOG_DEBUG("Backing up original exception ports.");
467474
kr = task_get_exception_ports(thisTask, mask, g_previousExceptionPorts.masks,
468475
&g_previousExceptionPorts.count, g_previousExceptionPorts.ports,

Sources/SentryCrash/Recording/Monitors/SentryCrashMonitor_Signal.c

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949

5050
static volatile bool g_isEnabled = false;
5151
static bool g_isSigtermReportingEnabled = false;
52+
static _Thread_local int tl_ignoreSignum = 0;
5253

5354
static SentryCrash_MonitorContext g_monitorContext;
5455
static SentryCrashStackCursor g_stackCursor;
@@ -63,6 +64,23 @@ static struct sigaction *g_previousSignalHandlers = NULL;
6364

6465
static char g_eventID[37];
6566

67+
// ============================================================================
68+
# pragma mark - Utility -
69+
// ============================================================================
70+
71+
static void
72+
restorePreviousSignalHandler(int sigNum)
73+
{
74+
const int *fatalSignals = sentrycrashsignal_fatalSignals();
75+
int count = sentrycrashsignal_numFatalSignals();
76+
for (int i = 0; i < count; i++) {
77+
if (fatalSignals[i] == sigNum) {
78+
sigaction(sigNum, &g_previousSignalHandlers[i], NULL);
79+
return;
80+
}
81+
}
82+
}
83+
6684
// ============================================================================
6785
# pragma mark - Callbacks -
6886
// ============================================================================
@@ -82,8 +100,11 @@ static char g_eventID[37];
82100
static void
83101
handleSignal(int sigNum, siginfo_t *signalInfo, void *userContext)
84102
{
103+
int ignoreSignum = tl_ignoreSignum;
104+
tl_ignoreSignum = 0;
105+
85106
SENTRY_ASYNC_SAFE_LOG_DEBUG("Trapped signal %d", sigNum);
86-
if (g_isEnabled) {
107+
if (g_isEnabled && sigNum != ignoreSignum) {
87108
thread_act_array_t threads = NULL;
88109
mach_msg_type_number_t numThreads = 0;
89110
// Signal handlers preempt the crashing thread, so reentrancy can
@@ -112,6 +133,10 @@ handleSignal(int sigNum, siginfo_t *signalInfo, void *userContext)
112133
}
113134

114135
SENTRY_ASYNC_SAFE_LOG_DEBUG("Re-raising signal for regular handlers to catch.");
136+
if (!g_isEnabled || sigNum == ignoreSignum) {
137+
// Avoid re-entering this handler on raise().
138+
restorePreviousSignalHandler(sigNum);
139+
}
115140
// This is technically not allowed, but it works in OSX and iOS.
116141
raise(sigNum);
117142
}
@@ -123,6 +148,15 @@ handleSignal(int sigNum, siginfo_t *signalInfo, void *userContext)
123148
static bool
124149
installSignalHandler(void)
125150
{
151+
# ifdef SENTRY_CRASH_MANAGED_RUNTIME
152+
// Already installed by onPreload(). Reinstalling would overwrite
153+
// g_previousSignalHandlers with the managed runtime's handler instead
154+
// of the original system handler.
155+
if (g_previousSignalHandlers != NULL) {
156+
return true;
157+
}
158+
# endif
159+
126160
SENTRY_ASYNC_SAFE_LOG_DEBUG("Installing signal handler.");
127161

128162
# if SENTRY_HAS_SIGNAL_STACK
@@ -222,6 +256,10 @@ installSignalHandler(void)
222256
static void
223257
uninstallSignalHandler(void)
224258
{
259+
# ifdef SENTRY_CRASH_MANAGED_RUNTIME
260+
// Keep the handlers installed to preserve the managed runtime's signal
261+
// chain. handleSignal() restores individual handlers before re-raising.
262+
# else
225263
SENTRY_ASYNC_SAFE_LOG_DEBUG("Uninstalling signal handlers.");
226264

227265
const int *fatalSignals = sentrycrashsignal_fatalSignals();
@@ -237,10 +275,11 @@ uninstallSignalHandler(void)
237275
sigaction(fatalSignals[i], &g_previousSignalHandlers[i], NULL);
238276
}
239277

240-
# if SENTRY_HAS_SIGNAL_STACK
278+
# if SENTRY_HAS_SIGNAL_STACK
241279
g_signalStack = (stack_t) { 0 };
242-
# endif
280+
# endif
243281
SENTRY_ASYNC_SAFE_LOG_DEBUG("Signal handlers uninstalled.");
282+
# endif
244283
}
245284

246285
static void
@@ -284,6 +323,14 @@ sentrycrashcm_setEnableSigtermReporting(bool enabled)
284323
#endif
285324
}
286325

326+
void
327+
sentrycrashcm_signal_ignore_next(int signum)
328+
{
329+
#if SENTRY_HAS_SIGNAL
330+
tl_ignoreSignum = signum;
331+
#endif
332+
}
333+
287334
SentryCrashMonitorAPI *
288335
sentrycrashcm_signal_getAPI(void)
289336
{

Sources/SentryCrash/Recording/SentryCrashC.c

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
#include "SentryCrashFileUtils.h"
3232
#include "SentryCrashMonitorContext.h"
3333
#include "SentryCrashMonitor_AppState.h"
34+
#include "SentryCrashMonitor_Signal.h"
3435
#include "SentryCrashMonitor_System.h"
3536
#include "SentryCrashObjC.h"
3637
#include "SentryCrashReport.h"
@@ -63,6 +64,21 @@ static void (*g_saveTransaction)(void) = 0;
6364
#pragma mark - Utility -
6465
// ============================================================================
6566

67+
#ifdef SENTRY_CRASH_MANAGED_RUNTIME
68+
/** Preload signal handlers before the managed (.NET/Mono) runtime installs its
69+
* own, to ensure the correct handler chain order:
70+
* managed runtime -> SentryCrash -> system.
71+
*/
72+
__attribute__((constructor)) static void
73+
onPreload(void)
74+
{
75+
if (g_installed) {
76+
return;
77+
}
78+
sentrycrashcm_setActiveMonitors(SentryCrashMonitorTypeSignal);
79+
}
80+
#endif
81+
6682
// ============================================================================
6783
#pragma mark - Callbacks -
6884
// ============================================================================
@@ -171,6 +187,12 @@ sentrycrash_setMonitoring(SentryCrashMonitorType monitors)
171187
return g_monitoring;
172188
}
173189

190+
void
191+
sentrycrash_ignore_next_signal(int signum)
192+
{
193+
sentrycrashcm_signal_ignore_next(signum);
194+
}
195+
174196
void
175197
sentrycrash_setUserInfoJSON(const char *const userInfoJSON)
176198
{

0 commit comments

Comments
 (0)