Repository navigation
[Java.Interop] Fix .NET 10 peer control-block lifetime leaks - #1502
Merged
jonathanpeppers merged 1 commit intoOct 28, 2026
Merged
jonathanpeppers merged 1 commit into
jonathanpeppers merged 1 commit into
Conversation
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
There was a problem hiding this comment.
🟢 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
approved these changes
Oct 28, 2026
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>
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.
Summary
Service the .NET 10 peer-lifetime fixes for dotnet/android#13290 on
release/10.0.1xx, based on Java.Interop8e8291c9ef45b4bbc28b09456dc5888eb80af77b.JavaObject.Finalized()andJavaException.Finalized(), release a still-invalid native control block infinallyafterDispose(false). Keep the .NET 10FEATURE_JNIOBJECTREFERENCE_INTPTRSguards.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.DisposePeerUnlessReferenced()through publicDisposePeer(value), setting the disposed state and invokingDispose(true)with a valid JNI reference before ordinary unregister/delete/clear/suppress processing.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-interopinto 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.1xxbackport is not included.Validation
Built the changed in-tree .NET 10 Android SDK with the repository preparation/bootstrap/
PrepareJavaInteropworkflow andmake leeroy, including native runtimes, API 36/36.1 bindings, andConfigureLocalWorkload. Built the repository instrumentation APK and public Rect reproducer with./dotnet-local.sh, not modified stock framework DLLs.On an Android 15 ARM64 emulator:
Dispose(true)callbacks with retry; and JNI reference deletion.RuntimeFrameworkVersion="10.0.13". Loaded framework identities were checked. For 75,000 additional wrappers between batches 5 and 20:DisposeUnlessReferenced()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, andAndroidValueManagerContractTestsfixtures passed.