[Java.Interop] fix global ref leak in ConstructPeer#1403
Open
jonathanpeppers wants to merge 2 commits intomainfrom
Open
[Java.Interop] fix global ref leak in ConstructPeer#1403jonathanpeppers wants to merge 2 commits intomainfrom
jonathanpeppers wants to merge 2 commits intomainfrom
Conversation
Fixes: dotnet/android#11101 Fixes: dotnet/android#10989 When ConstructPeer is called on a peer that already has a valid PeerReference (but without Activatable set), lines 107-108 had a bug: the second JniObjectReference.Dispose call was a duplicate of line 100, and NewGlobalRef created a new global ref without disposing the old one. This caused a global ref leak every time ConstructPeer was called multiple times during constructor chains. Changes: - Fix ConstructPeer lines 107-108 to properly dispose the old PeerReference before replacing it with a new global ref. - Make JniObjectReference.Dispose a no-op for Invalid type refs, since activation code stores raw jobject handles with Invalid type that cannot be deleted via JNI. - Add regression test that calls ConstructPeer twice on the same peer and asserts the global ref count does not grow. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Fixes a JNI global reference leak when ConstructPeer is invoked multiple times for the same managed peer (common in constructor chains during activation).
Changes:
- Update
JniRuntime.JniValueManager.ConstructPeerto dispose the previousPeerReferencewhen replacing it with a new global ref. - Make
JniObjectReference.Dispose (ref ...)safely handleInvalidreference types (skip JNI delete, but still invalidate). - Add a regression test asserting global reference count does not increase across repeated
ConstructPeercalls.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| tests/Java.Interop-Tests/Java.Interop/JniRuntimeJniValueManagerContract.cs | Adds regression test covering repeated ConstructPeer calls and global ref accounting. |
| src/Java.Interop/Java.Interop/JniRuntime.JniValueManager.cs | Fixes the leak by disposing the old peer global ref when re-globalizing an existing PeerReference. |
| src/Java.Interop/Java.Interop/JniObjectReference.cs | Allows disposing Invalid-typed references without throwing. |
Comment on lines
+150
to
+166
|
|
||
| // Now simulate the constructor chain calling ConstructPeer multiple times | ||
| var ref1 = new JniObjectReference (handle.Handle); | ||
| valueManager.ConstructPeer (peer, ref ref1, JniObjectReferenceOptions.Copy); | ||
|
|
||
| int grefAfterFirst = JniEnvironment.Runtime.GlobalReferenceCount; | ||
|
|
||
| var ref2 = new JniObjectReference (handle.Handle); | ||
| valueManager.ConstructPeer (peer, ref ref2, JniObjectReferenceOptions.Copy); | ||
|
|
||
| int grefAfterSecond = JniEnvironment.Runtime.GlobalReferenceCount; | ||
|
|
||
| // The second ConstructPeer should NOT create an additional global ref | ||
| Assert.AreEqual (grefAfterFirst, grefAfterSecond, | ||
| "Second ConstructPeer call should not create an additional global ref"); | ||
|
|
||
| peer.Dispose (); |
There was a problem hiding this comment.
peer.Dispose () is only executed if the assertions above it pass. If this test fails, the constructed peer may stay registered and keep a global ref alive, which can cascade into unrelated test failures. Wrap the body after peer creation in a try/finally (or use using for peer) to guarantee cleanup even on assertion failures.
Suggested change
| // Now simulate the constructor chain calling ConstructPeer multiple times | |
| var ref1 = new JniObjectReference (handle.Handle); | |
| valueManager.ConstructPeer (peer, ref ref1, JniObjectReferenceOptions.Copy); | |
| int grefAfterFirst = JniEnvironment.Runtime.GlobalReferenceCount; | |
| var ref2 = new JniObjectReference (handle.Handle); | |
| valueManager.ConstructPeer (peer, ref ref2, JniObjectReferenceOptions.Copy); | |
| int grefAfterSecond = JniEnvironment.Runtime.GlobalReferenceCount; | |
| // The second ConstructPeer should NOT create an additional global ref | |
| Assert.AreEqual (grefAfterFirst, grefAfterSecond, | |
| "Second ConstructPeer call should not create an additional global ref"); | |
| peer.Dispose (); | |
| try { | |
| // Now simulate the constructor chain calling ConstructPeer multiple times | |
| var ref1 = new JniObjectReference (handle.Handle); | |
| valueManager.ConstructPeer (peer, ref ref1, JniObjectReferenceOptions.Copy); | |
| int grefAfterFirst = JniEnvironment.Runtime.GlobalReferenceCount; | |
| var ref2 = new JniObjectReference (handle.Handle); | |
| valueManager.ConstructPeer (peer, ref ref2, JniObjectReferenceOptions.Copy); | |
| int grefAfterSecond = JniEnvironment.Runtime.GlobalReferenceCount; | |
| // The second ConstructPeer should NOT create an additional global ref | |
| Assert.AreEqual (grefAfterFirst, grefAfterSecond, | |
| "Second ConstructPeer call should not create an additional global ref"); | |
| } finally { | |
| peer.Dispose (); | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes: dotnet/android#11101
Fixes: dotnet/android#10989
When ConstructPeer is called on a peer that already has a valid PeerReference (but without Activatable set), lines 107-108 had a bug: the second JniObjectReference.Dispose call was a duplicate of line 100, and NewGlobalRef created a new global ref without disposing the old one. This caused a global ref leak every time ConstructPeer was called multiple times during constructor chains.
Changes:
Fix ConstructPeer lines 107-108 to properly dispose the old PeerReference before replacing it with a new global ref.
Make JniObjectReference.Dispose a no-op for Invalid type refs, since activation code stores raw jobject handles with Invalid type that cannot be deleted via JNI.
Add regression test that calls ConstructPeer twice on the same peer and asserts the global ref count does not grow.