Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

- Add WinGDK (Gaming.Desktop.x64) platform support ([#1631](https://github.com/getsentry/sentry-native/pull/1631))
- Track discarded events via client reports. ([#1549](https://github.com/getsentry/sentry-native/pull/1549))
- Android: allow Sentry.NET to preload the NDK integration to install signal handlers before the .NET runtime. ([#1613](https://github.com/getsentry/sentry-native/pull/1613))

## 0.13.5

Expand Down
5 changes: 4 additions & 1 deletion include/sentry.h
Original file line number Diff line number Diff line change
Expand Up @@ -1869,7 +1869,10 @@ SENTRY_API int sentry_flush(uint64_t timeout);
*
* Note that this does not uninstall any crash handler installed by our
* backends, which will still process crashes after `sentry_close()`, except
* when using `crashpad` on Linux or the `inproc` backend.
* when using `crashpad` on Linux or the `inproc` backend. The Android
* preload mode of `inproc` is a special case: a lightweight signal-chain
* placeholder may remain installed across `sentry_close()` to preserve
* ordering relative to the managed runtime until a later re-init.
*
* Further note that this function will block the thread it was called from
* until the sentry background worker has finished its work, or it timed out,
Expand Down
11 changes: 11 additions & 0 deletions ndk/lib/api/sentry-native-ndk.api
Original file line number Diff line number Diff line change
Expand Up @@ -104,5 +104,16 @@ public final class io/sentry/ndk/SentryNdk {
public static fun close ()V
public static fun init (Lio/sentry/ndk/NdkOptions;)V
public static fun loadNativeLibraries ()V
public static fun preload ()V
}

public final class io/sentry/ndk/SentryNdkPreloadProvider : android/content/ContentProvider {
public fun <init> ()V
public fun delete (Landroid/net/Uri;Ljava/lang/String;[Ljava/lang/String;)I
public fun getType (Landroid/net/Uri;)Ljava/lang/String;
public fun insert (Landroid/net/Uri;Landroid/content/ContentValues;)Landroid/net/Uri;
public fun onCreate ()Z
public fun query (Landroid/net/Uri;[Ljava/lang/String;Ljava/lang/String;[Ljava/lang/String;Ljava/lang/String;)Landroid/database/Cursor;
public fun update (Landroid/net/Uri;Landroid/content/ContentValues;Ljava/lang/String;[Ljava/lang/String;)I
}

15 changes: 15 additions & 0 deletions ndk/lib/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<!-- Must run before the managed runtime provider emitted by dotnet/android
so that sentry-native is already in the signal chain when the runtime
installs its own handlers.
initOrder must stay higher than AppInitOrder-1 (1999999999) used by the .NET runtime:
https://github.com/dotnet/android/blob/3c50ff4573b5/src/Xamarin.Android.Build.Tasks/Utilities/ManifestDocument.cs#L724 -->
<provider
android:name="io.sentry.ndk.SentryNdkPreloadProvider"
android:authorities="${applicationId}.SentryNdkPreloadProvider"
android:initOrder="2000000000"
android:exported="false" />
</application>
</manifest>
20 changes: 20 additions & 0 deletions ndk/lib/src/main/java/io/sentry/ndk/SentryNdk.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ public final class SentryNdk {

private SentryNdk() {}

private static native void preloadSentryNative();

/**
* Initializes sentry-native and returns 0 on success, non-zero on failure.
*
Expand All @@ -20,6 +22,24 @@ private SentryNdk() {}

private static native void shutdown();

/**
* Preloads sentry-native into the process signal chain before full
* initialization.
*
* <p>Intended for downstream SDK/runtime integrations on Android. This does
* not initialize sentry-native, configure a database path, or start the
* inproc handler thread; it only establishes signal-handler ordering ahead of
* the managed runtime.
*
* <p>This is intended to be used by downstream integrations that gate the
* preload flow to supported runtimes and then call {@link #init(NdkOptions)}
* with the normal DEFAULT handler strategy.
*/
public static void preload() {
loadNativeLibraries();
preloadSentryNative();
}

/**
* Init the NDK integration
*
Expand Down
88 changes: 88 additions & 0 deletions ndk/lib/src/main/java/io/sentry/ndk/SentryNdkPreloadProvider.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package io.sentry.ndk;

import android.content.ContentProvider;
import android.content.ContentValues;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

/**
* Preloads the NDK integration before the .NET runtime provider on Android.
*
* <p>This is intended for downstream SDK integrations that run with CoreCLR on
* Android. By installing sentry-native before the managed runtime registers
* its own signal handlers, native crash signals can chain from the runtime
* back to the Native SDK, while runtime-generated fault signals can still be
* consumed by the runtime for managed exception handling.
*
* <p>This is the preload alternative to CHAIN_AT_START. Mono on Android
* continues to use CHAIN_AT_START.
*
* <p>Enabled by setting {@code io.sentry.ndk.preload} to {@code true} in the
* app manifest metadata. The high {@code initOrder} ensures this runs before
* the runtime provider emitted by dotnet/android.
*/
public final class SentryNdkPreloadProvider extends ContentProvider {

@Override
public boolean onCreate() {
final Context context = getContext();
if (context == null) {
return false;
}
try {
final ApplicationInfo info =
context
.getPackageManager()
.getApplicationInfo(context.getPackageName(), PackageManager.GET_META_DATA);
final Bundle metadata = info.metaData;
if (metadata != null && metadata.getBoolean("io.sentry.ndk.preload", false)) {
android.util.Log.d("sentry", "io.sentry.ndk.preload read: true");
SentryNdk.preload();
android.util.Log.d("sentry", "SentryNdk.preload() completed");
}
} catch (Throwable e) {
android.util.Log.e("sentry", "SentryNdk.preload() failed", e);
}
return true;
}

@Override
public @Nullable Cursor query(
@NotNull Uri uri,
@Nullable String[] projection,
@Nullable String selection,
@Nullable String[] selectionArgs,
@Nullable String sortOrder) {
return null;
}

@Override
public @Nullable String getType(@NotNull Uri uri) {
return null;
}

@Override
public @Nullable Uri insert(@NotNull Uri uri, @Nullable ContentValues values) {
return null;
}

@Override
public int delete(@NotNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) {
return 0;
}

@Override
public int update(
@NotNull Uri uri,
@Nullable ContentValues values,
@Nullable String selection,
@Nullable String[] selectionArgs) {
return 0;
}
}
9 changes: 9 additions & 0 deletions ndk/lib/src/main/jni/sentry.c
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,15 @@ static void send_envelope(sentry_envelope_t *envelope, void *data) {
sentry_envelope_free(envelope);
}

// sentry_backend.h
extern void sentry__backend_preload(void);

JNIEXPORT void JNICALL
Java_io_sentry_ndk_SentryNdk_preloadSentryNative(JNIEnv *env, jclass cls)
{
sentry__backend_preload();
}

JNIEXPORT jint JNICALL
Java_io_sentry_ndk_SentryNdk_initSentryNative(
JNIEnv *env,
Expand Down
5 changes: 5 additions & 0 deletions src/backends/sentry_backend_breakpad.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,11 @@ breakpad_backend_except(

extern "C" {

void
sentry__backend_preload(void)
{
}

sentry_backend_t *
sentry__backend_new(void)
{
Expand Down
5 changes: 5 additions & 0 deletions src/backends/sentry_backend_crashpad.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1029,6 +1029,11 @@ crashpad_backend_remove_attachment(
}
#endif

void
sentry__backend_preload(void)
{
}

sentry_backend_t *
sentry__backend_new(void)
{
Expand Down
117 changes: 91 additions & 26 deletions src/backends/sentry_backend_inproc.c
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,14 @@ static volatile long g_handler_has_work = 0;
#define CRASH_STATE_DONE 2
static volatile long g_crash_handling_state = CRASH_STATE_IDLE;

// Set once handlers were installed via sentry__backend_preload(). In this
// mode sentry may keep its chain position across sentry_close() until either
// full init reuses it, or a pre-init/post-close signal falls through, and
// consumes the preload state.
#ifdef SENTRY_PLATFORM_UNIX
static volatile long g_preloaded = 0;
#endif

// trigger/schedule primitives that block the other side until this side is done
#ifdef SENTRY_PLATFORM_UNIX
static int g_handler_pipe[2] = { -1, -1 };
Expand Down Expand Up @@ -468,6 +476,37 @@ invoke_signal_handler(int signum, siginfo_t *info, void *user_context)
}
}

static int
install_signal_handlers(void)
{
if (sentry__atomic_fetch(&g_preloaded)) {
return 0;
}

memset(g_previous_handlers, 0, sizeof(g_previous_handlers));
for (size_t i = 0; i < SIGNAL_COUNT; ++i) {
if (sigaction(
SIGNAL_DEFINITIONS[i].signum, NULL, &g_previous_handlers[i])
== -1) {
return 1;
}
}

setup_sigaltstack(&g_signal_stack, "init");

sigemptyset(&g_sigaction.sa_mask);
g_sigaction.sa_sigaction = handle_signal;
// SA_NODEFER allows the signal to be delivered while the handler is
// 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;
for (size_t i = 0; i < SIGNAL_COUNT; ++i) {
sigaction(SIGNAL_DEFINITIONS[i].signum, &g_sigaction, NULL);
}
return 0;
}

static int
startup_inproc_backend(
sentry_backend_t *backend, const sentry_options_t *options)
Expand Down Expand Up @@ -503,39 +542,27 @@ startup_inproc_backend(
return 1;
}

// save the old signal handlers
memset(g_previous_handlers, 0, sizeof(g_previous_handlers));
for (size_t i = 0; i < SIGNAL_COUNT; ++i) {
if (sigaction(
SIGNAL_DEFINITIONS[i].signum, NULL, &g_previous_handlers[i])
== -1) {
return 1;
}
}

setup_sigaltstack(&g_signal_stack, "init");

// install our own signal handler
sigemptyset(&g_sigaction.sa_mask);
g_sigaction.sa_sigaction = handle_signal;
// SA_NODEFER allows the signal to be delivered while the handler is
// 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;
for (size_t i = 0; i < SIGNAL_COUNT; ++i) {
sigaction(SIGNAL_DEFINITIONS[i].signum, &g_sigaction, NULL);
}
return 0;
return install_signal_handlers();
}

static void
shutdown_inproc_backend(sentry_backend_t *backend)
{
stop_handler_thread();

teardown_sigaltstack(&g_signal_stack);
reset_signal_handlers();
// In Android preload mode, we intentionally keep our signal handlers
// installed across shutdown. The handler thread is torn down, but our
// position in the signal chain must remain stable so that a later
// sentry_init() can reactivate full crash handling without losing the
// ordering relative to the managed runtime.
//
// While in this state, full inproc crash processing is inactive and any
// signal we still observe will fall through to the previously installed
// handler.
if (!sentry__atomic_fetch(&g_preloaded)) {
teardown_sigaltstack(&g_signal_stack);
reset_signal_handlers();
}

if (backend) {
backend->data = NULL;
Expand Down Expand Up @@ -1643,6 +1670,34 @@ process_ucontext(const sentry_ucontext_t *uctx)
"multiple recursive crashes detected, bailing out");
goto cleanup;
}

// If we were only preloaded (before sentry_init()) or were closed after
// preload (handlers still installed, but no handler thread), do not attempt
// full crash capture here. In this state, preload only serves as a
// placeholder in the signal chain.
//
// We therefore remove our placeholder from the chain and forward the
// signal to the previously installed handler set.
//
// This path is expected to be terminal for real crash signals. If it
// returns, preload mode is consumed for the remainder of the process
// lifetime until a later sentry_init() reinstalls handlers from the
// then-current chain.
//
// This also covers the window after preload but before the managed runtime
// has installed its own handlers: a signal seen in that window is forwarded
// to the pre-preload handler set rather than being captured by Sentry.
if (sentry__atomic_fetch(&g_preloaded)
&& !sentry__atomic_fetch(&g_handler_thread_ready)) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
&& !sentry__atomic_fetch(&g_handler_thread_ready)) {
// If we were only preloaded (before sentry_init()) or were closed after
// preload (handlers still installed, but no handler thread), do not attempt
// full crash capture here. In this state, preload only serves as a
// placeholder in the signal chain.
//
// We therefore remove our placeholder from the chain and forward the
// signal to the previously installed handler set.
//
// This path is expected to be terminal for real crash signals. If it
// returns, preload mode is consumed for the remainder of the process
// lifetime until a later sentry_init() reinstalls handlers from the
// then-current chain.
//
// This also covers the window after preload but before the managed runtime
// has installed its own handlers: a signal seen in that window is forwarded
// to the pre-preload handler set rather than being captured by Sentry.
if (sentry__atomic_fetch(&g_preloaded)

SENTRY_SIGNAL_SAFE_LOG(
"handler thread not ready, falling through to previous handler");
reset_signal_handlers();
sentry__atomic_store(&g_preloaded, 0);
sentry__leave_signal_handler();
invoke_signal_handler(
uctx->signum, uctx->siginfo, (void *)uctx->user_context);
return;
}
#endif

if (!g_backend_config.enable_logging_when_crashed) {
Expand Down Expand Up @@ -1742,6 +1797,16 @@ handle_except(sentry_backend_t *UNUSED(backend), const sentry_ucontext_t *uctx)
process_ucontext(uctx);
}

void
sentry__backend_preload(void)
{
#ifdef SENTRY_PLATFORM_UNIX
if (install_signal_handlers() == 0) {
sentry__atomic_store(&g_preloaded, 1);
}
#endif
}

sentry_backend_t *
sentry__backend_new(void)
{
Expand Down
5 changes: 5 additions & 0 deletions src/backends/sentry_backend_native.c
Original file line number Diff line number Diff line change
Expand Up @@ -919,6 +919,11 @@ native_backend_except(sentry_backend_t *backend, const sentry_ucontext_t *uctx)
}
}

void
sentry__backend_preload(void)
{
}

/**
* Create native backend
*/
Expand Down
5 changes: 5 additions & 0 deletions src/backends/sentry_backend_none.c
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
#include "sentry_backend.h"

void
sentry__backend_preload(void)
{
}

sentry_backend_t *
sentry__backend_new(void)
{
Expand Down
Loading
Loading