Skip to content

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

Merged
jonathanpeppers merged 3 commits intomainfrom
dev/peppers/gref-leak
Apr 16, 2026
Merged

[Java.Interop] fix global ref leak in ConstructPeer#1403
jonathanpeppers merged 3 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 thread tests/Java.Interop-Tests/Java.Interop/JniRuntimeJniValueManagerContract.cs Outdated
Comment on lines +107 to +109
var orig = newRef;
newRef = orig.NewGlobalRef ();
JniObjectReference.Dispose (ref orig);
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.

Maybe I'm missing something, but I wonder why we even create a new global ref to the same object if we then release the old handle? This means that we own the gref and we could maybe just keep using it without the need to create a new one? I must be missing something 🤔

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

The peer's current PeerReference (orig) might not be a global ref — it could be Invalid type (from SetPeerReference with a raw jobject), a local ref, or a weak global ref. NewGlobalRef() ensures we get a proper global ref regardless of the input type. We can't just keep using the existing ref because:

  1. If it's Invalid type, JNI operations on it may fail or behave unpredictably
  2. If it's a local ref, it becomes stale when the JNI frame ends
  3. Only global refs provide the stable, long-lived reference the peer table needs

So NewGlobalRef + Dispose(old) normalizes any ref type into a proper global ref.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@jonathanpeppers jonathanpeppers merged commit 7b018fe into main Apr 16, 2026
2 checks passed
@jonathanpeppers jonathanpeppers deleted the dev/peppers/gref-leak branch April 16, 2026 14:51
jonathanpeppers added a commit that referenced this pull request Apr 16, 2026
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 pushed a commit to dotnet/android that referenced this pull request Apr 24, 2026
…kiness fixed (#11202)

Fixes: #11201

The test is failing across multiple CI configurations. Multiple fixes
have been merged (#11112, dotnet/java-interop#1403,
dotnet/java-interop#1410) but the leak is not fully resolved yet.

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

3 participants