Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
### Fixes

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

## 9.9.0

Expand Down
6 changes: 6 additions & 0 deletions Sources/Sentry/PrivateSentrySDKOnly.m
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -386,4 +387,9 @@ + (void)setReplayTags:(NSDictionary<NSString *, id> *)tags

#endif

+ (void)ignoreNextSignal:(int)signum
{
sentrycrash_ignore_next_signal(signum);
}

@end
10 changes: 10 additions & 0 deletions Sources/Sentry/Public/PrivateSentrySDKOnly.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
4 changes: 3 additions & 1 deletion Sources/Sentry/SentrySDKInternal.m
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 9 additions & 0 deletions Sources/Sentry/include/SentryCrashC.h
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
6 changes: 6 additions & 0 deletions Sources/Sentry/include/SentryCrashMonitor_Signal.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 -
// ============================================================================
Expand All @@ -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
Expand Down Expand Up @@ -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);
}
Expand All @@ -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
Expand Down Expand Up @@ -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();
Expand All @@ -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
Expand Down Expand Up @@ -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)
{
Expand Down
22 changes: 22 additions & 0 deletions Sources/SentryCrash/Recording/SentryCrashC.c
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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 -
// ============================================================================
Expand Down Expand Up @@ -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)
{
Expand Down
26 changes: 26 additions & 0 deletions develop-docs/SENTRYCRASH.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down
36 changes: 36 additions & 0 deletions sdk_api.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
{
Expand Down
Loading