Skip to content

JNI beforeSend blocking callback can crash due to incorrect isolate ownership check #3601

@buenaflor

Description

@buenaflor

Problem

Sentry Flutter registers the beforeSend callback as a JNI → Dart blocking callback proxy. When the Android native side calls captureEvent, it invokes this callback which re-enters the Dart isolate synchronously.

The underlying issue is in package:jni itself: the blocking callback mechanism uses a simple isOnMainThread() check to decide whether it can enter the target isolate, but this isn't strict enough. The assumptions are violated in app close/reopen flows — the target isolate may already be entered by another thread, causing an assertion failure and crash.

Root Cause (in package:jni)

From dart-lang/native#3250:

The only EnterIsolate call is for blocking callbacks where mayEnterIsolate is true and the invoking thread is not entered into an isolate. mayEnterIsolate is supposed to be true only if the target isolate is owned by the main thread, and the invoking thread is the main thread. But the assertion failure says the target isolate is already entered by another thread.

Looking at how jni is doing the ownership check, it predates the isolate ownership API. package:objective_c uses the isolate ownership API, but package:jni uses a simple main thread check. So the simple main thread check isn't strict enough, and its assumptions are violated in the close/reopen flow.

The fix in package:jni migrates blocking callbacks to the proper isolate ownership API (same approach as package:objective_c).

Impact on Sentry Flutter

Our beforeSend callback registered via JNI is affected by this. Any native Android event capture that triggers the blocking callback during or after an isolate ownership change (e.g., app backgrounding/foregrounding) can crash.

Upstream Fix

Mitigation

We cannot bump jni on the main branch yet, so the mitigation is to move the beforeSend callback logic entirely to Kotlin, avoiding the JNI → Dart cross-boundary call:

// Currently (creates JNI -> Dart callback proxy, vulnerable to isolate crash):
beforeSend.use((cb) {
  androidOptions.setBeforeSend(cb);
});

// Mitigation (calls Kotlin callback, no Dart isolate entry needed):
final nativeBeforeSend = native.SentryFlutterPlugin.createBeforeSendCallback();
androidOptions.setBeforeSend(nativeBeforeSend);

By keeping the callback on the native Kotlin side, we avoid the JNI → Dart isolate re-entry entirely, sidestepping the bug until the upstream fix can be adopted.

Context

  • Related: DART-221 (allowing Flutter beforeSend to apply to native events)
  • Related: DART-274 (JNI 0.15.0 bump)

Metadata

Metadata

Assignees

No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions