Skip to content
This repository was archived by the owner on Aug 27, 2026. It is now read-only.

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

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

[Java.Interop] fix global ref leak in ConstructPeer#1403
jonathanpeppers merged 3 commits into
mainfrom
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.

jonathanpeppersand others added 2 commits April 15, 2026 08:31
Fixes: dotnet/android#11101Fixes: 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
CopilotAI review requested due to automatic review settings April 15, 2026 21:24

CopilotAI left a comment

Copy link
Copy Markdown

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.

FileDescription
tests/Java.Interop-Tests/Java.Interop/JniRuntimeJniValueManagerContract.csAdds regression test covering repeated ConstructPeer calls and global ref accounting.
src/Java.Interop/Java.Interop/JniRuntime.JniValueManager.csFixes the leak by disposing the old peer global ref when re-globalizing an existing PeerReference.
src/Java.Interop/Java.Interop/JniObjectReference.csAllows disposing Invalid-typed references without throwing.

Comment threadtests/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
MemberAuthor

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 mainApr 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#11101Fixes: 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>
@github-actionsgithub-actionsBot locked and limited conversation to collaborators May 17, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

3 participants

@jonathanpeppers@simonrozsival