Summary
Since the UE 5.8 Android JNI rework (UE::Jni namespace), FScopedJavaObject’s destructor no longer uses the JNIEnv* it was constructed with; it deletes the local ref through the new inline thread_local const UE::Jni::FEnv Env resolved at destruction time (Runtime/Core/Public/Android/AndroidJavaEnv.h:1170, engine 5.8).
When that destructor is inlined into a plugin (non-engine) module’s translation unit in a packaged monolithic Android build, the thread-local access yields an invalid JNIEnv* and the process crashes with SIGSEGV inside the inline _JNIEnv::DeleteLocalRef wrapper (jni.h:554) — i.e. while dereferencing the env’s function table, on the game thread, with a perfectly valid local ref.
The same thread-local read is fine when it happens inside engine-compiled code: AndroidJavaEnv::GetJavaEnv() (exported from Core, returns the same UE::Jni::Env) returns a valid env when called from the very same plugin functions, and all JNI made with that env — including explicit DeleteLocalRef of the same references — succeeds.
What Type of Bug are you experiencing?
Platform Mobile
Steps to Reproduce
- In any plugin runtime module, on the game thread of a packaged Android build, run code that creates and destroys an
FScopedJavaObject— e.g.:JNIEnv* Env = AndroidJavaEnv::GetJavaEnv(); // Env is valid; direct JNI calls with it succeed { auto JavaString = FJavaHelper::ToJavaString(Env, TEXT("hello")); } // <-- destructor: UE::Jni::Env->DeleteLocalRef(...) --> SIGSEGV - Package for Android, deploy, launch on device.
Observed with the Sentry plugin (sentry-unreal 1.16.1), which crashed on its first FScopedJavaObject destruction during subsystem init, and — after that call site was patched to explicit ref management — on the next FScopedJavaObject destruction in plugin code. See stack traces below.
Expected Result
The destructor deletes the local reference with a valid env for the current (attached) thread, as in 5.7 and earlier where FScopedJavaObject stored and reused the JNIEnv* it was created with.
Observed Result
_JNIEnv::DeleteLocalRef jni.h:554
FScopedJavaObject<T>::~FScopedJavaObject AndroidJavaEnv.h:1170
FAndroidSentrySubsystem::InitWithSettings AndroidSentrySubsystem.cpp:116 (plugin code)
USentrySubsystem::Initialize SentrySubsystem.cpp
FSubsystemCollectionBase::Initialize
UEngine::Init / FEngineLoop::Init / AndroidMain
and, after removing that call site, the next scoped destruction in plugin code:
_JNIEnv::DeleteLocalRef jni.h:554
FScopedJavaObject<T>::~FScopedJavaObject AndroidJavaEnv.h:1170
FSentryJavaObjectWrapper::FSentryJavaObjectWrapper AndroidSentryJavaObjectWrapper.cpp:30 (plugin code)
...
UEngine::Init / FEngineLoop::Init / AndroidMain
Affects Versions
5.8
Platform(s)
Android
For crash reports, include your callstack
SIGSEGV: Segfault
libUnreal 0x7bec7d4acc _JNIEnv::DeleteLocalRef (jni.h:554)
libUnreal 0x7bec7d4acc FScopedJavaObject::~FScopedJavaObject (AndroidJavaEnv.h:1170)
libUnreal 0x7bec7d4acc FSentryJavaObjectWrapper::FSentryJavaObjectWrapper (AndroidSentryJavaObjectWrapper.cpp:30)
libUnreal 0x7bec7d9830 FAndroidSentryConverters::VariantMapToNative (AndroidSentryConverters.cpp:118)
libUnreal 0x7bec8197c8 FAndroidSentrySubsystem::SetContext (AndroidSentrySubsystem.cpp:399)
libUnreal 0x7bec7ed4f8 USentrySubsystem::AddDefaultContext (SentrySubsystem.cpp:950)
libUnreal 0x7bec7c82e4 USentrySubsystem::Initialize (SentrySubsystem.cpp:152)
libUnreal 0x7bec7ec46c USentrySubsystem::Initialize (SentrySubsystem.cpp:62)
libUnreal 0x7be595ad90 FSubsystemCollectionBase::AddAndInitializeValidatedSubsystem
libUnreal 0x7be595dd6c FSubsystemCollectionBase::AddAndInitializeSubsystems
libUnreal 0x7be595c6b4 FSubsystemCollectionBase::Initialize
libUnreal 0x7be5c510cc UEngine::Init
libUnreal 0x7be4de2240 UGameEngine::Init
libUnreal 0x7be851b154 FEngineLoop::Init
libUnreal 0x7be8519f84 AndroidMain
libUnreal 0x7be85208d8 android_main
libUnreal 0x7be853c258 android_app_entry
libc 0x7d1a7c20ac null
libc 0x7d1a759f70 null
Additional Notes
Evidence that isolates the thread-local access
All of the following ran on the same game thread, in the same session, in plugin-compiled code:
| Operation | Result |
|---|---|
AndroidJavaEnv::GetJavaEnv() (exported Core function) |
valid env |
NewStringUTF, NewObjectV, NewGlobalRef, CallStaticVoidMethodV, … via that env |
all succeed |
explicit Env->DeleteLocalRef(ref) via that env, incl. env re-queried from GJavaVM->GetEnv (same pointer before/after an intervening Java call) |
succeeds |
inline FScopedJavaObject destructor (UE::Jni::Env->DeleteLocalRef(...)) |
SIGSEGV, 100% reproducible |
The crash reproduces identically regardless of when the code runs (early engine init or deferred via ticker), ruling out initialization-order effects on the JavaVM/activity. We did not capture a register-level tombstone, so we cannot say whether the TLS lookup resolves to a wrong address or to uninitialized storage — but the access-path split above is fully reproducible.
Workaround
Avoid instantiating FScopedJavaObject (directly, or via FJavaHelper::ToJavaString / NewScopedJavaObject) in non-engine modules; use a module-local RAII type that stores the JNIEnv* obtained from AndroidJavaEnv::GetJavaEnv() and deletes through it. This fully resolves the crashes in our project.
Impact
Any plugin using the (widely documented) FScopedJavaObject / FJavaHelper patterns crashes at runtime on 5.8 Android. Public example: getsentry/sentry-unreal issue #1484.