SIGSEGV in art::JNI::GetObjectClass (dangling jobject) at fleet scale — race between AOT managed-to-native wrapper's GC-safe transition and handle invalidation

Open
#12,765 11 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Assessment

Difficulty
5/5
Estimated time
Over a week
Newbie friendliness
38/100
Issue type
Bug
Clarity
Mostly clear
Activity status
Active
Tech stack
android, csharp

Research direction

Start by inspecting the shared native_indirect wrappers in libaot-Java.Interop.dll.so, especially the transition around mono_threads_enter_gc_safe_region_unbalanced and the GetObjectClass call. Trace the listed peer-creation callers, then use the proposed debuggable build with CheckJNI, GC stress, and UI soak to validate a candidate fix. Done means the crash is resolved or reproducibly narrowed, with fleet-scale telemetry available for staged validation.

Written by the indexing model from the issue text.

Description

Area: App Runtime
Android framework version

net10.0-android

Affected platform version

.NET 10, Android runtime pack Microsoft.Android.Runtime.Mono.36.android-arm64 36.1.30 (verified by SHA-256 of libmonodroid.so in shipped AAB); crash signature observed since at least May 2026 on earlier versions too. Devices: Samsung/Pixel/others, Android 12-16, arm64-v8a.

Description

Production .NET MAUI app (net10.0-android, Mono runtime, full AOT with RunAOTCompilation=true, assembly store), ~8,000-device fleet. We collect ApplicationExitInfo records (incl. native-crash tombstones). One native crash signature dominates everything else:

signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x0        // sometimes 0x4
Crashed thread: <main thread>                                     // or ".NET TP Worker"
  #00 libart.so +0x69ecf0  art::JNI<false>::GetObjectClass(_JNIEnv*, _jobject*)
  dotnet/android#1 split_config.arm64_v8a.apk +0x59f50                         // earlier app build: +0x58be0

Frequency: ~330 crash events from ~290 distinct devices per week (~4% of the fleet weekly, ~40-60/day in steady state). It occurs on both the UI thread and thread-pool threads, across unrelated screens, elevated under GC pressure (e.g. heavy allocation right after app update: DB migration + full data sync) but persistent during completely normal use.

Frame dotnet/android#1 identified — shared AOT transition wrapper, race window visible

We symbolized frame dotnet/android#1 by recompiling Java.Interop.dll with mono-aot-cross keeping symbols (the Android SDK's AOT step passes -s in ld-flags, so shipped AOT images are always stripped) and byte-matching against the stripped image from our shipped AAB (code regions are byte-identical at the same file offsets between runtime 36.1.30 and 36.1.43):

  • Both offsets are inside Mono's shared managed-to-native native_indirect signature wrappers in libaot-Java.Interop.dll.so (wrapper_managed_to_native ... wrapper_native_indirect_...). +0x59f50 is exactly the wrapper's indirect call instruction (blr x2) for the 2-argument signature matching GetObjectClass(env, jobject) — unwinders report caller frames as LR-4. Because the wrappers are shared per signature, every caller funnels through the same address — hence exactly one offset per app build across the whole fleet.
  • The race window: the wrapper calls mono_threads_enter_gc_safe_region_unbalanced immediately before the indirect JNI call. The raw jobject was extracted from the JniObjectReference before entering GC-safe mode; a collection (and GC-bridge handle processing) running concurrently in that window can invalidate the handle, so ART dereferences a dangling jobject → SIGSEGV with fault addr 0x0/0x4.
  • IL scan of the linked assemblies shows the managed GetObjectClass callers funneling into this wrapper: JniRuntime.JniValueManager.CreatePeer, AndroidTypeManager/JavaTypeManager.CreateInstance (the Java→managed peer-creation path), JniEnvironment.Types.GetJniTypeNameFromInstance, Android.Runtime.JNIEnv.GetObjectClass/InvokeConstructor/AssertCompatibleArrayTypes, JavaConvert.GetTypeMapping.
  • Ruled out on the native side: the exact shipped libmonodroid.so (36.1.30) contains five JNIEnv->GetObjectClass call sites (init + 4 in OSBridge), none matching the crash offsets; libmonosgen-2.0.so and the crypto PAL contain none.

Possibly related: dotnet/runtime#123495 (SIGSEGV inside sgen-tarjan-bridge.c on .NET 10 Android), dotnet/runtime#112109.

Related diagnosability gap

The crashing thread's backtrace always ends at frame dotnet/android#1 because Mono's Android AOT emits no OS-readable unwind info — no .eh_frame/PT_GNU_EH_FRAME in the AOT images, and the LLVM AOT backend likewise generates assembly without any .cfi directives (verified with mono-aot-cross ... llvm). Consequently no Mono-based Android app can symbolize past the first managed frame in ApplicationExitInfo / Play Console / Crashlytics native crash reports. If AOT could emit CFI (even optionally), issues like this one would arrive with full stacks.

We can attach full tombstones (all threads + memory maps) and the exact shipped libmonodroid.so / libaot-*.so binaries on request, and we can measure any candidate fix at fleet scale within days via a staged rollout.

Steps to Reproduce

No deterministic repro — statistical at fleet scale (hundreds of events/week, ~4% of devices weekly). Elevated under GC pressure (first launch after app update: database migration + full sync), but occurs steadily during normal use on both UI and thread-pool threads. We are setting up a local repro rig (debuggable build + CheckJNI + small nursery GC stress + UI soak) and can run experimental builds.

Did you find any workaround?

Not yet. Evaluating MONO_GC_PARAMS=bridge-implementation=old and the latest servicing runtime (36.1.69) as mitigations; happy to report fleet-scale telemetry for either.

Relevant log output

Dominant language
C#
Stars
2.1k
Forks
580
Avg merge
2d 19m
Merged PRs (30d)
228

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from dotnet/android

All issues in dotnet/android

Similar issues

More C# issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.