From c1bd96a3d5c75b980f270170eea08970430237a2 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Mon, 29 Sep 2025 12:05:36 +0200 Subject: [PATCH 01/20] fix: interop with managed .NET runtimes --- src/backends/sentry_backend_inproc.c | 60 +++++++++++++++++++ tests/fixtures/dotnet_signal/Program.cs | 13 ++-- .../fixtures/dotnet_signal/test_dotnet.csproj | 3 + tests/test_dotnet_signals.py | 49 +++++++++++---- 4 files changed, 109 insertions(+), 16 deletions(-) diff --git a/src/backends/sentry_backend_inproc.c b/src/backends/sentry_backend_inproc.c index 4ed124990..1a1f72e0c 100644 --- a/src/backends/sentry_backend_inproc.c +++ b/src/backends/sentry_backend_inproc.c @@ -455,6 +455,53 @@ registers_from_uctx(const sentry_ucontext_t *uctx) return registers; } +#ifdef SENTRY_PLATFORM_LINUX +static uintptr_t +get_stack_pointer(const sentry_ucontext_t *uctx) +{ +# if defined(__i386__) + return uctx->user_context->uc_mcontext.gregs[REG_ESP]; +# elif defined(__x86_64) + return uctx->user_context->uc_mcontext.gregs[REG_RSP]; +# elif defined(__ARM_EABI__) + return uctx->user_context->uc_mcontext.arm_sp; +# elif defined(__aarch64__) + return uctx->user_context->uc_mcontext.sp; +# elif defined(__mips__) + return uctx->user_context->uc_mcontext.gregs[MD_CONTEXT_MIPS_REG_SP]; +# elif defined(__riscv) + return uctx->user_context->uc_mcontext.__gregs[MD_CONTEXT_RISCV_REG_SP]; +# else + SENTRY_WARN("get_stack_pointer is not implemented for this architecture. " + "Signal chaining may not work as expected."); + return NULL; +# endif +} + +static uintptr_t +get_instruction_pointer(const sentry_ucontext_t *uctx) +{ +# if defined(__i386__) + return uctx->user_context->uc_mcontext.gregs[REG_EIP]; +# elif defined(__x86_64) + return uctx->user_context->uc_mcontext.gregs[REG_RIP]; +# elif defined(__ARM_EABI__) + return uctx->user_context->uc_mcontext.arm_pc; +# elif defined(__aarch64__) + return uctx->user_context->uc_mcontext.pc; +# elif defined(__mips__) + return uctx->user_context->uc_mcontext.pc; +# elif defined(__riscv) + return uctx->user_context->uc_mcontext.__gregs[MD_CONTEXT_RISCV_REG_PC]; +# else + SENTRY_WARN( + "get_instruction_pointer is not implemented for this architecture. " + "Signal chaining may not work as expected."); + return NULL; +# endif +} +#endif + static sentry_value_t make_signal_event( const struct signal_slot *sig_slot, const sentry_ucontext_t *uctx) @@ -569,11 +616,24 @@ handle_ucontext(const sentry_ucontext_t *uctx) // the next signal coming in if we didn't "leave" here. sentry__leave_signal_handler(); + uintptr_t ip = get_instruction_pointer(uctx); + uintptr_t sp = get_stack_pointer(uctx); + // invoke the previous handler (typically the CLR/Mono // signal-to-managed-exception handler) invoke_signal_handler( uctx->signum, uctx->siginfo, (void *)uctx->user_context); + // If the instruction or stack pointer changed, CLR/Mono converted + // the signal into a managed exception: + // https://github.com/dotnet/runtime/blob/6d96e28597e7da0d790d495ba834cc4908e442cd/src/mono/mono/mini/exceptions-arm64.c#L538 + if (ip != get_instruction_pointer(uctx) + || sp != get_stack_pointer(uctx)) { + SENTRY_DEBUG("runtime converted the signal to a managed " + "exception, we do not handle the signal"); + return; + } + // let's re-enter because it means this was an actual native crash sentry__enter_signal_handler(); SENTRY_DEBUG( diff --git a/tests/fixtures/dotnet_signal/Program.cs b/tests/fixtures/dotnet_signal/Program.cs index 951adaaf7..489b495d6 100644 --- a/tests/fixtures/dotnet_signal/Program.cs +++ b/tests/fixtures/dotnet_signal/Program.cs @@ -51,13 +51,16 @@ static void Main(string[] args) { Console.WriteLine("dereference a NULL object from managed code"); var s = default(string); - var c = s.Length; + var c = s!.Length; } - catch (NullReferenceException exception) + catch (NullReferenceException) { - Console.WriteLine("dereference another NULL object from managed code"); - var s = default(string); - var c = s.Length; + if (args is ["managed-exception"]) + { + Console.WriteLine("dereference another NULL object from managed code"); + var s = default(string); + var c = s!.Length; + } } } } diff --git a/tests/fixtures/dotnet_signal/test_dotnet.csproj b/tests/fixtures/dotnet_signal/test_dotnet.csproj index 64e34a8d4..b9c05c0e1 100644 --- a/tests/fixtures/dotnet_signal/test_dotnet.csproj +++ b/tests/fixtures/dotnet_signal/test_dotnet.csproj @@ -4,5 +4,8 @@ net8.0 enable enable + true + linux-x64 + Release diff --git a/tests/test_dotnet_signals.py b/tests/test_dotnet_signals.py index 8048cc600..ed8dc85a2 100644 --- a/tests/test_dotnet_signals.py +++ b/tests/test_dotnet_signals.py @@ -35,12 +35,12 @@ def assert_run_dir_with_envelope(database_path): ), f"There is more than one crash envelope ({len(crash_envelopes)}" -def run_dotnet(tmp_path, args): +def run_dotnet(tmp_path, args=[]): env = os.environ.copy() env["LD_LIBRARY_PATH"] = str(tmp_path) + ":" + env.get("LD_LIBRARY_PATH", "") return subprocess.Popen( - args, - cwd=str(project_fixture_path), + [str(tmp_path / "bin/test_dotnet")] + args, + cwd=tmp_path, env=env, text=True, stdout=subprocess.PIPE, @@ -49,11 +49,11 @@ def run_dotnet(tmp_path, args): def run_dotnet_managed_exception(tmp_path): - return run_dotnet(tmp_path, ["dotnet", "run"]) + return run_dotnet(tmp_path, ["managed-exception"]) def run_dotnet_native_crash(tmp_path): - return run_dotnet(tmp_path, ["dotnet", "run", "native-crash"]) + return run_dotnet(tmp_path, ["native-crash"]) @pytest.mark.skipif( @@ -83,20 +83,47 @@ def test_dotnet_signals_inproc(cmake): check=True, ) - # this runs the dotnet program with the Native SDK and chain-at-start, when managed code raises a signal that CLR convert to an exception. - dotnet_run = run_dotnet_managed_exception(tmp_path) + # AOT-compile the dotnet program + subprocess.run( + [ + "dotnet", + "publish", + "-o", + str(tmp_path / "bin"), + ], + cwd=project_fixture_path, + check=True, + ) + + # this runs the dotnet program with the Native SDK and chain-at-start, and triggers a `NullReferenceException` + # raising a signal that CLR converts to a managed exception, which is then handled by the managed code and + # not leaked out to the native code so no crash is registered. + dotnet_run = run_dotnet(tmp_path) dotnet_run_stdout, dotnet_run_stderr = dotnet_run.communicate() - # the program will fail with a `NullReferenceException`, but the Native SDK won't register a crash. - assert dotnet_run.returncode != 0 - assert ( + # the program handles the `NullReferenceException`, so the Native SDK won't register a crash. + assert dotnet_run.returncode == 0 + assert not ( "NullReferenceException" in dotnet_run_stderr ), f"Managed exception run failed.\nstdout:\n{dotnet_run_stdout}\nstderr:\n{dotnet_run_stderr}" - database_path = project_fixture_path / ".sentry-native" + database_path = tmp_path / ".sentry-native" assert database_path.exists(), "No database-path exists" assert not (database_path / "last_crash").exists(), "A crash was registered" assert_empty_run_dir(database_path) + # this runs the dotnet program with the Native SDK and chain-at-start, and triggers a `NullReferenceException` + # raising a signal that CLR converts to a managed exception. in this case, the managed code does not handle + # the exception, so it is leaked out to the native code and a crash is registered. + dotnet_run = run_dotnet_managed_exception(tmp_path) + dotnet_run_stdout, dotnet_run_stderr = dotnet_run.communicate() + + # the program does not handle the `NullReferenceException`, so the Native SDK will register a crash. + assert dotnet_run.returncode != 0 + assert ( + "NullReferenceException" in dotnet_run_stderr + ), f"Managed exception run failed.\nstdout:\n{dotnet_run_stdout}\nstderr:\n{dotnet_run_stderr}" + assert (database_path / "last_crash").exists(), "A crash was registered" + # this runs the dotnet program with the Native SDK and chain-at-start, when an actual native crash raises a signal dotnet_run = run_dotnet_native_crash(tmp_path) dotnet_run_stdout, dotnet_run_stderr = dotnet_run.communicate() From 45dccd6bccf044da7b7b359e59264302413a1b9c Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Mon, 29 Sep 2025 12:59:20 +0200 Subject: [PATCH 02/20] Update CHANGELOG.md --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 891bd6cb9..d22528e3a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## Unreleased + +**Fixes**: +- Fix interop with managed .NET runtimes. ([#1392](https://github.com/getsentry/sentry-native/pull/1392)) + ## 0.11.1 **Features**: From 02ebf36e5366e97c6a572c61737b1ef1e8d530de Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Mon, 29 Sep 2025 13:07:35 +0200 Subject: [PATCH 03/20] Clarify the comment --- src/backends/sentry_backend_inproc.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/backends/sentry_backend_inproc.c b/src/backends/sentry_backend_inproc.c index 1a1f72e0c..a5392d4f6 100644 --- a/src/backends/sentry_backend_inproc.c +++ b/src/backends/sentry_backend_inproc.c @@ -625,7 +625,8 @@ handle_ucontext(const sentry_ucontext_t *uctx) uctx->signum, uctx->siginfo, (void *)uctx->user_context); // If the instruction or stack pointer changed, CLR/Mono converted - // the signal into a managed exception: + // the signal into a managed exception and transferred execution to + // a managed exception handler. // https://github.com/dotnet/runtime/blob/6d96e28597e7da0d790d495ba834cc4908e442cd/src/mono/mono/mini/exceptions-arm64.c#L538 if (ip != get_instruction_pointer(uctx) || sp != get_stack_pointer(uctx)) { From 81293d859c6603d8a311f91ea2a958a8d4fd6030 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Mon, 29 Sep 2025 13:31:47 +0200 Subject: [PATCH 04/20] Remove hard-coded RID --- tests/fixtures/dotnet_signal/test_dotnet.csproj | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/fixtures/dotnet_signal/test_dotnet.csproj b/tests/fixtures/dotnet_signal/test_dotnet.csproj index b9c05c0e1..60ab93c37 100644 --- a/tests/fixtures/dotnet_signal/test_dotnet.csproj +++ b/tests/fixtures/dotnet_signal/test_dotnet.csproj @@ -5,7 +5,6 @@ enable enable true - linux-x64 Release From 10ba6bd8cfd60bd5d979ebd14e6015fcbea77624 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Mon, 29 Sep 2025 14:22:04 +0200 Subject: [PATCH 05/20] Align macros and fix constants --- src/backends/sentry_backend_inproc.c | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/backends/sentry_backend_inproc.c b/src/backends/sentry_backend_inproc.c index a5392d4f6..8418c3e2c 100644 --- a/src/backends/sentry_backend_inproc.c +++ b/src/backends/sentry_backend_inproc.c @@ -461,16 +461,16 @@ get_stack_pointer(const sentry_ucontext_t *uctx) { # if defined(__i386__) return uctx->user_context->uc_mcontext.gregs[REG_ESP]; -# elif defined(__x86_64) +# elif defined(__x86_64__) return uctx->user_context->uc_mcontext.gregs[REG_RSP]; -# elif defined(__ARM_EABI__) +# elif defined(__arm__) return uctx->user_context->uc_mcontext.arm_sp; # elif defined(__aarch64__) return uctx->user_context->uc_mcontext.sp; # elif defined(__mips__) - return uctx->user_context->uc_mcontext.gregs[MD_CONTEXT_MIPS_REG_SP]; + return uctx->user_context->uc_mcontext.gregs[29]; // REG_SP # elif defined(__riscv) - return uctx->user_context->uc_mcontext.__gregs[MD_CONTEXT_RISCV_REG_SP]; + return uctx->user_context->uc_mcontext.__gregs[2]; // REG_SP # else SENTRY_WARN("get_stack_pointer is not implemented for this architecture. " "Signal chaining may not work as expected."); @@ -483,16 +483,16 @@ get_instruction_pointer(const sentry_ucontext_t *uctx) { # if defined(__i386__) return uctx->user_context->uc_mcontext.gregs[REG_EIP]; -# elif defined(__x86_64) +# elif defined(__x86_64__) return uctx->user_context->uc_mcontext.gregs[REG_RIP]; -# elif defined(__ARM_EABI__) +# elif defined(__arm__) return uctx->user_context->uc_mcontext.arm_pc; # elif defined(__aarch64__) return uctx->user_context->uc_mcontext.pc; # elif defined(__mips__) return uctx->user_context->uc_mcontext.pc; # elif defined(__riscv) - return uctx->user_context->uc_mcontext.__gregs[MD_CONTEXT_RISCV_REG_PC]; + return uctx->user_context->uc_mcontext.__gregs[0]; // REG_PC # else SENTRY_WARN( "get_instruction_pointer is not implemented for this architecture. " From d9eafcdb355eb50a45b6ed5fa7f5439f2181a497 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Tue, 30 Sep 2025 11:18:13 +0200 Subject: [PATCH 06/20] Update tests/test_dotnet_signals.py Co-authored-by: Mischan Toosarani-Hausberger --- tests/test_dotnet_signals.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_dotnet_signals.py b/tests/test_dotnet_signals.py index ed8dc85a2..ec864d9b9 100644 --- a/tests/test_dotnet_signals.py +++ b/tests/test_dotnet_signals.py @@ -35,7 +35,9 @@ def assert_run_dir_with_envelope(database_path): ), f"There is more than one crash envelope ({len(crash_envelopes)}" -def run_dotnet(tmp_path, args=[]): +def run_dotnet(tmp_path, args=None): + if args is None: + args = [] env = os.environ.copy() env["LD_LIBRARY_PATH"] = str(tmp_path) + ":" + env.get("LD_LIBRARY_PATH", "") return subprocess.Popen( From 1bf0f524fe02c1023b9f68a95c834d1d5b8ecc48 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Tue, 30 Sep 2025 11:37:50 +0200 Subject: [PATCH 07/20] Clarify AOT in CHANGELOG & comment --- CHANGELOG.md | 2 +- src/backends/sentry_backend_inproc.c | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d22528e3a..fd3e9425d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ ## Unreleased **Fixes**: -- Fix interop with managed .NET runtimes. ([#1392](https://github.com/getsentry/sentry-native/pull/1392)) +- Fix AOT interop with managed .NET runtimes. ([#1392](https://github.com/getsentry/sentry-native/pull/1392)) ## 0.11.1 diff --git a/src/backends/sentry_backend_inproc.c b/src/backends/sentry_backend_inproc.c index 8418c3e2c..b9fbb1891 100644 --- a/src/backends/sentry_backend_inproc.c +++ b/src/backends/sentry_backend_inproc.c @@ -624,9 +624,10 @@ handle_ucontext(const sentry_ucontext_t *uctx) invoke_signal_handler( uctx->signum, uctx->siginfo, (void *)uctx->user_context); - // If the instruction or stack pointer changed, CLR/Mono converted - // the signal into a managed exception and transferred execution to - // a managed exception handler. + // If the execution returns here in AOT mode, and the instruction + // or stack pointer were changed, it means CLR/Mono converted the + // signal into a managed exception and transferred execution to a + // managed exception handler. // https://github.com/dotnet/runtime/blob/6d96e28597e7da0d790d495ba834cc4908e442cd/src/mono/mono/mini/exceptions-arm64.c#L538 if (ip != get_instruction_pointer(uctx) || sp != get_stack_pointer(uctx)) { From 060b18be6e3dbe1809fcdf108a957ee01824648a Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Tue, 30 Sep 2025 11:51:58 +0200 Subject: [PATCH 08/20] Separate JIT vs. AOT test cases --- .../fixtures/dotnet_signal/test_dotnet.csproj | 2 - tests/test_dotnet_signals.py | 116 ++++++++++++++---- 2 files changed, 92 insertions(+), 26 deletions(-) diff --git a/tests/fixtures/dotnet_signal/test_dotnet.csproj b/tests/fixtures/dotnet_signal/test_dotnet.csproj index 60ab93c37..64e34a8d4 100644 --- a/tests/fixtures/dotnet_signal/test_dotnet.csproj +++ b/tests/fixtures/dotnet_signal/test_dotnet.csproj @@ -4,7 +4,5 @@ net8.0 enable enable - true - Release diff --git a/tests/test_dotnet_signals.py b/tests/test_dotnet_signals.py index ec864d9b9..08435a1e8 100644 --- a/tests/test_dotnet_signals.py +++ b/tests/test_dotnet_signals.py @@ -35,7 +35,86 @@ def assert_run_dir_with_envelope(database_path): ), f"There is more than one crash envelope ({len(crash_envelopes)}" -def run_dotnet(tmp_path, args=None): +def run_jit(tmp_path, args): + env = os.environ.copy() + env["LD_LIBRARY_PATH"] = str(tmp_path) + ":" + env.get("LD_LIBRARY_PATH", "") + return subprocess.Popen( + args, + cwd=str(project_fixture_path), + env=env, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + + +def run_jit_managed_exception(tmp_path): + return run_jit(tmp_path, ["dotnet", "run", "managed-exception"]) + + +def run_jit_native_crash(tmp_path): + return run_jit(tmp_path, ["dotnet", "run", "native-crash"]) + + +@pytest.mark.skipif( + sys.platform != "linux" or is_x86 or is_asan or is_tsan, + reason="dotnet JIT signal handling is currently only supported on 64-bit Linux without sanitizers", +) +def test_jit_signals_inproc(cmake): + try: + # build native client library with inproc and the example for crash dumping + tmp_path = cmake( + ["sentry"], + {"SENTRY_BACKEND": "inproc", "SENTRY_TRANSPORT": "none"}, + ) + + # build the crashing native library + subprocess.run( + [ + "gcc", + "-Wall", + "-Wextra", + "-fPIC", + "-shared", + str(project_fixture_path / "crash.c"), + "-o", + str(tmp_path / "libcrash.so"), + ], + check=True, + ) + + # this runs the dotnet program with the Native SDK and chain-at-start, when managed code raises a signal that CLR convert to an exception. + dotnet_run = run_jit_managed_exception(tmp_path) + dotnet_run_stdout, dotnet_run_stderr = dotnet_run.communicate() + + # the program will fail with a `NullReferenceException`, but the Native SDK won't register a crash. + assert dotnet_run.returncode != 0 + assert ( + "NullReferenceException" in dotnet_run_stderr + ), f"Managed exception run failed.\nstdout:\n{dotnet_run_stdout}\nstderr:\n{dotnet_run_stderr}" + database_path = project_fixture_path / ".sentry-native" + assert database_path.exists(), "No database-path exists" + assert not (database_path / "last_crash").exists(), "A crash was registered" + assert_empty_run_dir(database_path) + + # this runs the dotnet program with the Native SDK and chain-at-start, when an actual native crash raises a signal + dotnet_run = run_jit_native_crash(tmp_path) + dotnet_run_stdout, dotnet_run_stderr = dotnet_run.communicate() + + # the program will fail with a SIGSEGV, that has been processed by the Native SDK which produced a crash envelope + assert dotnet_run.returncode != 0 + assert ( + "crash has been captured" in dotnet_run_stderr + ), f"Native exception run failed.\nstdout:\n{dotnet_run_stdout}\nstderr:\n{dotnet_run_stderr}" + assert (database_path / "last_crash").exists() + assert_run_dir_with_envelope(database_path) + finally: + shutil.rmtree(project_fixture_path / ".sentry-native", ignore_errors=True) + shutil.rmtree(project_fixture_path / "bin", ignore_errors=True) + shutil.rmtree(project_fixture_path / "obj", ignore_errors=True) + + +def run_aot(tmp_path, args=None): if args is None: args = [] env = os.environ.copy() @@ -50,19 +129,19 @@ def run_dotnet(tmp_path, args=None): ) -def run_dotnet_managed_exception(tmp_path): - return run_dotnet(tmp_path, ["managed-exception"]) +def run_aot_managed_exception(tmp_path): + return run_aot(tmp_path, ["managed-exception"]) -def run_dotnet_native_crash(tmp_path): - return run_dotnet(tmp_path, ["native-crash"]) +def run_aot_native_crash(tmp_path): + return run_aot(tmp_path, ["native-crash"]) @pytest.mark.skipif( sys.platform != "linux" or is_x86 or is_asan or is_tsan, - reason="dotnet signal handling is currently only supported on 64-bit Linux without sanitizers", + reason="dotnet AOT signal handling is currently only supported on 64-bit Linux without sanitizers", ) -def test_dotnet_signals_inproc(cmake): +def test_aot_signals_inproc(cmake): try: # build native client library with inproc and the example for crash dumping tmp_path = cmake( @@ -90,6 +169,8 @@ def test_dotnet_signals_inproc(cmake): [ "dotnet", "publish", + "-p:PublishAot=true", + "-p:Configuration=Release", "-o", str(tmp_path / "bin"), ], @@ -97,10 +178,10 @@ def test_dotnet_signals_inproc(cmake): check=True, ) - # this runs the dotnet program with the Native SDK and chain-at-start, and triggers a `NullReferenceException` + # this runs the dotnet program in AOT mode with the Native SDK and chain-at-start, and triggers a `NullReferenceException` # raising a signal that CLR converts to a managed exception, which is then handled by the managed code and # not leaked out to the native code so no crash is registered. - dotnet_run = run_dotnet(tmp_path) + dotnet_run = run_aot(tmp_path) dotnet_run_stdout, dotnet_run_stderr = dotnet_run.communicate() # the program handles the `NullReferenceException`, so the Native SDK won't register a crash. @@ -113,21 +194,8 @@ def test_dotnet_signals_inproc(cmake): assert not (database_path / "last_crash").exists(), "A crash was registered" assert_empty_run_dir(database_path) - # this runs the dotnet program with the Native SDK and chain-at-start, and triggers a `NullReferenceException` - # raising a signal that CLR converts to a managed exception. in this case, the managed code does not handle - # the exception, so it is leaked out to the native code and a crash is registered. - dotnet_run = run_dotnet_managed_exception(tmp_path) - dotnet_run_stdout, dotnet_run_stderr = dotnet_run.communicate() - - # the program does not handle the `NullReferenceException`, so the Native SDK will register a crash. - assert dotnet_run.returncode != 0 - assert ( - "NullReferenceException" in dotnet_run_stderr - ), f"Managed exception run failed.\nstdout:\n{dotnet_run_stdout}\nstderr:\n{dotnet_run_stderr}" - assert (database_path / "last_crash").exists(), "A crash was registered" - # this runs the dotnet program with the Native SDK and chain-at-start, when an actual native crash raises a signal - dotnet_run = run_dotnet_native_crash(tmp_path) + dotnet_run = run_aot_native_crash(tmp_path) dotnet_run_stdout, dotnet_run_stderr = dotnet_run.communicate() # the program will fail with a SIGSEGV, that has been processed by the Native SDK which produced a crash envelope @@ -138,6 +206,6 @@ def test_dotnet_signals_inproc(cmake): assert (database_path / "last_crash").exists() assert_run_dir_with_envelope(database_path) finally: - shutil.rmtree(project_fixture_path / ".sentry-native", ignore_errors=True) + shutil.rmtree(tmp_path / ".sentry-native", ignore_errors=True) shutil.rmtree(project_fixture_path / "bin", ignore_errors=True) shutil.rmtree(project_fixture_path / "obj", ignore_errors=True) From d4dd399b4827ea1e43b3330dffb6b580ea635c32 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Tue, 30 Sep 2025 21:15:41 +0200 Subject: [PATCH 09/20] Apply suggestions from code review --- src/backends/sentry_backend_inproc.c | 34 ++++++++++++++++------------ 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/src/backends/sentry_backend_inproc.c b/src/backends/sentry_backend_inproc.c index b9fbb1891..779591a64 100644 --- a/src/backends/sentry_backend_inproc.c +++ b/src/backends/sentry_backend_inproc.c @@ -580,20 +580,6 @@ handle_ucontext(const sentry_ucontext_t *uctx) SENTRY_INFO("entering signal handler"); - const struct signal_slot *sig_slot = NULL; - for (int i = 0; i < SIGNAL_COUNT; ++i) { -#ifdef SENTRY_PLATFORM_UNIX - if (SIGNAL_DEFINITIONS[i].signum == uctx->signum) { -#elif defined SENTRY_PLATFORM_WINDOWS - if (SIGNAL_DEFINITIONS[i].signum - == uctx->exception_ptrs.ExceptionRecord->ExceptionCode) { -#else -# error Unsupported platform -#endif - sig_slot = &SIGNAL_DEFINITIONS[i]; - } - } - #ifdef SENTRY_PLATFORM_UNIX // inform the sentry_sync system that we're in a signal handler. This will // make mutexes spin on a spinlock instead as it's no longer safe to use a @@ -615,6 +601,9 @@ handle_ucontext(const sentry_ucontext_t *uctx) // handler and that would mean we couldn't enter this handler with // the next signal coming in if we didn't "leave" here. sentry__leave_signal_handler(); + if (!options->enable_logging_when_crashed) { + sentry__logger_enable(); + } uintptr_t ip = get_instruction_pointer(uctx); uintptr_t sp = get_stack_pointer(uctx); @@ -637,12 +626,29 @@ handle_ucontext(const sentry_ucontext_t *uctx) } // let's re-enter because it means this was an actual native crash + if (!options->enable_logging_when_crashed) { + sentry__logger_disable(); + } sentry__enter_signal_handler(); SENTRY_DEBUG( "return from runtime signal handler, we handle the signal"); } #endif + const struct signal_slot *sig_slot = NULL; + for (int i = 0; i < SIGNAL_COUNT; ++i) { +#ifdef SENTRY_PLATFORM_UNIX + if (SIGNAL_DEFINITIONS[i].signum == uctx->signum) { +#elif defined SENTRY_PLATFORM_WINDOWS + if (SIGNAL_DEFINITIONS[i].signum + == uctx->exception_ptrs.ExceptionRecord->ExceptionCode) { +#else +# error Unsupported platform +#endif + sig_slot = &SIGNAL_DEFINITIONS[i]; + } + } + #ifdef SENTRY_PLATFORM_UNIX // use a signal-safe allocator before we tear down. sentry__page_allocator_enable(); From 4da4c4ade08e4475c75132e4491c3d37a9b03536 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Tue, 30 Sep 2025 21:36:56 +0200 Subject: [PATCH 10/20] Add __mips64__ and __s390x__ --- src/backends/sentry_backend_inproc.c | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/backends/sentry_backend_inproc.c b/src/backends/sentry_backend_inproc.c index 779591a64..bc1c4ae6b 100644 --- a/src/backends/sentry_backend_inproc.c +++ b/src/backends/sentry_backend_inproc.c @@ -467,10 +467,12 @@ get_stack_pointer(const sentry_ucontext_t *uctx) return uctx->user_context->uc_mcontext.arm_sp; # elif defined(__aarch64__) return uctx->user_context->uc_mcontext.sp; -# elif defined(__mips__) +# elif defined(__mips__) || defined(__mips64__) return uctx->user_context->uc_mcontext.gregs[29]; // REG_SP # elif defined(__riscv) return uctx->user_context->uc_mcontext.__gregs[2]; // REG_SP +# elif defined(__s390x__) + return uctx->user_context->uc_mcontext.gregs[15]; # else SENTRY_WARN("get_stack_pointer is not implemented for this architecture. " "Signal chaining may not work as expected."); @@ -489,10 +491,12 @@ get_instruction_pointer(const sentry_ucontext_t *uctx) return uctx->user_context->uc_mcontext.arm_pc; # elif defined(__aarch64__) return uctx->user_context->uc_mcontext.pc; -# elif defined(__mips__) +# elif defined(__mips__) || defined(__mips64__) return uctx->user_context->uc_mcontext.pc; # elif defined(__riscv) return uctx->user_context->uc_mcontext.__gregs[0]; // REG_PC +# elif defined(__s390x__) + return uctx->user_context->uc_mcontext.psw.addr; # else SENTRY_WARN( "get_instruction_pointer is not implemented for this architecture. " From e8576cf197714c4dcae360865c5445c477ef2a28 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Tue, 21 Oct 2025 08:23:51 +0200 Subject: [PATCH 11/20] Avoid double-invoking signal handlers when CHAIN_AT_START (crashes on arm64) --- src/backends/sentry_backend_inproc.c | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/backends/sentry_backend_inproc.c b/src/backends/sentry_backend_inproc.c index cace1cf12..725637057 100644 --- a/src/backends/sentry_backend_inproc.c +++ b/src/backends/sentry_backend_inproc.c @@ -586,6 +586,8 @@ handle_ucontext(const sentry_ucontext_t *uctx) SENTRY_INFO("entering signal handler"); #ifdef SENTRY_PLATFORM_UNIX + sentry_handler_strategy_t strategy = SENTRY_HANDLER_STRATEGY_DEFAULT; + // inform the sentry_sync system that we're in a signal handler. This will // make mutexes spin on a spinlock instead as it's no longer safe to use a // pthread mutex. @@ -603,8 +605,8 @@ handle_ucontext(const sentry_ucontext_t *uctx) // cases, we shouldn't react to the signal at all and let their handler // discontinue the signal chain by invoking the runtime handler before // we process the signal. - if (sentry_options_get_handler_strategy(options) - == SENTRY_HANDLER_STRATEGY_CHAIN_AT_START) { + strategy = sentry_options_get_handler_strategy(options); + if (strategy == SENTRY_HANDLER_STRATEGY_CHAIN_AT_START) { SENTRY_DEBUG("defer to runtime signal handler at start"); // there is a good chance that we won't return from the previous // handler and that would mean we couldn't enter this handler with @@ -719,8 +721,10 @@ handle_ucontext(const sentry_ucontext_t *uctx) // forward as we're not restoring the page allocator. reset_signal_handlers(); sentry__leave_signal_handler(); - invoke_signal_handler( - uctx->signum, uctx->siginfo, (void *)uctx->user_context); + if (strategy != SENTRY_HANDLER_STRATEGY_CHAIN_AT_START) { + invoke_signal_handler( + uctx->signum, uctx->siginfo, (void *)uctx->user_context); + } #endif } From 06c84e12eb69ca9f21beabc0f89d0e99f7b8d4e8 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Tue, 21 Oct 2025 10:12:53 +0200 Subject: [PATCH 12/20] Minimize test diff --- tests/fixtures/dotnet_signal/Program.cs | 6 +++--- tests/test_dotnet_signals.py | 18 +++++++++--------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/tests/fixtures/dotnet_signal/Program.cs b/tests/fixtures/dotnet_signal/Program.cs index 489b495d6..bcb5654ae 100644 --- a/tests/fixtures/dotnet_signal/Program.cs +++ b/tests/fixtures/dotnet_signal/Program.cs @@ -51,15 +51,15 @@ static void Main(string[] args) { Console.WriteLine("dereference a NULL object from managed code"); var s = default(string); - var c = s!.Length; + var c = s.Length; } - catch (NullReferenceException) + catch (NullReferenceException exception) { if (args is ["managed-exception"]) { Console.WriteLine("dereference another NULL object from managed code"); var s = default(string); - var c = s!.Length; + var c = s.Length; } } } diff --git a/tests/test_dotnet_signals.py b/tests/test_dotnet_signals.py index 08435a1e8..0f75184d5 100644 --- a/tests/test_dotnet_signals.py +++ b/tests/test_dotnet_signals.py @@ -35,7 +35,7 @@ def assert_run_dir_with_envelope(database_path): ), f"There is more than one crash envelope ({len(crash_envelopes)}" -def run_jit(tmp_path, args): +def run_dotnet(tmp_path, args): env = os.environ.copy() env["LD_LIBRARY_PATH"] = str(tmp_path) + ":" + env.get("LD_LIBRARY_PATH", "") return subprocess.Popen( @@ -48,19 +48,19 @@ def run_jit(tmp_path, args): ) -def run_jit_managed_exception(tmp_path): - return run_jit(tmp_path, ["dotnet", "run", "managed-exception"]) +def run_dotnet_managed_exception(tmp_path): + return run_dotnet(tmp_path, ["dotnet", "run", "managed-exception"]) -def run_jit_native_crash(tmp_path): - return run_jit(tmp_path, ["dotnet", "run", "native-crash"]) +def run_dotnet_native_crash(tmp_path): + return run_dotnet(tmp_path, ["dotnet", "run", "native-crash"]) @pytest.mark.skipif( sys.platform != "linux" or is_x86 or is_asan or is_tsan, - reason="dotnet JIT signal handling is currently only supported on 64-bit Linux without sanitizers", + reason="dotnet signal handling is currently only supported on 64-bit Linux without sanitizers", ) -def test_jit_signals_inproc(cmake): +def test_dotnet_signals_inproc(cmake): try: # build native client library with inproc and the example for crash dumping tmp_path = cmake( @@ -84,7 +84,7 @@ def test_jit_signals_inproc(cmake): ) # this runs the dotnet program with the Native SDK and chain-at-start, when managed code raises a signal that CLR convert to an exception. - dotnet_run = run_jit_managed_exception(tmp_path) + dotnet_run = run_dotnet_managed_exception(tmp_path) dotnet_run_stdout, dotnet_run_stderr = dotnet_run.communicate() # the program will fail with a `NullReferenceException`, but the Native SDK won't register a crash. @@ -98,7 +98,7 @@ def test_jit_signals_inproc(cmake): assert_empty_run_dir(database_path) # this runs the dotnet program with the Native SDK and chain-at-start, when an actual native crash raises a signal - dotnet_run = run_jit_native_crash(tmp_path) + dotnet_run = run_dotnet_native_crash(tmp_path) dotnet_run_stdout, dotnet_run_stderr = dotnet_run.communicate() # the program will fail with a SIGSEGV, that has been processed by the Native SDK which produced a crash envelope From e92bb00018835fd3d7cd241891a733bf1fe00994 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Tue, 21 Oct 2025 15:23:09 +0200 Subject: [PATCH 13/20] Less is more - drop mips, risc, s390x They are irrelevant for sentry-dotnet or .NET on Android, and there are no tests checking if they even work. --- src/backends/sentry_backend_inproc.c | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/backends/sentry_backend_inproc.c b/src/backends/sentry_backend_inproc.c index 725637057..98c2c1e08 100644 --- a/src/backends/sentry_backend_inproc.c +++ b/src/backends/sentry_backend_inproc.c @@ -468,12 +468,6 @@ get_stack_pointer(const sentry_ucontext_t *uctx) return uctx->user_context->uc_mcontext.arm_sp; # elif defined(__aarch64__) return uctx->user_context->uc_mcontext.sp; -# elif defined(__mips__) || defined(__mips64__) - return uctx->user_context->uc_mcontext.gregs[29]; // REG_SP -# elif defined(__riscv) - return uctx->user_context->uc_mcontext.__gregs[2]; // REG_SP -# elif defined(__s390x__) - return uctx->user_context->uc_mcontext.gregs[15]; # else SENTRY_WARN("get_stack_pointer is not implemented for this architecture. " "Signal chaining may not work as expected."); @@ -492,12 +486,6 @@ get_instruction_pointer(const sentry_ucontext_t *uctx) return uctx->user_context->uc_mcontext.arm_pc; # elif defined(__aarch64__) return uctx->user_context->uc_mcontext.pc; -# elif defined(__mips__) || defined(__mips64__) - return uctx->user_context->uc_mcontext.pc; -# elif defined(__riscv) - return uctx->user_context->uc_mcontext.__gregs[0]; // REG_PC -# elif defined(__s390x__) - return uctx->user_context->uc_mcontext.psw.addr; # else SENTRY_WARN( "get_instruction_pointer is not implemented for this architecture. " From de6d67c6e76e6a54a3c4a0d0de25bbd9bce31935 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Wed, 22 Oct 2025 13:48:18 +0200 Subject: [PATCH 14/20] Move log flush --- src/backends/sentry_backend_inproc.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/backends/sentry_backend_inproc.c b/src/backends/sentry_backend_inproc.c index 98c2c1e08..141ee0d08 100644 --- a/src/backends/sentry_backend_inproc.c +++ b/src/backends/sentry_backend_inproc.c @@ -583,10 +583,6 @@ handle_ucontext(const sentry_ucontext_t *uctx) #endif SENTRY_WITH_OPTIONS (options) { - // Flush logs in a crash-safe manner before crash handling - if (options->enable_logs) { - sentry__logs_flush_crash_safe(); - } #ifdef SENTRY_PLATFORM_LINUX // On Linux (and thus Android) CLR/Mono converts signals provoked by // AOT/JIT-generated native code into managed code exceptions. In these @@ -652,6 +648,10 @@ handle_ucontext(const sentry_ucontext_t *uctx) // use a signal-safe allocator before we tear down. sentry__page_allocator_enable(); #endif + // Flush logs in a crash-safe manner before crash handling + if (options->enable_logs) { + sentry__logs_flush_crash_safe(); + } sentry_value_t event = make_signal_event(sig_slot, uctx); bool should_handle = true; From 4a739075e215473c7bdf65ead3aabfbe58aa4be0 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Wed, 22 Oct 2025 14:02:03 +0200 Subject: [PATCH 15/20] Update test (native-crash, managed-exception, unhandled-managed-exception) --- tests/fixtures/dotnet_signal/Program.cs | 14 +++++++------- tests/test_dotnet_signals.py | 20 ++++++++++++++++++++ 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/tests/fixtures/dotnet_signal/Program.cs b/tests/fixtures/dotnet_signal/Program.cs index bcb5654ae..a3c2596dc 100644 --- a/tests/fixtures/dotnet_signal/Program.cs +++ b/tests/fixtures/dotnet_signal/Program.cs @@ -45,7 +45,7 @@ static void Main(string[] args) { native_crash(); } - else + else if (args.Contains("managed-exception")) { try { @@ -55,13 +55,13 @@ static void Main(string[] args) } catch (NullReferenceException exception) { - if (args is ["managed-exception"]) - { - Console.WriteLine("dereference another NULL object from managed code"); - var s = default(string); - var c = s.Length; - } } } + else if (args.Contains("unhandled-managed-exception")) + { + Console.WriteLine("dereference another NULL object from managed code"); + var s = default(string); + var c = s.Length; + } } } \ No newline at end of file diff --git a/tests/test_dotnet_signals.py b/tests/test_dotnet_signals.py index 0f75184d5..799d06db9 100644 --- a/tests/test_dotnet_signals.py +++ b/tests/test_dotnet_signals.py @@ -52,6 +52,10 @@ def run_dotnet_managed_exception(tmp_path): return run_dotnet(tmp_path, ["dotnet", "run", "managed-exception"]) +def run_dotnet_unhandled_managed_exception(tmp_path): + return run_dotnet(tmp_path, ["dotnet", "run", "unhandled-managed-exception"]) + + def run_dotnet_native_crash(tmp_path): return run_dotnet(tmp_path, ["dotnet", "run", "native-crash"]) @@ -84,9 +88,25 @@ def test_dotnet_signals_inproc(cmake): ) # this runs the dotnet program with the Native SDK and chain-at-start, when managed code raises a signal that CLR convert to an exception. + # raising a signal that CLR converts to a managed exception, which is then handled by the managed code and + # not leaked out to the native code so no crash is registered. dotnet_run = run_dotnet_managed_exception(tmp_path) dotnet_run_stdout, dotnet_run_stderr = dotnet_run.communicate() + # the program handles the `NullReferenceException`, so the Native SDK won't register a crash. + assert dotnet_run.returncode == 0 + assert not ( + "NullReferenceException" in dotnet_run_stderr + ), f"Managed exception run failed.\nstdout:\n{dotnet_run_stdout}\nstderr:\n{dotnet_run_stderr}" + database_path = project_fixture_path / ".sentry-native" + assert database_path.exists(), "No database-path exists" + assert not (database_path / "last_crash").exists(), "A crash was registered" + assert_empty_run_dir(database_path) + + # this runs the dotnet program with the Native SDK and chain-at-start, when managed code raises a signal that CLR convert to an exception. + dotnet_run = run_dotnet_unhandled_managed_exception(tmp_path) + dotnet_run_stdout, dotnet_run_stderr = dotnet_run.communicate() + # the program will fail with a `NullReferenceException`, but the Native SDK won't register a crash. assert dotnet_run.returncode != 0 assert ( From 72df376acad95e524c1eb435b7c5afa9f3df5319 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Wed, 22 Oct 2025 16:57:20 +0200 Subject: [PATCH 16/20] Update tests/fixtures/dotnet_signal/Program.cs Co-authored-by: Mischan Toosarani-Hausberger --- tests/fixtures/dotnet_signal/Program.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/fixtures/dotnet_signal/Program.cs b/tests/fixtures/dotnet_signal/Program.cs index a3c2596dc..4e6217ac3 100644 --- a/tests/fixtures/dotnet_signal/Program.cs +++ b/tests/fixtures/dotnet_signal/Program.cs @@ -59,7 +59,7 @@ static void Main(string[] args) } else if (args.Contains("unhandled-managed-exception")) { - Console.WriteLine("dereference another NULL object from managed code"); + Console.WriteLine("dereference a NULL object from managed code (unhandled)"); var s = default(string); var c = s.Length; } From 95c3c9b4d04921511bb3117160f455050ca686ef Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Wed, 22 Oct 2025 17:35:25 +0200 Subject: [PATCH 17/20] AOT test case: add unhandled-managed-exception --- tests/test_dotnet_signals.py | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/tests/test_dotnet_signals.py b/tests/test_dotnet_signals.py index 799d06db9..9d7fe07c9 100644 --- a/tests/test_dotnet_signals.py +++ b/tests/test_dotnet_signals.py @@ -153,6 +153,10 @@ def run_aot_managed_exception(tmp_path): return run_aot(tmp_path, ["managed-exception"]) +def run_aot_unhandled_managed_exception(tmp_path): + return run_aot(tmp_path, ["unhandled-managed-exception"]) + + def run_aot_native_crash(tmp_path): return run_aot(tmp_path, ["native-crash"]) @@ -201,7 +205,7 @@ def test_aot_signals_inproc(cmake): # this runs the dotnet program in AOT mode with the Native SDK and chain-at-start, and triggers a `NullReferenceException` # raising a signal that CLR converts to a managed exception, which is then handled by the managed code and # not leaked out to the native code so no crash is registered. - dotnet_run = run_aot(tmp_path) + dotnet_run = run_aot_managed_exception(tmp_path) dotnet_run_stdout, dotnet_run_stderr = dotnet_run.communicate() # the program handles the `NullReferenceException`, so the Native SDK won't register a crash. @@ -214,6 +218,22 @@ def test_aot_signals_inproc(cmake): assert not (database_path / "last_crash").exists(), "A crash was registered" assert_empty_run_dir(database_path) + # this runs the dotnet program in AOT mode with the Native SDK and chain-at-start, and triggers a `NullReferenceException` + # raising a signal that CLR converts to a managed exception, which is then not handled by the managed code but + # leaked out to the native code so a crash is registered. + dotnet_run = run_aot_unhandled_managed_exception(tmp_path) + dotnet_run_stdout, dotnet_run_stderr = dotnet_run.communicate() + + # the program will fail with a `NullReferenceException`, so the Native SDK will register a crash. + assert dotnet_run.returncode != 0 + assert ( + "NullReferenceException" in dotnet_run_stderr + ), f"Managed exception run failed.\nstdout:\n{dotnet_run_stdout}\nstderr:\n{dotnet_run_stderr}" + database_path = tmp_path / ".sentry-native" + assert database_path.exists(), "No database-path exists" + assert (database_path / "last_crash").exists() + assert_run_dir_with_envelope(database_path) + # this runs the dotnet program with the Native SDK and chain-at-start, when an actual native crash raises a signal dotnet_run = run_aot_native_crash(tmp_path) dotnet_run_stdout, dotnet_run_stderr = dotnet_run.communicate() From b6e37d1a7c677af6daf8154ba61c04bab1db93dd Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Wed, 22 Oct 2025 19:25:20 +0200 Subject: [PATCH 18/20] Skip the crashy libbacktrace fallback from chained signal handlers --- src/backends/sentry_backend_inproc.c | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/backends/sentry_backend_inproc.c b/src/backends/sentry_backend_inproc.c index 141ee0d08..402585d7a 100644 --- a/src/backends/sentry_backend_inproc.c +++ b/src/backends/sentry_backend_inproc.c @@ -496,8 +496,8 @@ get_instruction_pointer(const sentry_ucontext_t *uctx) #endif static sentry_value_t -make_signal_event( - const struct signal_slot *sig_slot, const sentry_ucontext_t *uctx) +make_signal_event(const struct signal_slot *sig_slot, + const sentry_ucontext_t *uctx, sentry_handler_strategy_t strategy) { sentry_value_t event = sentry_value_new_event(); sentry_value_set_by_key( @@ -533,10 +533,11 @@ make_signal_event( = sentry_unwind_stack_from_ucontext(uctx, &backtrace[0], MAX_FRAMES); SENTRY_DEBUGF( "captured backtrace from ucontext with %lu frames", frame_count); - // if unwinding from a ucontext didn't yield any results, try again with a - // direct unwind. this is most likely the case when using `libbacktrace`, - // since that does not allow to unwind from a ucontext at all. - if (!frame_count) { + // if unwinding from a ucontext didn't yield any results and we haven't + // chained signal handlers, try again with a direct unwind. this is most + // likely the case when using `libbacktrace`, since that does not allow to + // unwind from a ucontext at all. + if (!frame_count && strategy != SENTRY_HANDLER_STRATEGY_CHAIN_AT_START) { frame_count = sentry_unwind_stack(NULL, &backtrace[0], MAX_FRAMES); } SENTRY_DEBUGF("captured backtrace with %lu frames", frame_count); @@ -653,7 +654,7 @@ handle_ucontext(const sentry_ucontext_t *uctx) sentry__logs_flush_crash_safe(); } - sentry_value_t event = make_signal_event(sig_slot, uctx); + sentry_value_t event = make_signal_event(sig_slot, uctx, strategy); bool should_handle = true; sentry__write_crash_marker(options); From ac6877e2c795e63703c0ffd5353b380ece535647 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Wed, 22 Oct 2025 19:36:29 +0200 Subject: [PATCH 19/20] fix build on windows --- src/backends/sentry_backend_inproc.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/backends/sentry_backend_inproc.c b/src/backends/sentry_backend_inproc.c index 402585d7a..d3e3cffbd 100644 --- a/src/backends/sentry_backend_inproc.c +++ b/src/backends/sentry_backend_inproc.c @@ -574,9 +574,8 @@ handle_ucontext(const sentry_ucontext_t *uctx) SENTRY_INFO("entering signal handler"); -#ifdef SENTRY_PLATFORM_UNIX sentry_handler_strategy_t strategy = SENTRY_HANDLER_STRATEGY_DEFAULT; - +#ifdef SENTRY_PLATFORM_UNIX // inform the sentry_sync system that we're in a signal handler. This will // make mutexes spin on a spinlock instead as it's no longer safe to use a // pthread mutex. From 2ef7c5bccffe7eeabe2c71ba51cccaad93f13bfa Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Mon, 27 Oct 2025 14:34:38 +0100 Subject: [PATCH 20/20] Clarify why the fallback is skipped --- src/backends/sentry_backend_inproc.c | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/backends/sentry_backend_inproc.c b/src/backends/sentry_backend_inproc.c index d3e3cffbd..8a8755a75 100644 --- a/src/backends/sentry_backend_inproc.c +++ b/src/backends/sentry_backend_inproc.c @@ -533,10 +533,11 @@ make_signal_event(const struct signal_slot *sig_slot, = sentry_unwind_stack_from_ucontext(uctx, &backtrace[0], MAX_FRAMES); SENTRY_DEBUGF( "captured backtrace from ucontext with %lu frames", frame_count); - // if unwinding from a ucontext didn't yield any results and we haven't - // chained signal handlers, try again with a direct unwind. this is most - // likely the case when using `libbacktrace`, since that does not allow to - // unwind from a ucontext at all. + // if unwinding from a ucontext didn't yield any results, try again with a + // direct unwind. this is most likely the case when using `libbacktrace`, + // since that does not allow to unwind from a ucontext at all. the fallback + // is skipped with the "chain at start" strategy because `libbacktrace` + // crashes, and would likely not provide helpful information anyway. if (!frame_count && strategy != SENTRY_HANDLER_STRATEGY_CHAIN_AT_START) { frame_count = sentry_unwind_stack(NULL, &backtrace[0], MAX_FRAMES); }