Skip to content

[Java.Interop] fix global ref leak in ConstructPeer#1403

Open
jonathanpeppers wants to merge 2 commits intomainfrom
dev/peppers/gref-leak
Open

[Java.Interop] fix global ref leak in ConstructPeer#1403
jonathanpeppers wants to merge 2 commits intomainfrom
dev/peppers/gref-leak

Conversation

@jonathanpeppers
Copy link
Copy Markdown
Member

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.

jonathanpeppers and others added 2 commits April 15, 2026 08:31
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>
@jonathanpeppers jonathanpeppers marked this pull request as ready for review April 15, 2026 21:24
Copilot AI review requested due to automatic review settings April 15, 2026 21:24
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

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.ConstructPeer to dispose the previous PeerReference when replacing it with a new global ref.
  • Make JniObjectReference.Dispose (ref ...) safely handle Invalid reference types (skip JNI delete, but still invalidate).
  • Add a regression test asserting global reference count does not increase across repeated ConstructPeer calls.

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 ();
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

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

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 ();
}

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants