Skip to content

Commit c1ac26c

Browse files
jpnurmiclaude
andauthored
fix(android): mask signal during CHAIN_AT_START to survive chained Mono handler re-raises (#1572)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 937f5f2 commit c1ac26c

9 files changed

Lines changed: 358 additions & 21 deletions

File tree

.github/workflows/ci.yml

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -191,12 +191,17 @@ jobs:
191191
MINGW_ASM_MASM_COMPILER: llvm-ml
192192
MINGW_ASM_MASM_FLAGS: -m64
193193
- name: Android (API 21, NDK 23)
194-
os: macos-15-large
194+
os: ubuntu-latest
195195
ANDROID_API: 21
196196
ANDROID_NDK: 23.2.8568313
197197
ANDROID_ARCH: x86_64
198+
- name: Android (API 26, NDK 27)
199+
os: ubuntu-latest
200+
ANDROID_API: 26
201+
ANDROID_NDK: 27.3.13750724
202+
ANDROID_ARCH: x86_64
198203
- name: Android (API 31, NDK 27)
199-
os: macos-15-large
204+
os: ubuntu-latest
200205
ANDROID_API: 31
201206
ANDROID_NDK: 27.3.13750724
202207
ANDROID_ARCH: x86_64
@@ -242,12 +247,12 @@ jobs:
242247
cache: "pip"
243248

244249
- name: Check Linux CC/CXX
245-
if: ${{ runner.os == 'Linux' && !matrix.container }}
250+
if: ${{ runner.os == 'Linux' && !env['ANDROID_API'] &&!matrix.container }}
246251
run: |
247252
[ -n "$CC" ] && [ -n "$CXX" ] || { echo "Ubuntu runner configurations require toolchain selection via CC and CXX" >&2; exit 1; }
248253
249254
- name: Installing Linux Dependencies
250-
if: ${{ runner.os == 'Linux' && !env['TEST_X86'] && !matrix.container }}
255+
if: ${{ runner.os == 'Linux' && !env['TEST_X86'] && !env['ANDROID_API'] && !matrix.container }}
251256
run: |
252257
sudo apt update
253258
# Install common dependencies
@@ -278,7 +283,7 @@ jobs:
278283
sudo make install
279284
280285
- name: Installing Linux 32-bit Dependencies
281-
if: ${{ runner.os == 'Linux' && env['TEST_X86'] && !matrix.container }}
286+
if: ${{ runner.os == 'Linux' && env['TEST_X86'] && !env['ANDROID_API'] &&!matrix.container }}
282287
run: |
283288
sudo dpkg --add-architecture i386
284289
sudo apt update
@@ -357,6 +362,22 @@ jobs:
357362
with:
358363
gradle-home-cache-cleanup: true
359364

365+
- name: Setup .NET for Android
366+
if: ${{ env['ANDROID_API'] }}
367+
uses: actions/setup-dotnet@v5
368+
with:
369+
dotnet-version: '10.0.x'
370+
371+
- name: Install .NET Android workload
372+
if: ${{ env['ANDROID_API'] }}
373+
run: dotnet workload restore tests/fixtures/dotnet_signal/test_dotnet.csproj
374+
375+
- name: Enable KVM group perms
376+
if: ${{ runner.os == 'Linux' && env['ANDROID_API'] }}
377+
run: |
378+
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
379+
sudo udevadm control --reload-rules
380+
sudo udevadm trigger --name-match=kvm
360381
361382
- name: Add sentry.native.test hostname
362383
if: ${{ runner.os == 'Windows' }}

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Changelog
22

3-
## Unreleased:
3+
## Unreleased
44

55
**Features**:
66

@@ -11,6 +11,7 @@
1111
- inproc: only the handling thread cleans up after the crash. ([#1579](https://github.com/getsentry/sentry-native/pull/1579))
1212
- Propagate transport options (`ca_certs`, `proxy`, `user_agent`) and `handler_path` to the native backend crash daemon. Previously, the daemon did not receive SSL certificate or proxy settings from the parent process, causing SSL errors (curl code 60) when uploading crash reports. The daemon also ignored the user-configured handler path, requiring the `sentry-crash` binary to be placed next to the application executable. ([#1573](https://github.com/getsentry/sentry-native/pull/1573))
1313
- Add module header pages to MemoryList and fix exception code in the native backend. ([#1576](https://github.com/getsentry/sentry-native/pull/1576))
14+
- Fix `CHAIN_AT_START` handler strategy crashing on Android when the chained Mono handler resets the signal handler and re-raises. ([#1572](https://github.com/getsentry/sentry-native/pull/1572))
1415

1516
## 0.13.2
1617

src/backends/sentry_backend_inproc.c

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@
2626
#ifdef SENTRY_PLATFORM_UNIX
2727
# include <poll.h>
2828
#endif
29+
#ifdef SENTRY_PLATFORM_ANDROID
30+
# include <android/api-level.h>
31+
# include <sys/syscall.h>
32+
#endif
2933
#include <string.h>
3034

3135
/**
@@ -480,6 +484,17 @@ startup_inproc_backend(
480484
options ? sentry_options_get_handler_strategy(options) :
481485
# endif
482486
SENTRY_HANDLER_STRATEGY_DEFAULT;
487+
# ifdef SENTRY_PLATFORM_ANDROID
488+
// CHAIN_AT_START invokes the previous handler and expects to regain
489+
// control. On Android API < 26, the old debuggerd daemon kills the
490+
// crashing process via SIGKILL after the chained handler triggers
491+
// it, so we fall back to DEFAULT which chains at the end instead.
492+
if (g_backend_config.handler_strategy
493+
== SENTRY_HANDLER_STRATEGY_CHAIN_AT_START
494+
&& android_get_device_api_level() < 26) {
495+
g_backend_config.handler_strategy = SENTRY_HANDLER_STRATEGY_DEFAULT;
496+
}
497+
# endif
483498
if (backend) {
484499
backend->data = &g_backend_config;
485500
}
@@ -1564,6 +1579,29 @@ process_ucontext(const sentry_ucontext_t *uctx)
15641579
uintptr_t ip = get_instruction_pointer(uctx);
15651580
uintptr_t sp = get_stack_pointer(uctx);
15661581

1582+
# ifdef SENTRY_PLATFORM_ANDROID
1583+
// Mask the signal so SA_NODEFER doesn't let re-raises from the chained
1584+
// handler kill the process before we regain control.
1585+
sigset_t mask, old_mask;
1586+
sigemptyset(&mask);
1587+
sigaddset(&mask, uctx->signum);
1588+
// Raw syscall because ART's libsigchain intercepts
1589+
// sigprocmask() and silently drops the request when called
1590+
// outside its own special handlers. Without the raw syscall
1591+
// the mask change would be ignored and SA_NODEFER would let
1592+
// the chained handler's raise() re-deliver the signal
1593+
// immediately, crashing the process before we can inspect
1594+
// the modified IP/SP.
1595+
//
1596+
// DANGER: this makes libsigchain's internal mask state
1597+
// diverge from the kernel's actual mask. If ART ever relies
1598+
// on that state for correctness (e.g. GC safepoints), this
1599+
// could cause subtle failures. We restore the mask right
1600+
// after the chained handler returns, limiting the window.
1601+
syscall(
1602+
SYS_rt_sigprocmask, SIG_BLOCK, &mask, &old_mask, sizeof(sigset_t));
1603+
# endif
1604+
15671605
// invoke the previous handler (typically the CLR/Mono
15681606
// signal-to-managed-exception handler)
15691607
invoke_signal_handler(
@@ -1579,6 +1617,19 @@ process_ucontext(const sentry_ucontext_t *uctx)
15791617
return;
15801618
}
15811619

1620+
# ifdef SENTRY_PLATFORM_ANDROID
1621+
// restore our handler after resend_signal() set SIG_DFL
1622+
sigaction(uctx->signum, &g_sigaction, NULL);
1623+
1624+
// consume pending signal
1625+
struct timespec timeout = { 0, 0 };
1626+
syscall(SYS_rt_sigtimedwait, &mask, NULL, &timeout, sizeof(sigset_t));
1627+
1628+
// unmask
1629+
syscall(
1630+
SYS_rt_sigprocmask, SIG_SETMASK, &old_mask, NULL, sizeof(sigset_t));
1631+
# endif
1632+
15821633
// return from runtime handler; continue processing the crash on the
15831634
// signal thread until the worker takes over
15841635
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
<!-- Prevent MSBuild from using parent Directory.Build.props -->
2+
<Project />
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
using Android.App;
2+
using Android.OS;
3+
4+
// Required for "adb shell run-as" to access the app's data directory in Release builds
5+
[assembly: Application(Debuggable = true)]
6+
7+
namespace dotnet_signal;
8+
9+
[Activity(Name = "dotnet_signal.MainActivity", MainLauncher = true)]
10+
public class MainActivity : Activity
11+
{
12+
protected override void OnResume()
13+
{
14+
base.OnResume();
15+
16+
var arg = Intent?.GetStringExtra("arg");
17+
if (!string.IsNullOrEmpty(arg))
18+
{
19+
var databasePath = FilesDir?.AbsolutePath + "/.sentry-native";
20+
21+
// Post to the message queue so the activity finishes starting
22+
// before the crash test runs. Without this, "am start -W" may hang.
23+
new Handler(Looper.MainLooper!).Post(() =>
24+
{
25+
Program.RunTest(new[] { arg }, databasePath);
26+
FinishAndRemoveTask();
27+
Java.Lang.JavaSystem.Exit(0);
28+
});
29+
}
30+
}
31+
}

tests/fixtures/dotnet_signal/Program.cs

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,13 @@ class Program
2020
[DllImport("sentry", EntryPoint = "sentry_options_set_debug")]
2121
static extern IntPtr sentry_options_set_debug(IntPtr options, int debug);
2222

23+
[DllImport("sentry", EntryPoint = "sentry_options_set_database_path")]
24+
static extern void sentry_options_set_database_path(IntPtr options, string path);
25+
2326
[DllImport("sentry", EntryPoint = "sentry_init")]
2427
static extern int sentry_init(IntPtr options);
2528

26-
static void Main(string[] args)
29+
public static void RunTest(string[] args, string? databasePath = null)
2730
{
2831
var githubActions = Environment.GetEnvironmentVariable("GITHUB_ACTIONS") ?? string.Empty;
2932
if (githubActions == "true") {
@@ -38,10 +41,13 @@ static void Main(string[] args)
3841
var options = sentry_options_new();
3942
sentry_options_set_handler_strategy(options, 1);
4043
sentry_options_set_debug(options, 1);
44+
if (databasePath != null)
45+
{
46+
sentry_options_set_database_path(options, databasePath);
47+
}
4148
sentry_init(options);
4249

43-
var doNativeCrash = args is ["native-crash"];
44-
if (doNativeCrash)
50+
if (args.Contains("native-crash"))
4551
{
4652
native_crash();
4753
}
@@ -51,17 +57,24 @@ static void Main(string[] args)
5157
{
5258
Console.WriteLine("dereference a NULL object from managed code");
5359
var s = default(string);
54-
var c = s.Length;
60+
var c = s!.Length;
5561
}
56-
catch (NullReferenceException exception)
62+
catch (NullReferenceException)
5763
{
5864
}
5965
}
6066
else if (args.Contains("unhandled-managed-exception"))
6167
{
6268
Console.WriteLine("dereference a NULL object from managed code (unhandled)");
6369
var s = default(string);
64-
var c = s.Length;
70+
var c = s!.Length;
6571
}
6672
}
73+
74+
#if !ANDROID
75+
static void Main(string[] args)
76+
{
77+
RunTest(args);
78+
}
79+
#endif
6780
}
Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,23 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22
<PropertyGroup>
33
<OutputType>Exe</OutputType>
4-
<TargetFramework>net10.0</TargetFramework>
4+
<TargetFrameworks>net10.0</TargetFrameworks>
5+
<TargetFrameworks Condition="'$(ANDROID_API)' != ''">$(TargetFrameworks);net10.0-android</TargetFrameworks>
56
<ImplicitUsings>enable</ImplicitUsings>
67
<Nullable>enable</Nullable>
78
</PropertyGroup>
9+
10+
<PropertyGroup Condition="$(TargetFramework.Contains('-android'))">
11+
<ApplicationId>io.sentry.ndk.dotnet.signal.test</ApplicationId>
12+
<SupportedOSPlatformVersion>21</SupportedOSPlatformVersion>
13+
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
14+
</PropertyGroup>
15+
16+
<ItemGroup Condition="!$(TargetFramework.Contains('-android'))">
17+
<Compile Remove="Platforms\Android\**" />
18+
</ItemGroup>
19+
20+
<ItemGroup Condition="$(TargetFramework.Contains('-android'))">
21+
<AndroidNativeLibrary Include="native\**\*.so" />
22+
</ItemGroup>
823
</Project>

tests/test_build_static.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import sys
33
import os
44
import pytest
5-
from .conditions import has_breakpad, has_crashpad, has_native
5+
from .conditions import has_breakpad, has_crashpad, has_native, is_android
66

77

88
def test_static_lib(cmake):
@@ -16,7 +16,7 @@ def test_static_lib(cmake):
1616
)
1717

1818
# on linux we can use `ldd` to check that we don’t link to `libsentry.so`
19-
if sys.platform == "linux":
19+
if sys.platform == "linux" and not is_android:
2020
output = subprocess.check_output("ldd sentry_example", cwd=tmp_path, shell=True)
2121
assert b"libsentry.so" not in output
2222

0 commit comments

Comments
 (0)