Skip to content

[Mono.Android] Fix JNI local reference disposal with ownership flags - #12380

Open
jonathanpeppers wants to merge 1 commit into
mainfrom
jonathanpeppers-fix-java-collection-jni-leak
Open

[Mono.Android] Fix JNI local reference disposal with ownership flags#12380
jonathanpeppers wants to merge 1 commit into
mainfrom
jonathanpeppers-fix-java-collection-jni-leak

Conversation

@jonathanpeppers

@jonathanpeppers jonathanpeppers commented Aug 13, 2026

Copy link
Copy Markdown
Member

Description

JavaConvert creates temporary primitive and string wrappers using JniHandleOwnership.TransferLocalRef | JniHandleOwnership.DoNotRegister. JNIEnv.DeleteRef() previously switched on the full flags value, so this combination matched no ownership case and the local reference was not deleted.

Mask non-ownership flags before selecting the JNI disposal path. This fixes the local-reference accumulation reached through JavaDictionary keys/values, JavaSet/JavaCollection, System.Linq.Extensions.ToEnumerator_Dispose, and JavaConvert, including AndroidMessageHandler response-header enumeration.

Add a deterministic regression that consumes one local string reference with the affected ownership combination and verifies the thread-local JNI reference count remains unchanged. The old code leaves the count at initial + 1, so no table-size-dependent loop or GC/local-frame workaround is needed.

Fixes #10589

Testing

  • Repository source build
  • Focused on-device regression: 1 passed, 0 failed on Pixel 7, Android 17 / API 37
  • Android 7 / API 24-25 hardware run (no Android 7 device was attached)

  • Useful description of why the change is necessary.
  • Links to issues fixed
  • Unit tests

Copilot AI lite review requested due to automatic review settings August 13, 2026 21:47

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes a local JNI reference leak when JniHandleOwnership includes non-ownership flags (notably DoNotRegister) by masking those flags before choosing the disposal path in JNIEnv.DeleteRef. It also adds an on-device regression test to ensure local reference counts remain stable after creating/disposing many local string references.

Changes:

  • Mask JniHandleOwnership to only transfer-ownership bits before switching disposal logic in JNIEnv.DeleteRef.
  • Add a regression test that creates/disposes 600 local string references using TransferLocalRef | DoNotRegister and verifies no local-ref accumulation.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/JavaConvertTest.cs Adds a regression test to catch local-reference accumulation when DoNotRegister is combined with transfer ownership.
src/Mono.Android/Android.Runtime/JNIEnv.cs Fixes DeleteRef to ignore non-ownership flags when selecting the deletion path, ensuring local refs are disposed correctly.

Comment thread tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/JavaConvertTest.cs Outdated
@jonathanpeppers
jonathanpeppers force-pushed the jonathanpeppers-fix-java-collection-jni-leak branch from f3b8228 to 6b98592 Compare August 13, 2026 21:53
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
@jonathanpeppers
jonathanpeppers force-pushed the jonathanpeppers-fix-java-collection-jni-leak branch from 6b98592 to 99716ed Compare August 13, 2026 21:53
@jonathanpeppers

Copy link
Copy Markdown
Member Author

/review

@jonathanpeppers jonathanpeppers added the ready-to-review This PR is ready to review/merge, I think any CI failures are just flaky (ignorable). label Aug 14, 2026
@jonathanpeppers

Copy link
Copy Markdown
Member Author

@dalexsoto review

@github-actions

github-actions Bot commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Android PR Reviewer completed successfully!

Generated by Android PR Reviewer for #12380

@dalexsoto dalexsoto left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Masking non-ownership flags before JNI reference disposal correctly handles DoNotRegister combinations, and the deterministic local-reference-count regression covers the leak path.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Needs Changes

Findings: 0 errors · 0 warnings · 1 suggestion

The ownership-mask fix correctly addresses the reported local-reference leak, and the deterministic reference-count assertion is well targeted. The remaining suggestion is to cover the global-reference branch that this same implementation change also affects.

CI is still pending: 42 of 44 checks are complete and passing; MSBuild+Emulator 2 is running and the aggregate dotnet-android check remains queued.

Generated by Android PR Reviewer for #12380 · gpt56 · 93 AIC · ⌖ 19 AIC · ⊞ 25.6K
Comment /review to run again


using (var value = new Java.Lang.String (
JNIEnv.NewString ("value"),
JniHandleOwnership.TransferLocalRef | JniHandleOwnership.DoNotRegister)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 💡 TestingDeleteRef() now changes handling for both TransferLocalRef | DoNotRegister and TransferGlobalRef | DoNotRegister, but this regression only protects the local-reference branch. Please add or parameterize a global-reference case using GlobalReferenceCount so the other newly supported ownership combination cannot regress unnoticed.

Rule: Bug fixes need regression tests

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-to-review This PR is ready to review/merge, I think any CI failures are just flaky (ignorable).

Projects

None yet

Development

Successfully merging this pull request may close these issues.

JNI Error with .NET 10 / Android 16

3 participants