Fix SIGABRT on activity destroy: null view in registerFragment

Crash on device (Pixel 9 Pro, Android 17) when swiping the app away
from recents:

  JNI DETECTED ERROR: java_object == null in call to GetObjectClass
  #06 libgio.so (registerFragment+104)

Root cause: Gio's window.detach sends an EMPTY AndroidViewEvent
(View == 0) as its detach signal (os_android.go: window.detach ->
processEvent(AndroidViewEvent{})), which fires when the GioView is
destroyed — i.e. the activity going away on a recents-wipe. Our
handleEvent passed that null ref straight into registerFragment,
whose GetObjectClass(null) aborts. Pre-existing latent bug; the
emulator never delivered a detach event in testing (home keeps the
view attached, force-stop kills before the event dispatches).

Guard on both sides: handleEvent ignores the View == 0 detach
signal (a re-attach arrives as a fresh event with a live view), and
registerFragment returns early on a null view as defense in depth.

Verified on the crashing device: recents swipe now closes the app
cleanly, crash buffer empty.
This commit is contained in:
Greg Pomerantz 2026-08-20 18:49:00 -04:00
parent 2bdff5c8e8
commit fdbffc9f5c
2 changed files with 20 additions and 7 deletions

View File

@ -41,6 +41,15 @@ func impl_start() {}
func handleEvent(e event.Event) { func handleEvent(e event.Event) {
switch e := e.(type) { switch e := e.(type) {
case app.AndroidViewEvent: case app.AndroidViewEvent:
// View == 0 is Gio's DETACH signal (window.detach sends an empty
// AndroidViewEvent when the GioView is destroyed — e.g. the activity
// going away on a recents-wipe). There is nothing to register for a
// detached view; passing the null ref on would abort in JNI
// (GetObjectClass on null). A re-attach arrives as a fresh event
// with a live view.
if e.View == 0 {
return
}
theJVM = (*C.JavaVM)(unsafe.Pointer(app.JavaVM())) theJVM = (*C.JavaVM)(unsafe.Pointer(app.JavaVM()))
RunInJVM(func(env *JNIEnv) { RunInJVM(func(env *JNIEnv) {
C.registerFragment(env, (C.jobject)(unsafe.Pointer(e.View))) C.registerFragment(env, (C.jobject)(unsafe.Pointer(e.View)))

View File

@ -15,13 +15,17 @@ static jobject g_view = NULL;
void void
registerFragment(JNIEnv *env, jobject view) { registerFragment(JNIEnv *env, jobject view) {
if (view != NULL) { if (view == NULL) {
if (g_view == NULL) { // Detach signal (Gio sends an empty AndroidViewEvent when the
g_view = (*env)->NewGlobalRef(env, view); // view is destroyed). The Go side already filters these; this
} else if (g_view != view) { // guard keeps the JNI calls safe if one ever slips through.
(*env)->DeleteGlobalRef(env, g_view); return;
g_view = (*env)->NewGlobalRef(env, view); }
} if (g_view == NULL) {
g_view = (*env)->NewGlobalRef(env, view);
} else if (g_view != view) {
(*env)->DeleteGlobalRef(env, g_view);
g_view = (*env)->NewGlobalRef(env, view);
} }
jclass cls = (*env)->GetObjectClass(env, view); jclass cls = (*env)->GetObjectClass(env, view);
jmethodID mid = (*env)->GetMethodID(env, cls, "getContext", "()Landroid/content/Context;"); jmethodID mid = (*env)->GetMethodID(env, cls, "getContext", "()Landroid/content/Context;");