Skip to content
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
967c4c8
fix: skip SA_NODEFER when CHAIN_AT_START is active
jpnurmi Mar 11, 2026
8f1f592
Update CHANGELOG.md
jpnurmi Mar 11, 2026
e1c7799
Revert "fix: skip SA_NODEFER when CHAIN_AT_START is active"
jpnurmi Mar 11, 2026
c13706d
fix: mask signal during CHAIN_AT_START to prevent re-raise from killi…
jpnurmi Mar 11, 2026
46049f2
Update CHANGELOG.md
jpnurmi Mar 11, 2026
97bd7a8
fix: use raw syscall for sigtimedwait and correct sigset size
jpnurmi Mar 11, 2026
13a98fa
fix: gate raw syscalls to Android, use sigprocmask elsewhere
jpnurmi Mar 12, 2026
b4c3b36
fix: gate signal masking to Android where Mono resets handler and re-…
jpnurmi Mar 12, 2026
0044c23
Update CHANGELOG.md
jpnurmi Mar 12, 2026
ae8b07a
Use sizeof(sigset_t) instead of _NSIG/8 for raw syscalls
jpnurmi Mar 12, 2026
a40ffcd
test: add Android emulator test for dotnet signal handling (#1574)
jpnurmi Mar 13, 2026
137517b
Bump to API 28 to avoid debuggerd deadlocks
jpnurmi Mar 14, 2026
f2e3b6f
Clarify why raw syscall is needed to bypass libsigchain
jpnurmi Mar 14, 2026
176d4af
Revert "Bump to API 28 to avoid debuggerd deadlocks"
jpnurmi Mar 14, 2026
3b3b5e2
fix(inproc): disable CHAIN_AT_START on Android API < 26
jpnurmi Mar 16, 2026
7fcc526
fix(inproc): move CHAIN_AT_START API level check to init
jpnurmi Mar 16, 2026
d811108
try 36
jpnurmi Mar 17, 2026
1a03a8c
Revert "try 36"
jpnurmi Mar 18, 2026
dd7b4f9
ci: switch Android emulator target back to google_apis
jpnurmi Mar 18, 2026
88dcbf0
fix(inproc): use clone(CLONE_VM) for CHAIN_AT_START on Android
jpnurmi Mar 18, 2026
dc83356
Revert "fix(inproc): use clone(CLONE_VM) for CHAIN_AT_START on Android"
jpnurmi Mar 18, 2026
d8e92a1
fix(inproc): use raw SYS_rt_sigaction to restore handler on Android
jpnurmi Mar 18, 2026
682f5a9
fix(tests): replace `pm clear` with force-stop + run-as on Android
jpnurmi Mar 19, 2026
ee61610
fix(tests): filter Android logcat by app PID
jpnurmi Mar 19, 2026
3ba6a33
fix(inproc): use sigaction() instead of raw SYS_rt_sigaction
jpnurmi Mar 19, 2026
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
31 changes: 26 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -191,12 +191,17 @@ jobs:
MINGW_ASM_MASM_COMPILER: llvm-ml
MINGW_ASM_MASM_FLAGS: -m64
- name: Android (API 21, NDK 23)
os: macos-15-large
os: ubuntu-latest
ANDROID_API: 21
ANDROID_NDK: 23.2.8568313
ANDROID_ARCH: x86_64
- name: Android (API 26, NDK 27)
os: ubuntu-latest
ANDROID_API: 26
ANDROID_NDK: 27.3.13750724
ANDROID_ARCH: x86_64
- name: Android (API 31, NDK 27)
os: macos-15-large
os: ubuntu-latest
ANDROID_API: 31
ANDROID_NDK: 27.3.13750724
ANDROID_ARCH: x86_64
Expand Down Expand Up @@ -242,12 +247,12 @@ jobs:
cache: "pip"

- name: Check Linux CC/CXX
if: ${{ runner.os == 'Linux' && !matrix.container }}
if: ${{ runner.os == 'Linux' && !env['ANDROID_API'] &&!matrix.container }}
run: |
[ -n "$CC" ] && [ -n "$CXX" ] || { echo "Ubuntu runner configurations require toolchain selection via CC and CXX" >&2; exit 1; }

- name: Installing Linux Dependencies
if: ${{ runner.os == 'Linux' && !env['TEST_X86'] && !matrix.container }}
if: ${{ runner.os == 'Linux' && !env['TEST_X86'] && !env['ANDROID_API'] && !matrix.container }}
run: |
sudo apt update
# Install common dependencies
Expand Down Expand Up @@ -278,7 +283,7 @@ jobs:
sudo make install

- name: Installing Linux 32-bit Dependencies
if: ${{ runner.os == 'Linux' && env['TEST_X86'] && !matrix.container }}
if: ${{ runner.os == 'Linux' && env['TEST_X86'] && !env['ANDROID_API'] &&!matrix.container }}
run: |
sudo dpkg --add-architecture i386
sudo apt update
Expand Down Expand Up @@ -357,6 +362,22 @@ jobs:
with:
gradle-home-cache-cleanup: true

- name: Setup .NET for Android
if: ${{ env['ANDROID_API'] }}
uses: actions/setup-dotnet@v5
with:
dotnet-version: '10.0.x'

- name: Install .NET Android workload
if: ${{ env['ANDROID_API'] }}
run: dotnet workload restore tests/fixtures/dotnet_signal/test_dotnet.csproj

- name: Enable KVM group perms
if: ${{ runner.os == 'Linux' && env['ANDROID_API'] }}
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm

- name: Add sentry.native.test hostname
if: ${{ runner.os == 'Windows' }}
Expand Down
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
# Changelog

## Unreleased:
## Unreleased

**Fixes**:

- inproc: only the handling thread cleans up after the crash. ([#1579](https://github.com/getsentry/sentry-native/pull/1579))
- 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))
- Add module header pages to MemoryList and fix exception code in the native backend. ([#1576](https://github.com/getsentry/sentry-native/pull/1576))
- 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))

## 0.13.2

Expand Down
56 changes: 56 additions & 0 deletions src/backends/sentry_backend_inproc.c
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@
#ifdef SENTRY_PLATFORM_UNIX
# include <poll.h>
#endif
#ifdef SENTRY_PLATFORM_ANDROID
# include <android/api-level.h>
# include <sys/syscall.h>
#endif
#include <string.h>

/**
Expand Down Expand Up @@ -480,6 +484,17 @@ startup_inproc_backend(
options ? sentry_options_get_handler_strategy(options) :
# endif
SENTRY_HANDLER_STRATEGY_DEFAULT;
# ifdef SENTRY_PLATFORM_ANDROID
// CHAIN_AT_START invokes the previous handler and expects to regain
// control. On Android API < 26, the old debuggerd daemon kills the
// crashing process via SIGKILL after the chained handler triggers
// it, so we fall back to DEFAULT which chains at the end instead.
if (g_backend_config.handler_strategy
== SENTRY_HANDLER_STRATEGY_CHAIN_AT_START
&& android_get_device_api_level() < 26) {
g_backend_config.handler_strategy = SENTRY_HANDLER_STRATEGY_DEFAULT;
}
# endif
if (backend) {
backend->data = &g_backend_config;
}
Expand Down Expand Up @@ -1564,6 +1579,29 @@ process_ucontext(const sentry_ucontext_t *uctx)
uintptr_t ip = get_instruction_pointer(uctx);
uintptr_t sp = get_stack_pointer(uctx);

# ifdef SENTRY_PLATFORM_ANDROID
// Mask the signal so SA_NODEFER doesn't let re-raises from the chained
// handler kill the process before we regain control.
sigset_t mask, old_mask;
sigemptyset(&mask);
sigaddset(&mask, uctx->signum);
// Raw syscall because ART's libsigchain intercepts
// sigprocmask() and silently drops the request when called
// outside its own special handlers. Without the raw syscall
// the mask change would be ignored and SA_NODEFER would let
// the chained handler's raise() re-deliver the signal
// immediately, crashing the process before we can inspect
// the modified IP/SP.
//
// DANGER: this makes libsigchain's internal mask state
// diverge from the kernel's actual mask. If ART ever relies
// on that state for correctness (e.g. GC safepoints), this
// could cause subtle failures. We restore the mask right
// after the chained handler returns, limiting the window.
syscall(
SYS_rt_sigprocmask, SIG_BLOCK, &mask, &old_mask, sizeof(sigset_t));
# endif

// invoke the previous handler (typically the CLR/Mono
// signal-to-managed-exception handler)
invoke_signal_handler(
Expand All @@ -1579,6 +1617,24 @@ process_ucontext(const sentry_ucontext_t *uctx)
return;
}

# ifdef SENTRY_PLATFORM_ANDROID
// Restore our handler via raw syscall to bypass libsigchain.
// resend_signal() sets SIG_DFL via libsigchain, which updates
// libsigchain's internal state but also the kernel disposition.
// A regular sigaction() call goes through libsigchain which may
// not propagate to the kernel if its internal state is stale.
syscall(SYS_rt_sigaction, uctx->signum, &g_sigaction, NULL,
sizeof(sigset_t));

// consume pending signal
struct timespec timeout = { 0, 0 };
syscall(SYS_rt_sigtimedwait, &mask, NULL, &timeout, sizeof(sigset_t));

// unmask
syscall(
SYS_rt_sigprocmask, SIG_SETMASK, &old_mask, NULL, sizeof(sigset_t));
# endif

// return from runtime handler; continue processing the crash on the
// signal thread until the worker takes over
}
Expand Down
2 changes: 2 additions & 0 deletions tests/fixtures/dotnet_signal/Directory.Build.props
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<!-- Prevent MSBuild from using parent Directory.Build.props -->
<Project />
31 changes: 31 additions & 0 deletions tests/fixtures/dotnet_signal/Platforms/Android/MainActivity.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using Android.App;
using Android.OS;

// Required for "adb shell run-as" to access the app's data directory in Release builds
[assembly: Application(Debuggable = true)]

namespace dotnet_signal;

[Activity(Name = "dotnet_signal.MainActivity", MainLauncher = true)]
public class MainActivity : Activity
{
protected override void OnResume()
{
base.OnResume();

var arg = Intent?.GetStringExtra("arg");
if (!string.IsNullOrEmpty(arg))
{
var databasePath = FilesDir?.AbsolutePath + "/.sentry-native";

// Post to the message queue so the activity finishes starting
// before the crash test runs. Without this, "am start -W" may hang.
new Handler(Looper.MainLooper!).Post(() =>
{
Program.RunTest(new[] { arg }, databasePath);
FinishAndRemoveTask();
Java.Lang.JavaSystem.Exit(0);
});
}
}
}
25 changes: 19 additions & 6 deletions tests/fixtures/dotnet_signal/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,13 @@ class Program
[DllImport("sentry", EntryPoint = "sentry_options_set_debug")]
static extern IntPtr sentry_options_set_debug(IntPtr options, int debug);

[DllImport("sentry", EntryPoint = "sentry_options_set_database_path")]
static extern void sentry_options_set_database_path(IntPtr options, string path);

[DllImport("sentry", EntryPoint = "sentry_init")]
static extern int sentry_init(IntPtr options);

static void Main(string[] args)
public static void RunTest(string[] args, string? databasePath = null)
{
var githubActions = Environment.GetEnvironmentVariable("GITHUB_ACTIONS") ?? string.Empty;
if (githubActions == "true") {
Expand All @@ -38,10 +41,13 @@ static void Main(string[] args)
var options = sentry_options_new();
sentry_options_set_handler_strategy(options, 1);
sentry_options_set_debug(options, 1);
if (databasePath != null)
{
sentry_options_set_database_path(options, databasePath);
}
sentry_init(options);

var doNativeCrash = args is ["native-crash"];
if (doNativeCrash)
if (args.Contains("native-crash"))
{
native_crash();
}
Expand All @@ -51,17 +57,24 @@ 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)
{
}
}
else if (args.Contains("unhandled-managed-exception"))
{
Console.WriteLine("dereference a NULL object from managed code (unhandled)");
var s = default(string);
var c = s.Length;
var c = s!.Length;
}
}

#if !ANDROID
static void Main(string[] args)
{
RunTest(args);
}
#endif
}
17 changes: 16 additions & 1 deletion tests/fixtures/dotnet_signal/test_dotnet.csproj
Original file line number Diff line number Diff line change
@@ -1,8 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<TargetFrameworks>net10.0</TargetFrameworks>
<TargetFrameworks Condition="'$(ANDROID_API)' != ''">$(TargetFrameworks);net10.0-android</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<PropertyGroup Condition="$(TargetFramework.Contains('-android'))">
<ApplicationId>io.sentry.ndk.dotnet.signal.test</ApplicationId>
<SupportedOSPlatformVersion>21</SupportedOSPlatformVersion>
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
</PropertyGroup>

<ItemGroup Condition="!$(TargetFramework.Contains('-android'))">
<Compile Remove="Platforms\Android\**" />
</ItemGroup>

<ItemGroup Condition="$(TargetFramework.Contains('-android'))">
<AndroidNativeLibrary Include="native\**\*.so" />
</ItemGroup>
</Project>
4 changes: 2 additions & 2 deletions tests/test_build_static.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import sys
import os
import pytest
from .conditions import has_breakpad, has_crashpad, has_native
from .conditions import has_breakpad, has_crashpad, has_native, is_android


def test_static_lib(cmake):
Expand All @@ -16,7 +16,7 @@ def test_static_lib(cmake):
)

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

Expand Down
Loading
Loading