From eaf7854e9da9e147b44f1a3eb817bb364e56a98f Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Fri, 20 Mar 2026 11:29:05 +0100 Subject: [PATCH 1/4] [mono] Preserve signal handlers during crash chaining MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When mono_set_crash_chaining(true) is enabled, mono_handle_native_crash unconditionally resets SIGABRT, SIGILL, SIGCHLD, and SIGQUIT to SIG_DFL. This is problematic on Android where multiple threads may trigger signals concurrently — for example, FORTIFY detecting a destroyed mutex on an HWUI thread raises SIGABRT, but because the handler was reset to SIG_DFL, it kills the process before Mono can chain the original crash to the previous handler (e.g. sentry-native). Guard the signal handler reset with !mono_do_crash_chaining so that when crash chaining is enabled, Mono's handlers remain installed during native crash processing. This allows the crash to be fully handled and chained to any previously installed handlers before the process terminates. The fix has no effect when crash_chaining is disabled (the default), as the existing SIG_DFL reset behavior is preserved. Includes an Android functional test (CrashChaining) that: - Installs a SIGSEGV handler before mono_jit_init (gated by TEST_CRASH_CHAINING env var) to simulate a pre-Mono crash handler - Triggers a native SIGSEGV that Mono chains to the pre-Mono handler - Verifies SIGABRT was not reset to SIG_DFL during crash processing - Without the fix, the test process is killed by SIGABRT (test crashes) - With the fix, the test returns exit code 42 (pass) --- src/mono/mono/mini/mini-exceptions.c | 33 +++-- .../AndroidAppBuilder/Templates/monodroid.c | 125 ++++++++++++++++++ ....Device_Emulator.CrashChaining.Test.csproj | 18 +++ .../Device_Emulator/CrashChaining/Program.cs | 29 ++++ 4 files changed, 191 insertions(+), 14 deletions(-) create mode 100644 src/tests/FunctionalTests/Android/Device_Emulator/CrashChaining/Android.Device_Emulator.CrashChaining.Test.csproj create mode 100644 src/tests/FunctionalTests/Android/Device_Emulator/CrashChaining/Program.cs diff --git a/src/mono/mono/mini/mini-exceptions.c b/src/mono/mono/mini/mini-exceptions.c index 188fa9d340ede1..f3fe6c2c211d7d 100644 --- a/src/mono/mono/mini/mini-exceptions.c +++ b/src/mono/mono/mini/mini-exceptions.c @@ -2955,24 +2955,29 @@ mono_handle_native_crash (const char *signal, MonoContext *mctx, MONO_SIG_HANDLE MonoJitTlsData *jit_tls = mono_tls_get_jit_tls (); #ifdef MONO_ARCH_USE_SIGACTION - struct sigaction sa; - sa.sa_handler = SIG_DFL; - sigemptyset (&sa.sa_mask); - sa.sa_flags = 0; + // When crash chaining is enabled, keep our signal handlers installed so + // that secondary signals (e.g. SIGABRT from FORTIFY on other threads) + // don't kill the process with SIG_DFL before we can chain the original + // crash to the previous handler. + if (!mono_do_crash_chaining) { + struct sigaction sa; + sa.sa_handler = SIG_DFL; + sigemptyset (&sa.sa_mask); + sa.sa_flags = 0; - /* Remove our SIGABRT handler */ - g_assert (sigaction (SIGABRT, &sa, NULL) != -1); + /* Remove our SIGABRT handler */ + g_assert (sigaction (SIGABRT, &sa, NULL) != -1); - /* On some systems we get a SIGILL when calling abort (), because it might - * fail to raise SIGABRT */ - g_assert (sigaction (SIGILL, &sa, NULL) != -1); + /* On some systems we get a SIGILL when calling abort (), because it might + * fail to raise SIGABRT */ + g_assert (sigaction (SIGILL, &sa, NULL) != -1); - /* Remove SIGCHLD, it uses the finalizer thread */ - g_assert (sigaction (SIGCHLD, &sa, NULL) != -1); - - /* Remove SIGQUIT, we are already dumping threads */ - g_assert (sigaction (SIGQUIT, &sa, NULL) != -1); + /* Remove SIGCHLD, it uses the finalizer thread */ + g_assert (sigaction (SIGCHLD, &sa, NULL) != -1); + /* Remove SIGQUIT, we are already dumping threads */ + g_assert (sigaction (SIGQUIT, &sa, NULL) != -1); + } #endif if (mini_debug_options.suspend_on_native_crash) { diff --git a/src/tasks/AndroidAppBuilder/Templates/monodroid.c b/src/tasks/AndroidAppBuilder/Templates/monodroid.c index 5ba3952a43d78a..664f73009c639b 100644 --- a/src/tasks/AndroidAppBuilder/Templates/monodroid.c +++ b/src/tasks/AndroidAppBuilder/Templates/monodroid.c @@ -24,6 +24,7 @@ #include #include #include +#include #include /********* exported symbols *********/ @@ -46,6 +47,12 @@ Java_net_dot_MonoRunner_freeNativeResources (JNIEnv* env, jobject thiz); void invoke_external_native_api (void (*callback)(void)); +int +test_crash_chaining (void); + +void +test_crash_chaining_install_pre_mono_handler (void); + /********* implementation *********/ static const char* g_bundle_path = NULL; @@ -296,6 +303,11 @@ mono_droid_runtime_init (const char* executable, int local_date_time_offset) mono_set_signal_chaining (true); mono_set_crash_chaining (true); + // Install the crash chaining test handler before mono_jit_init + // only when the crash chaining test is running. + if (getenv("TEST_CRASH_CHAINING")) + test_crash_chaining_install_pre_mono_handler(); + if (wait_for_debugger) { char* options[] = { "--debugger-agent=transport=dt_socket,server=y,address=0.0.0.0:55556" }; mono_jit_parse_options (1, options); @@ -467,3 +479,116 @@ invoke_external_native_api (void (*callback)(void)) if (callback) callback(); } + +/* + * Test for crash chaining: verify that mono_handle_native_crash does not + * reset SIGABRT to SIG_DFL when crash_chaining is enabled. + * + * Strategy: + * 1. Install a custom SIGSEGV handler that saves Mono's handler and + * checks whether SIGABRT is still handled (not SIG_DFL). + * 2. Trigger a SIGSEGV from a non-JIT native function. + * 3. Mono chains to our handler (because signal_chaining is enabled). + * 4. Our handler checks SIGABRT disposition and reports the result. + * + * Returns 0 on success, non-zero on failure. + */ +static volatile int g_test_crash_chain_result = -1; +static volatile sig_atomic_t g_test_sigabrt_received = 0; + +__attribute__((noinline)) +static void do_test_crash(void) +{ + volatile int *ptr = 0; + *ptr = 42; +} + +static void +test_sigabrt_handler(int signum) +{ + (void)signum; + g_test_sigabrt_received = 1; +} + +/** + * Pre-Mono SIGSEGV handler installed before mono_jit_init. Mono's + * signal chaining saves this handler and calls it via mono_chain_signal + * when a native crash occurs. + * + * After Mono's crash diagnostics (mono_handle_native_crash) run, this + * handler verifies that SIGABRT was not reset to SIG_DFL. Without the + * fix, mono_handle_native_crash unconditionally resets SIGABRT to + * SIG_DFL, so raise(SIGABRT) kills the process (the test crashes). + * With the fix, SIGABRT retains Mono's handler, and we can catch the + * raised SIGABRT with a temporary handler (the test passes). + */ +static void +test_pre_mono_sigsegv_handler(int signum, siginfo_t *info, void *context) +{ + (void)signum; + (void)info; + + // By the time Mono chains to us, mono_handle_native_crash has + // already run. Check if SIGABRT was reset to SIG_DFL. + struct sigaction current_abrt; + sigaction(SIGABRT, NULL, ¤t_abrt); + + if (current_abrt.sa_handler == SIG_DFL) { + // Bug: SIGABRT is SIG_DFL. Demonstrate the failure by raising + // SIGABRT — just like a FORTIFY abort on another thread would. + // This kills the process (test crashes). + raise(SIGABRT); + _exit(1); // unreachable + } + + // SIGABRT is still handled — the fix works. Install a temporary + // catcher and raise SIGABRT to prove it's actually catchable. + struct sigaction tmp_abrt; + memset(&tmp_abrt, 0, sizeof(tmp_abrt)); + tmp_abrt.sa_handler = test_sigabrt_handler; + sigemptyset(&tmp_abrt.sa_mask); + sigaction(SIGABRT, &tmp_abrt, NULL); + + g_test_sigabrt_received = 0; + raise(SIGABRT); + + sigaction(SIGABRT, ¤t_abrt, NULL); + + g_test_crash_chain_result = g_test_sigabrt_received ? 0 : 1; + + // Skip the faulting instruction so execution can continue. +#if defined(__aarch64__) + ((ucontext_t *)context)->uc_mcontext.pc += 4; +#elif defined(__x86_64__) + ((ucontext_t *)context)->uc_mcontext.gregs[REG_RIP] += 7; +#elif defined(__i386__) + ((ucontext_t *)context)->uc_mcontext.gregs[REG_EIP] += 7; +#elif defined(__arm__) + ((ucontext_t *)context)->uc_mcontext.arm_pc += 4; +#endif +} + +void +test_crash_chaining_install_pre_mono_handler(void) +{ + struct sigaction sa; + memset(&sa, 0, sizeof(sa)); + sa.sa_sigaction = test_pre_mono_sigsegv_handler; + sa.sa_flags = SA_SIGINFO; + sigemptyset(&sa.sa_mask); + sigaction(SIGSEGV, &sa, NULL); +} + +int +test_crash_chaining(void) +{ + g_test_crash_chain_result = -1; + do_test_crash(); + + if (g_test_crash_chain_result == -1) { + LOG_ERROR("test_crash_chaining: handler was not called"); + return 3; + } + + return g_test_crash_chain_result; +} diff --git a/src/tests/FunctionalTests/Android/Device_Emulator/CrashChaining/Android.Device_Emulator.CrashChaining.Test.csproj b/src/tests/FunctionalTests/Android/Device_Emulator/CrashChaining/Android.Device_Emulator.CrashChaining.Test.csproj new file mode 100644 index 00000000000000..d897abbba713a1 --- /dev/null +++ b/src/tests/FunctionalTests/Android/Device_Emulator/CrashChaining/Android.Device_Emulator.CrashChaining.Test.csproj @@ -0,0 +1,18 @@ + + + Exe + false + true + $(NetCoreAppCurrent) + Android.Device_Emulator.CrashChaining.Test.dll + 42 + + + + + + + + + + diff --git a/src/tests/FunctionalTests/Android/Device_Emulator/CrashChaining/Program.cs b/src/tests/FunctionalTests/Android/Device_Emulator/CrashChaining/Program.cs new file mode 100644 index 00000000000000..bbe31e77e45834 --- /dev/null +++ b/src/tests/FunctionalTests/Android/Device_Emulator/CrashChaining/Program.cs @@ -0,0 +1,29 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Runtime.InteropServices; + +/// +/// Test that crash chaining preserves signal handlers during +/// mono_handle_native_crash. When crash_chaining is enabled, +/// mono_handle_native_crash should NOT reset SIGABRT etc. to SIG_DFL, +/// because that would let secondary signals (e.g. FORTIFY aborts on +/// other threads) kill the process before the crash can be chained. +/// +public static class Program +{ + // Returns 0 on success (SIGABRT handler preserved during crash chaining), + // non-zero on failure. + [DllImport("__Internal")] + private static extern int test_crash_chaining(); + + public static int Main() + { + int result = test_crash_chaining(); + Console.WriteLine(result == 0 + ? "PASS: crash chaining preserved signal handlers" + : $"FAIL: crash chaining test returned {result}"); + return result == 0 ? 42 : result; + } +} From 3e7cb0f33b7ec9af07651a61591c0ce25de2dfdc Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Fri, 20 Mar 2026 16:26:39 +0100 Subject: [PATCH 2/4] Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/tasks/AndroidAppBuilder/Templates/monodroid.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/tasks/AndroidAppBuilder/Templates/monodroid.c b/src/tasks/AndroidAppBuilder/Templates/monodroid.c index 664f73009c639b..95e209654e5352 100644 --- a/src/tasks/AndroidAppBuilder/Templates/monodroid.c +++ b/src/tasks/AndroidAppBuilder/Templates/monodroid.c @@ -25,6 +25,7 @@ #include #include #include +#include #include /********* exported symbols *********/ @@ -493,7 +494,7 @@ invoke_external_native_api (void (*callback)(void)) * * Returns 0 on success, non-zero on failure. */ -static volatile int g_test_crash_chain_result = -1; +static volatile sig_atomic_t g_test_crash_chain_result = -1; static volatile sig_atomic_t g_test_sigabrt_received = 0; __attribute__((noinline)) From fcdda4d70c01c446912d0e047643cfffe098efd2 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Fri, 20 Mar 2026 16:19:17 +0100 Subject: [PATCH 3/4] Use siglongjmp instead of manually skipping the faulting instruction --- .../AndroidAppBuilder/Templates/monodroid.c | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/src/tasks/AndroidAppBuilder/Templates/monodroid.c b/src/tasks/AndroidAppBuilder/Templates/monodroid.c index 95e209654e5352..467e65f8417079 100644 --- a/src/tasks/AndroidAppBuilder/Templates/monodroid.c +++ b/src/tasks/AndroidAppBuilder/Templates/monodroid.c @@ -24,6 +24,7 @@ #include #include #include +#include #include #include #include @@ -496,6 +497,7 @@ invoke_external_native_api (void (*callback)(void)) */ static volatile sig_atomic_t g_test_crash_chain_result = -1; static volatile sig_atomic_t g_test_sigabrt_received = 0; +static sigjmp_buf g_test_jmpbuf; __attribute__((noinline)) static void do_test_crash(void) @@ -528,6 +530,7 @@ test_pre_mono_sigsegv_handler(int signum, siginfo_t *info, void *context) { (void)signum; (void)info; + (void)context; // By the time Mono chains to us, mono_handle_native_crash has // already run. Check if SIGABRT was reset to SIG_DFL. @@ -557,16 +560,8 @@ test_pre_mono_sigsegv_handler(int signum, siginfo_t *info, void *context) g_test_crash_chain_result = g_test_sigabrt_received ? 0 : 1; - // Skip the faulting instruction so execution can continue. -#if defined(__aarch64__) - ((ucontext_t *)context)->uc_mcontext.pc += 4; -#elif defined(__x86_64__) - ((ucontext_t *)context)->uc_mcontext.gregs[REG_RIP] += 7; -#elif defined(__i386__) - ((ucontext_t *)context)->uc_mcontext.gregs[REG_EIP] += 7; -#elif defined(__arm__) - ((ucontext_t *)context)->uc_mcontext.arm_pc += 4; -#endif + // Jump back to test_crash_chaining to report the result. + siglongjmp(g_test_jmpbuf, 1); } void @@ -584,7 +579,9 @@ int test_crash_chaining(void) { g_test_crash_chain_result = -1; - do_test_crash(); + if (sigsetjmp(g_test_jmpbuf, 1) == 0) { + do_test_crash(); + } if (g_test_crash_chain_result == -1) { LOG_ERROR("test_crash_chaining: handler was not called"); From faf46615207010cd3397a4a003d87a2880f27a41 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Fri, 20 Mar 2026 17:07:27 +0100 Subject: [PATCH 4/4] Use designated initializer instead of memset in signal handler --- src/tasks/AndroidAppBuilder/Templates/monodroid.c | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/tasks/AndroidAppBuilder/Templates/monodroid.c b/src/tasks/AndroidAppBuilder/Templates/monodroid.c index 467e65f8417079..10dd1c1cfcef0a 100644 --- a/src/tasks/AndroidAppBuilder/Templates/monodroid.c +++ b/src/tasks/AndroidAppBuilder/Templates/monodroid.c @@ -547,10 +547,7 @@ test_pre_mono_sigsegv_handler(int signum, siginfo_t *info, void *context) // SIGABRT is still handled — the fix works. Install a temporary // catcher and raise SIGABRT to prove it's actually catchable. - struct sigaction tmp_abrt; - memset(&tmp_abrt, 0, sizeof(tmp_abrt)); - tmp_abrt.sa_handler = test_sigabrt_handler; - sigemptyset(&tmp_abrt.sa_mask); + struct sigaction tmp_abrt = { .sa_handler = test_sigabrt_handler }; sigaction(SIGABRT, &tmp_abrt, NULL); g_test_sigabrt_received = 0;