From 99716ed44029a229e3c280a5f934b627eb684309 Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Thu, 13 Aug 2026 16:53:12 -0500 Subject: [PATCH] Fix JNI reference disposal with ownership flags JavaConvert creates temporary primitive and string wrappers with TransferLocalRef combined with DoNotRegister. JNIEnv.DeleteRef previously switched on the full flags value, so the combined value matched no ownership case and leaked the local reference. Mask non-ownership flags before selecting the disposal path. Add a deterministic regression that consumes a local string reference with the affected flag combination and verifies the thread-local reference count remains unchanged. Fixes #10589 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: b5fb7339-61f9-45af-bdac-4bdcdee623d9 --- src/Mono.Android/Android.Runtime/JNIEnv.cs | 2 +- .../Java.Interop/JavaConvertTest.cs | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/Mono.Android/Android.Runtime/JNIEnv.cs b/src/Mono.Android/Android.Runtime/JNIEnv.cs index 4a6e9a468dd..d214cf9576b 100644 --- a/src/Mono.Android/Android.Runtime/JNIEnv.cs +++ b/src/Mono.Android/Android.Runtime/JNIEnv.cs @@ -346,7 +346,7 @@ public static void EnsureLocalCapacity (int capacity) internal static void DeleteRef (IntPtr handle, JniHandleOwnership transfer) { - switch (transfer) { + switch (transfer & (JniHandleOwnership.TransferLocalRef | JniHandleOwnership.TransferGlobalRef)) { case JniHandleOwnership.DoNotTransfer: break; case JniHandleOwnership.TransferLocalRef: diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/JavaConvertTest.cs b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/JavaConvertTest.cs index f59d79b9aa6..79e72d03458 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/JavaConvertTest.cs +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/JavaConvertTest.cs @@ -114,6 +114,20 @@ public void NullStringMarshalsAsIntPtrZero () Assert.AreEqual (null, list [0]); } + [Test] + public void DeleteLocalRefWithDoNotRegister () + { + int initialLocalReferenceCount = Java.Interop.Runtime.LocalReferenceCount; + + using (var value = new Java.Lang.String ( + JNIEnv.NewString ("value"), + JniHandleOwnership.TransferLocalRef | JniHandleOwnership.DoNotRegister)) { + Assert.AreEqual ("value", value.ToString ()); + } + + Assert.AreEqual (initialLocalReferenceCount, Java.Interop.Runtime.LocalReferenceCount); + } + [Test] public void MarshalInt23Array () {