From 91afd1a751bd8cfa89469c2a4d0bf37d9ce6baa8 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Wed, 11 Mar 2026 13:24:49 +0100 Subject: [PATCH 1/6] fix: skip SA_NODEFER when CHAIN_AT_START is active SA_NODEFER (added in #1446) is incompatible with the CHAIN_AT_START signal handler strategy. When chaining to the runtime's signal handler (e.g. Mono), the runtime may reset the signal to SIG_DFL and re-raise. With SA_NODEFER the re-raised signal is delivered immediately, killing the process before our handler can regain control. Without SA_NODEFER, the re-raised signal is blocked during handler execution, allowing the runtime handler to return and sentry-native to proceed with crash capture. Co-Authored-By: Claude Opus 4.6 --- src/backends/sentry_backend_inproc.c | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/backends/sentry_backend_inproc.c b/src/backends/sentry_backend_inproc.c index 6efd2ad47..7fd7f1a71 100644 --- a/src/backends/sentry_backend_inproc.c +++ b/src/backends/sentry_backend_inproc.c @@ -507,7 +507,16 @@ startup_inproc_backend( // 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; + // However, SA_NODEFER is incompatible with CHAIN_AT_START: when we + // chain to the runtime's signal handler (e.g. Mono), it may reset + // the signal to SIG_DFL and re-raise. With SA_NODEFER the re-raised + // signal is delivered immediately (killing the process) before our + // handler can regain control. + g_sigaction.sa_flags = SA_SIGINFO | SA_ONSTACK; + if (g_backend_config.handler_strategy + != SENTRY_HANDLER_STRATEGY_CHAIN_AT_START) { + g_sigaction.sa_flags |= SA_NODEFER; + } for (size_t i = 0; i < SIGNAL_COUNT; ++i) { sigaction(SIGNAL_DEFINITIONS[i].signum, &g_sigaction, NULL); } From ab267631e159fee380dfe188cb452239a1da8d84 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Wed, 11 Mar 2026 13:39:30 +0100 Subject: [PATCH 2/6] Update CHANGELOG.md --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index aebb8a8cc..292680da6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +**Fixes**: + +- Skip `SA_NODEFER` when the `CHAIN_AT_START` handler strategy is used. The flag causes the runtime's re-raised signal to be delivered immediately, killing the process before `inproc` can capture the crash. ([#1572](https://github.com/getsentry/sentry-native/pull/1572)) + ## 0.13.2 **Features**: From 66b86a61989dec5ea0efc0c226e1c6fe3ca10121 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Wed, 11 Mar 2026 14:27:51 +0100 Subject: [PATCH 3/6] Revert "fix: skip SA_NODEFER when CHAIN_AT_START is active" This reverts commit 91afd1a751bd8cfa89469c2a4d0bf37d9ce6baa8. --- src/backends/sentry_backend_inproc.c | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/src/backends/sentry_backend_inproc.c b/src/backends/sentry_backend_inproc.c index 7fd7f1a71..6efd2ad47 100644 --- a/src/backends/sentry_backend_inproc.c +++ b/src/backends/sentry_backend_inproc.c @@ -507,16 +507,7 @@ startup_inproc_backend( // 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 is incompatible with CHAIN_AT_START: when we - // chain to the runtime's signal handler (e.g. Mono), it may reset - // the signal to SIG_DFL and re-raise. With SA_NODEFER the re-raised - // signal is delivered immediately (killing the process) before our - // handler can regain control. - g_sigaction.sa_flags = SA_SIGINFO | SA_ONSTACK; - if (g_backend_config.handler_strategy - != SENTRY_HANDLER_STRATEGY_CHAIN_AT_START) { - g_sigaction.sa_flags |= SA_NODEFER; - } + 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); } From b25f91d5301feb52518e14897bc8d94e4fb0b163 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Wed, 11 Mar 2026 17:28:01 +0100 Subject: [PATCH 4/6] fix: mask signal during CHAIN_AT_START to prevent re-raise from killing process With SA_NODEFER, the chained handler's re-raise is delivered immediately and kills the process before we regain control. Mask the signal via raw rt_sigprocmask (to bypass Android's libsigchain), then after the chain: reinstall our handler if it was reset to SIG_DFL, consume any pending signal with sigtimedwait, and unmask. Co-Authored-By: Claude Opus 4.6 --- src/backends/sentry_backend_inproc.c | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/backends/sentry_backend_inproc.c b/src/backends/sentry_backend_inproc.c index 6efd2ad47..3a7e9b508 100644 --- a/src/backends/sentry_backend_inproc.c +++ b/src/backends/sentry_backend_inproc.c @@ -25,6 +25,7 @@ #include #ifdef SENTRY_PLATFORM_UNIX # include +# include #endif #include @@ -1563,6 +1564,15 @@ process_ucontext(const sentry_ucontext_t *uctx) uintptr_t ip = get_instruction_pointer(uctx); uintptr_t sp = get_stack_pointer(uctx); + // Mask the signal so SA_NODEFER doesn't let re-raises from the chained + // handler to kill the process before we regain control. + sigset_t mask, old_mask; + sigemptyset(&mask); + sigaddset(&mask, uctx->signum); + // raw syscall to bypass libsigchain on Android + syscall( + SYS_rt_sigprocmask, SIG_BLOCK, &mask, &old_mask, sizeof(sigset_t)); + // invoke the previous handler (typically the CLR/Mono // signal-to-managed-exception handler) invoke_signal_handler( @@ -1578,6 +1588,21 @@ process_ucontext(const sentry_ucontext_t *uctx) return; } + // restore our handler + struct sigaction current; + sigaction(uctx->signum, NULL, ¤t); + if (current.sa_handler == SIG_DFL) { + sigaction(uctx->signum, &g_sigaction, NULL); + } + + // consume pending signal + struct timespec timeout = { 0, 0 }; + sigtimedwait(&mask, NULL, &timeout); + + // unmask + syscall( + SYS_rt_sigprocmask, SIG_SETMASK, &old_mask, NULL, sizeof(sigset_t)); + // return from runtime handler; continue processing the crash on the // signal thread until the worker takes over } From 57a447aed94d4554a511ef0c9df33c1fd8fd0175 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Wed, 11 Mar 2026 17:41:13 +0100 Subject: [PATCH 5/6] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 292680da6..1a7850f58 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ **Fixes**: -- Skip `SA_NODEFER` when the `CHAIN_AT_START` handler strategy is used. The flag causes the runtime's re-raised signal to be delivered immediately, killing the process before `inproc` can capture the crash. ([#1572](https://github.com/getsentry/sentry-native/pull/1572)) +- Fix `CHAIN_AT_START` handler strategy crashing when the chained handler resets the signal handler and re-raises. ([#1572](https://github.com/getsentry/sentry-native/pull/1572)) ## 0.13.2 From 986a15330b65242f7d36f6684420c557845183f6 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Wed, 11 Mar 2026 18:08:03 +0100 Subject: [PATCH 6/6] fix: use raw syscall for sigtimedwait and correct sigset size sigtimedwait is not declared without _POSIX_C_SOURCE >= 199309L, so use the raw SYS_rt_sigtimedwait syscall instead. Also replace sizeof(sigset_t) with _NSIG/8 since the kernel expects 8 bytes, not glibc's 128. Co-Authored-By: Claude Opus 4.6 --- src/backends/sentry_backend_inproc.c | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/backends/sentry_backend_inproc.c b/src/backends/sentry_backend_inproc.c index 3a7e9b508..0fed68d46 100644 --- a/src/backends/sentry_backend_inproc.c +++ b/src/backends/sentry_backend_inproc.c @@ -1570,8 +1570,7 @@ process_ucontext(const sentry_ucontext_t *uctx) sigemptyset(&mask); sigaddset(&mask, uctx->signum); // raw syscall to bypass libsigchain on Android - syscall( - SYS_rt_sigprocmask, SIG_BLOCK, &mask, &old_mask, sizeof(sigset_t)); + syscall(SYS_rt_sigprocmask, SIG_BLOCK, &mask, &old_mask, _NSIG / 8); // invoke the previous handler (typically the CLR/Mono // signal-to-managed-exception handler) @@ -1597,11 +1596,10 @@ process_ucontext(const sentry_ucontext_t *uctx) // consume pending signal struct timespec timeout = { 0, 0 }; - sigtimedwait(&mask, NULL, &timeout); + syscall(SYS_rt_sigtimedwait, &mask, NULL, &timeout, _NSIG / 8); // unmask - syscall( - SYS_rt_sigprocmask, SIG_SETMASK, &old_mask, NULL, sizeof(sigset_t)); + syscall(SYS_rt_sigprocmask, SIG_SETMASK, &old_mask, NULL, _NSIG / 8); // return from runtime handler; continue processing the crash on the // signal thread until the worker takes over