Repository navigation
Description
Summary
In release (and profile) builds, using a released JObject crashes the process with a native SIGSEGV at address 0x0 instead of throwing UseAfterReleaseError. The same applies to DoubleReleaseError.
The cause is JGlobalReference._releasedStackTrace in lib/src/jreference.dart (jni 1.0.3 and 1.1.0, and current main):
String? get _releasedStackTrace {
if (_releasedStackTracePointer.value != nullptr) { // <- dereferences the pointer itself
return _releasedStackTracePointer.value.cast<Utf8>().toDartString();
}
return null;
}_releasedStackTracePointer is only allocated inside an assert(() { ... }()) in the constructor, so with asserts disabled it stays nullptr, and .value reads address 0. The getter is evaluated as the argument of the error that is about to be thrown:
JObjectPtr get pointer {
if (isReleased) {
throw UseAfterReleaseError(_releasedStackTrace); // segfaults before the throw
}
...
}In debug builds this is a catchable Dart error with a helpful stack trace; in release builds it is an unrecoverable native crash with no Dart-side signal. Error reporters only see a tombstone pointing at JGlobalReference._releasedStackTrace.
_appendToStackTrace reads the same getter, so I believe registeredInArena would hit it too in release when Jni.captureStackTraceOnRelease is enabled.
This seems to have been introduced by #2851 (store and display release stack trace).
Reproduction
final s = 'hello'.toJString();
s.release();
s.toDartString(); // debug: UseAfterReleaseError; release: SIGSEGV 0x0Run with flutter run --release on Android.
Real-world trace
We hit this in production through package:maplibre (Android, 0.3.6). Its StyleController wraps a global ref to the Java Style, which the plugin releases when the map is disposed or the style reloads. An addImage that resolved after an async gap (Image.toByteData) then used the released ref:
null pointer dereference: SIGSEGV 0x0000000000000000
#00 pc ... libapp.so (JGlobalReference._releasedStackTrace [jreference.dart])
#01 pc ... libapp.so (Style$$Methods.addImage [jreference.dart:116])
#03 pc ... libapp.so (StyleControllerAndroid.addImage.<anonymous closure> [style_controller.dart:253])
#04 pc ... libapp.so (using [arena.dart:127])
#05 pc ... libapp.so (StyleControllerAndroid.addImage [style_controller.dart:235])
#06 pc ... libapp.so (StyleController.addImageFromCanvas [style_controller.dart:108])
...
#14 pc ... libapp.so (_Image.toByteData.<anonymous closure>.<anonymous closure> [painting.dart:2228])
The use-after-release is a bug on the caller side, but it should surface as UseAfterReleaseError, not a segfault.
Suggested fix
Check the outer pointer before dereferencing:
String? get _releasedStackTrace {
if (_releasedStackTracePointer == nullptr) return null;
final value = _releasedStackTracePointer.value;
return value == nullptr ? null : value.cast<Utf8>().toDartString();
}Environment
- jni 1.0.3 (also present in 1.1.0 and
main) - Flutter 3.47.7 stable, release build
- Android 15
Activity
liamappelbe commentedon Oct 30, 2026
Ok, looks like we just need to make the
_releasedStackTracegetter null safe. Maybe we also need to do an extra run of our tests on CI without asserts enabled.