Skip to content

[Java.Interop] Fix .NET 10 peer control-block lifetime leaks - #1502

Merged
jonathanpeppers merged 1 commit into
release/10.0.1xxfrom
simonrozsival-net-10-peer-control-block-lifetime
Oct 28, 2026
Merged

jonathanpeppers merged 1 commit into
release/10.0.1xxfrom
simonrozsival-net-10-peer-control-block-lifetime

Conversation

@simonrozsival

@simonrozsival simonrozsival commented Oct 28, 2026 •

Copy link
Copy Markdown
Member

Summary

Service the .NET 10 peer-lifetime fixes for dotnet/android#13290 on release/10.0.1xx, based on Java.Interop 8e8291c9ef45b4bbc28b09456dc5888eb80af77b.

  • In both JavaObject.Finalized() and JavaException.Finalized(), release a still-invalid native control block in finally after Dispose(false). Keep the .NET 10 FEATURE_JNIOBJECTREFERENCE_INTPTRS guards.
  • Preserve Dispose(false) entering with an invalid JNI reference, reference reconstruction by that callback (including when it subsequently throws), JNI handle deletion, and bridge/reference-tracking teardown order. This does not swap the finalization callback and reference-clear calls.
  • Route unregistered DisposePeerUnlessReferenced() through public DisposePeer(value), setting the disposed state and invoking Dispose(true) with a valid JNI reference before ordinary unregister/delete/clear/suppress processing.
  • Document ownership of the native allocation separately from ownership of the JNI reference.

This adapts the finalization fix in dotnet/android#13016 and the unregistered-disposal correction in dotnet/android#12772, without bringing their unrelated changes into servicing. No public API or native control-block layout changes.

The companion dotnet/android#13293 updates the submodule pin and carries the 36-case on-device regression fixture. Land this upstream servicing change first; the Android PR is explicitly dependent on it. Its gitlink references this canonical PR-head commit, which was successfully fetched from dotnet/java-interop into a fresh object database. If merging rewrites this commit, update the Android pin to the landed SHA before merging the companion PR.

Related main-only coverage is in dotnet/android#13292, where the production corrections already existed. The deferred release/11.0.1xx backport is not included.

Validation

Built the changed in-tree .NET 10 Android SDK with the repository preparation/bootstrap/PrepareJavaInterop workflow and make leeroy, including native runtimes, API 36/36.1 bindings, and ConfigureLocalWorkload. Built the repository instrumentation APK and public Rect reproducer with ./dotnet-local.sh, not modified stock framework DLLs.

On an Android 15 ARM64 emulator:

  • 32/32 focused lifecycle cases passed, plus 4/4 separately selected JNI global-reference deletion cases. Actual NUnit XML case identities/counts were checked. These runs used the source SDK's Mono 10.0.11 runtime.
  • Coverage includes both peer classes; real natural finalization; invalid and never-allocated blocks; throwing constructors; live Mono peer re-registration; invalid-reference callback entry; throwing finalization callbacks; reference reconstruction with and without exceptions; explicit/idempotent disposal; registered versus unregistered disposal; aliases; throwing Dispose(true) callbacks with retry; and JNI reference deletion.
  • Rebuilt the public reproducer with the changed in-tree framework and RuntimeFrameworkVersion="10.0.13". Loaded framework identities were checked. For 75,000 additional wrappers between batches 5 and 20:
Mode Native allocated delta JNI global-reference delta
Natural finalization -208 bytes 0
Unregister, then DisposeUnlessReferenced() -400 bytes 0
Explicit disposal +8,544 bytes 0

Median adjacent-batch native growth was 0 bytes per wrapper in all three modes. Whole-process allocation counters include background noise; the pointer-level regression assertions independently verify block release.

An additional run intended to select existing peer fixtures ignored the legacy suite selector and ran 945 tests: 934 passed, 5 ignored, and 6 HTTP redirect/certificate tests returned HTTP 403 errors. Those HTTP errors were not baselined and are not claimed to be pre-existing. All 34 cases in the intended existing JavaObjectTest, JavaExceptionTests, and AndroidValueManagerContractTests fixtures passed.

Context: dotnet/android#13290
Context: dotnet/android#13016
Context: dotnet/android#12772

Finalization clears the JNI reference before setting the disposed state, leaving JavaObject and JavaException native control blocks allocated. Release a still-invalid block in a finally clause after Dispose(false), retaining references reconstructed by the callback even if it throws. Preserve the existing callback entry state, JNI reference deletion, and reference-tracking teardown order.

Route unregistered DisposePeerUnlessReferenced through public DisposePeer so it sets the disposed state and invokes Dispose(true) before deleting the JNI reference and suppressing finalization. Retain the .NET 10 IntPtr feature guards and document the lifecycle contract.

Validated through an in-tree Android SDK build, 36 focused on-device regression cases, and the source-built Rect reproducer on Mono 10.0.13. The companion Android change carries the regression fixture and submodule pin.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 3ac73074-6c43-4940-8cde-8925203f171d

Copilot AI 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.

🟢 Approval recommended

No active review issues are reported for the JNI lifetime-management cleanup changes.

No open issues 🎉

What changed in this PR

Fixes .NET 10 native peer control-block leaks in Java.Interop’s JNI lifetime management without changing public APIs or the native control-block layout.

Changes:

  • Releases invalid control blocks after finalization callbacks, even when callbacks throw, while preserving reconstructed JNI references.
  • Routes unregistered peer disposal through the public disposal path so disposed state and callbacks are handled consistently.
  • Documents native allocation ownership separately from JNI reference cleanup.
File Description
src/​Java.Interop/​Java.Interop/​JniRuntime.JniValueManager.cs Uses public disposal handling for unregistered peers.
src/​Java.Interop/​Java.Interop/​JniObjectReferenceControlBlock.cs Adds conditional cleanup for invalid control blocks.
src/​Java.Interop/​Java.Interop/​JavaObject.cs Adds control-block cleanup in finalization’s finally block.
src/​Java.Interop/​Java.Interop/​JavaException.cs Applies the same finalization cleanup to exception peers.
src/​Java.Interop/​Documentation/​Java.Interop/​IJavaPeerable.xml Clarifies teardown ordering and allocation ownership.

Review effort: Balanced

@jonathanpeppers
jonathanpeppers merged commit 567e6b6 into release/10.0.1xx Oct 28, 2026
3 checks passed
@jonathanpeppers
jonathanpeppers deleted the simonrozsival-net-10-peer-control-block-lifetime branch October 28, 2026 13:31
jonathanpeppers added a commit to dotnet/android that referenced this pull request Oct 28, 2026
Update Java.Interop to the release/10.0.1xx tip after dotnet/java-interop#1502
was merged. The landed commit has the same source tree as the previously
pinned PR commit.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
jonathanpeppers added a commit to dotnet/android that referenced this pull request Oct 28, 2026
Context: #13290
Context: dotnet/java-interop#1502
Changes: dotnet/java-interop@8e8291c...f79a53b

Update the Java.Interop submodule to the canonical servicing PR commit, which frees still-invalid control blocks after finalization callbacks and routes unregistered disposal through the normal disposed-state/callback path. Preserve callback reference reconstruction, exception cleanup, and Mono bridge ordering rather than swapping Finalized and reference clearing.

Add 36 on-device regression cases for both JavaObject and JavaException, covering natural finalization, invalid and absent blocks, throwing constructors/callbacks, reference reconstruction, registered and unregistered disposal, aliases, idempotency, and JNI reference deletion. Preserve Mono's re-registration of peers whose Java references are still valid.

The in-tree SDK build and all 36 focused cases passed. The source-built public Rect reproducer on Mono 10.0.13 remained flat over 75,000 additional wrappers: native deltas were -208 bytes for natural finalization and -400 bytes for unregistered disposal, with no JNI global-reference growth.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Jonathan Peppers <jonathan.peppers@microsoft.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

Development

Successfully merging this pull request may close these issues.

3 participants