Skip to content

[Mono.Android] Release transferred JNI inputs on exceptions - #13015

Merged
simonrozsival merged 2 commits into
mainfrom
simonrozsival-transferred-jni-reference-cleanup
Oct 6, 2026
Merged

simonrozsival merged 2 commits into
mainfrom
simonrozsival-transferred-jni-reference-cleanup

Conversation

@simonrozsival

@simonrozsival simonrozsival commented Oct 6, 2026 •

Copy link
Copy Markdown
Member

Refs #13010 (finding 2). Independent, non-stacked fix; this does not close the investigation tracker.

Transferred JNI inputs leaked when peer construction, lookup, Throwable metadata extraction, or generated Java.Interop activation threw before success-path JNIEnv.DeleteRef(). Release the original input in finally at boundaries that retain its ownership while nested construction receives Copy/CopyAndDoNotRegister.

Java.Lang.Throwable reads message, cause, and stack before SetHandle(). A new borrowed-only protected Java.Interop.JavaException initialization overload releases a typed transferred input only on initialization failure; success leaves normal transfer consumption to SetHandle(). Existing constructors retain their behavior; Java.Interop has no Android ownership dependency. Wrapper-owned copied GREFs remain separate from the input.

Generated Java.Interop leaf/invoker activation uses try/finally IL with the return outside the EH region. Xamarin-style proxies still delegate transfer to their IntPtr constructors, without a second cleanup owner. Unsupported inherited class-constructor activation stays null; supported inherited-interface invokers are tested. No registration-order, finalizer suppression, control-block, bridge-synchronization, or Throwable DoNotRegister policy changes.

Regression evidence

Restoring the original unsafe Object/Throwable boundaries and post-constructor-only generated cleanup, rebuilding the SDK, and cleaning/rebuilding the APK produced 20 failures / 26 passes in the 46-case matrix. Each failure expected one deletion of the original transferred handle and observed zero. Covered failures: local/global construction, unmapped lookup, GetObject/direct leaf/invoker activation, independent message/cause/first-stack/second-stack extraction.

With fixes restored, 46 regressions + 5 existing invoker tests passed twice on CoreCLR/trimmable arm64 and all 51 passed on Release NativeAOT/trimmable arm64, zero ignored. Coverage includes success, existing-peer return, incompatible/null result, borrowing, local/global transfer, DoNotRegister, and separately disposed partial-wrapper copies. The test-only forwarding observer checks exact handle/type/local-thread deletion, delegates native operations/counts/logging, leaves the bridge's cached manager unchanged, and restores the runtime manager in finally. Its preserved private-setter access executed under NativeAOT. Source-object copied GREFs are independently tracked and asserted disposed.

Generator regressions originally found zero EH regions; fixed emitted-shape and related coverage passed 19/19. The non-Android JVM fixture, its Java fixture, and unused TestJVM observation hook were removed at user request in 3c230eb. Android and generator regressions remain unchanged. Historical JVM results are not claimed as retained coverage.

Local validation

Commands use this branch's own source-built SDK. Validation-only RuntimeFrameworkVersion=10.0.0 worked around unavailable host reference pack 10.0.13; no product pins changed and Android app builds did not use that override.

  • ./dotnet-local.sh build build-tools/scripts/Prepare.proj -t:Prepare -p:RuntimeFrameworkVersion=10.0.0 — passed; default preparation initially failed on missing Microsoft.NETCore.App.Ref 10.0.13.
  • RuntimeFrameworkVersion=10.0.0 DOTNET_ROLL_FORWARD=Major ./dotnet-local.sh build src/Mono.Android/Mono.Android.csproj -p:AndroidApiLevel=37.1 -p:AndroidPlatformId=37.1 -p:AndroidFrameworkVersion=v17.1 -v minimal — passed, 39 existing warnings, zero errors.
  • RuntimeFrameworkVersion=10.0.0 DOTNET_ROLL_FORWARD=Major ./dotnet-local.sh build src/Mono.Android/Mono.Android.csproj -p:AndroidApiLevel=37.2 -p:AndroidPlatformId=37.2 -p:AndroidFrameworkVersion=v17.2 -v minimal — passed, 39 existing warnings, zero errors.
  • RuntimeFrameworkVersion=10.0.0 DOTNET_ROLL_FORWARD=Major make all — passed, including workload configuration; repeated after restoring all fixes.
  • DOTNET_ROLL_FORWARD=Major ./dotnet-local.sh test src/Microsoft.Android.Build.Tasks/Tests/Microsoft.Android.Build.Tasks.Tests/Microsoft.Android.Build.Tasks.Tests.csproj -p:RuntimeFrameworkVersion=10.0.0 --filter 'FullyQualifiedName~TransferredReferenceActivationTests|FullyQualifiedName~GenerateTrimmableTypeMapTests' -v minimal — 19 passed, zero failed/skipped.

CoreCLR build/install:
ANDROID_SERIAL=emulator-5556 DOTNET_ROLL_FORWARD=Major ./dotnet-local.sh build -t:Install -c Debug tests/Mono.Android-Tests/Mono.Android-Tests/Mono.Android.NET-Tests.csproj -p:AndroidTypeMapImplementation=trimmable -p:AdbTarget='-s emulator-5556' -p:IncludeCategories=TransferredReferences%3BInvokerActivation — passed, zero warnings/errors.

From tests/Mono.Android-Tests/Mono.Android-Tests:
ANDROID_SERIAL=emulator-5556 DOTNET_ROLL_FORWARD=Major ../../../dotnet-local.sh test Mono.Android.NET-Tests.csproj --no-build -c Debug -p:AndroidTypeMapImplementation=trimmable -p:AdbTarget='-s emulator-5556' -p:IncludeCategories=TransferredReferences%3BInvokerActivation --device emulator-5556 --report-trx --results-directory ../../../bin/TestDebug/TestResults — 51 selected/executed/passed, zero failed/ignored, twice. Original-behavior negative control used only category TransferredReferences: 20 failed / 26 passed.

NativeAOT build/install:
ANDROID_SERIAL=emulator-5556 DOTNET_ROLL_FORWARD=Major ./dotnet-local.sh build -t:Install -c Release tests/Mono.Android-Tests/Mono.Android-Tests/Mono.Android.NET-Tests.csproj -p:PublishAot=true -p:RuntimeIdentifier=android-arm64 -p:AndroidTypeMapImplementation=trimmable -p:AdbTarget='-s emulator-5556' -p:IncludeCategories=TransferredReferences%3BInvokerActivation — passed, 13 experimental/trim/AOT warnings, zero errors; verified native application .so payload.

From the same test-project directory:
ANDROID_SERIAL=emulator-5556 DOTNET_ROLL_FORWARD=Major ../../../dotnet-local.sh test Mono.Android.NET-Tests.csproj --no-build -c Release -p:PublishAot=true -p:RuntimeIdentifier=android-arm64 -p:AndroidTypeMapImplementation=trimmable -p:AdbTarget='-s emulator-5556' -p:IncludeCategories=TransferredReferences%3BInvokerActivation --device emulator-5556 --report-trx --results-directory ../../../bin/TestRelease/TestResults — 51 selected/executed/passed, zero failed/ignored.

After removing non-Android scaffolding:
DOTNET_ROLL_FORWARD=Major ./dotnet-local.sh build tests/Mono.Android-Tests/Mono.Android-Tests/Mono.Android.NET-Tests.csproj -c Debug -p:AndroidTypeMapImplementation=trimmable -p:IncludeCategories=TransferredReferences%3BInvokerActivation -v minimal — passed, zero warnings/errors. Device tests were not rerun for this removal-only follow-up; no Android fixture or product behavior changed.

git diff --check — passed. Device runs used a dedicated API35 arm64 emulator, now shut down.

Remaining uncertainty and scope

Four initial VM-wide GREF-count assertions observed +1, including borrowed cases. These remain unattributed, not dismissed as unrelated. Final handle-level observer runs had no outstanding observed GREF acquisitions and separately checked peer copies. This exceptional-path defect is not established as the cause of the intermittent successful-activation leak check.

MonoVM, other ABIs, and full suites were not run. Unsupported inherited class activation remains unchanged.


Pull Request
title and
description
should follow the
commit-messages.md workflow documentation, and in particular should include:

  • Useful description of why the change is necessary.
  • Links to issues fixed
  • Unit tests

Keep raw-input cleanup at Object, Throwable, and generated Java.Interop activation boundaries. Guard Throwable metadata extraction before SetHandle without consuming borrowed inputs or double-deleting copied peer references.

Add exact-reference regressions and emitted exception-handler coverage, validated on CoreCLR and NativeAOT trimmable runtimes.

Refs #13010

Co-authored-by: Copilot App <[email protected]>
Copilot AI balanced review requested due to automatic review settings October 6, 2026 09:00

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

🔵 Needs a closer look

Shared JNI lifetime and generated exception-handling changes warrant maintainer review, with MonoVM and other ABIs still unvalidated.

Review effort: Balanced
Findings: None

What changed in this PR

Addresses the exception-path JNI input leaks identified in #13010, finding 2, across Mono.Android and Java.Interop without closing the broader investigation.

Changes:

  • Releases transferred inputs when construction, lookup, or Throwable initialization fails.
  • Adds generated activation cleanup using try/finally.
  • Adds Android, JVM, and generated-code regression coverage.
File Description
tests/​Mono.Android-Tests/​Mono.Android-Tests/​proguard.cfg Preserves the Java failure fixture.
tests/​Mono.Android-Tests/​Mono.Android-Tests/​Mono.Android.NET-Tests.csproj Includes transferred-reference tests.
tests/​Mono.Android-Tests/​Mono.Android-Tests/​Java.Interop/​TransferredReferenceTests.cs Tests ownership and exceptional cleanup.
tests/​Mono.Android-Tests/​Mono.Android-Tests/​Java.Interop/​InvokerActivationTests.cs Adds activation-failure hooks.
tests/​Mono.Android-Tests/​Mono.Android-Test.Library/​java/​net/​dot/​android/​test/​TransferFailureThrowable.java Injects Android Throwable extraction failures.
src/​Mono.Android/​Java.Lang/​Throwable.cs Protects initialization and handle cleanup.
src/​Mono.Android/​Java.Lang/​Object.cs Releases transferred inputs in finally.
src/​Mono.Android/​Android.Runtime/​JNIEnv.cs Clarifies ownership cleanup documentation.
src/​Microsoft.Android.Sdk.TrimmableTypeMap/​Generator/​TypeMapAssemblyEmitter.cs Emits activation cleanup handlers.
src/​Microsoft.Android.Sdk.TrimmableTypeMap/​Generator/​PEAssemblyBuilder.cs Adds an exception-aware emission overload.
src/​Microsoft.Android.Build.Tasks/​Tests/​Microsoft.Android.Build.Tasks.Tests/​TransferredReferenceActivationTests.cs Verifies generated exception-handler structure.
external/​Java.Interop/​tests/​TestJVM/​TestJVM.cs Exposes reference-deletion observations.
external/​Java.Interop/​tests/​Java.Interop-Tests/​java/​net/​dot/​jni/​test/​TransferFailureThrowable.java Injects JVM Throwable extraction failures.
external/​Java.Interop/​tests/​Java.Interop-Tests/​Java.Interop/​ThrowableInputCleanupTests.cs Tests failure-only input disposal.
external/​Java.Interop/​tests/​Java.Interop-Tests/​Java.Interop-Tests.csproj Includes the JVM failure fixture.
external/​Java.Interop/​src/​Java.Interop/​PublicAPI.Unshipped.txt Records the protected initialization overload.
external/​Java.Interop/​src/​Java.Interop/​Java.Interop/​JavaException.cs Adds failure-safe borrowed Throwable initialization.

@simonrozsival

Copy link
Copy Markdown
Member Author

@dalexsoto review

@dalexsoto dalexsoto left a comment

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.

The complete 17-file ownership and generated-activation review found no blocking issue. Original transferred inputs have one cleanup owner on success and failure; wrapper-owned copies remain separate. Throwable failure-only initialization and generated finally/leave/return structure preserve the existing Xamarin, inherited-class, registration and finalization policies. Approval is scoped to the exceptional-path fix: the unattributed VM-wide GREF observations and broader intermittent successful-activation tracker are not declared resolved.

Remove the host-only Throwable tests, their Java fixture, and the now-unused TestJVM deletion observer. Keep the Android ownership regressions and generator coverage.

Refs #13010

Co-authored-by: Copilot App <[email protected]>
@simonrozsival
simonrozsival enabled auto-merge (squash) October 6, 2026 14:00
@simonrozsival

Copy link
Copy Markdown
Member Author

@dalexsoto stamp

@dalexsoto dalexsoto left a comment

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.

Stamping as requested by @simonrozsival; no code review performed.

@simonrozsival
simonrozsival merged commit b08b66e into main Oct 6, 2026
42 checks passed
@simonrozsival
simonrozsival deleted the simonrozsival-transferred-jni-reference-cleanup branch October 6, 2026 16:13
simonrozsival added a commit that referenced this pull request Oct 7, 2026
Core JNI interop tests now run exclusively in the Android device suite. Keeping their sources in the former standalone test directory leaves misleading ownership, external source globs, and separate desktop/Android Java fixture selection. This Android-only relocation puts the sources beside their actual test project and simplifies fixture compilation without changing supported Android test behavior.

Follow-up to the merged #13018. This PR now targets `main`; its diff contains only the Android test relocation and cleanup. It preserves the removal of the desktop harness, reflection-backed JNI managers, and obsolete `JavaVMFixture` scaffolding.

- Move core C# sources and Java fixtures to `tests/Mono.Android-Tests/Java.Interop-Tests/`; use the project's default `Compile` glob and a local `TestJarEntry` glob. Preserve the existing exclusion for tests of the internal `JNIEnv` layout.
- Remove `__ANDROID__` branches while retaining their Android assertions. Remove unsupported desktop-only custom-type-manager remapping tests and unused helper fixtures. Preserve `NO_MARSHAL_MEMBER_BUILDER_SUPPORT`, `NO_GC_BRIDGE_SUPPORT`, and applicable runtime/AOT exclusions.
- Consolidate `GetThis.java` onto the Android-safe implementation for every typemap. Remove duplicate `java-trimmable` selection and unused desktop test targets, and prevent implicit `AndroidJavaSource` compilation from duplicating fixture classes in the JAR.
- Update directly related documentation, instructions, and test catalog/skill guidance. Unrelated Java.Interop tooling suites are unchanged.

**Migration guidance:** Core JNI test additions belong in the new Android directory, including C# fixtures such as `ThrowableInputCleanupTests.cs` and `JavaPeerControlBlockTests.cs`, and Java fixtures under `java/net/dot/jni/test/`. Preserve coverage introduced by #13014 and #13015 when moving or updating these files. Other branches touching the old directory, including #13016, should rebase into the new location. Do not recreate the removed standalone desktop test project.

**Validation**

The following results were recorded during implementation. The installed-SDK build passed, but repository-SDK/device coverage remains blocked; marking this PR ready for review does not imply that coverage passed.

| Command / check | Outcome |
|---|---|
| `dotnet build tests/Mono.Android-Tests/Java.Interop-Tests/Java.Interop-Tests.NET.csproj -c Release` | Passed using the installed Android SDK, including the local Java fixture JAR and AAR; 0 warnings, 0 errors. Repeated after rebasing onto the parent. This is compile coverage, not validation against a newly built repository SDK. |
| `dotnet msbuild tests/Mono.Android-Tests/Java.Interop-Tests/Java.Interop-Tests.NET.csproj -p:Configuration=Release -getItem:Compile,TestJarEntry,AndroidJavaSource` | Passed during implementation: verified 40 local Compile items, all 12 Java fixtures, and no duplicate AndroidJavaSource items. |
| `javac --release 17 -d <session-artifacts>/java-fixture-classes tests/Mono.Android-Tests/Java.Interop-Tests/java/net/dot/jni/test/*.java external/Java.Interop/src/Java.Interop/java/net/dot/jni/GCUserPeerable.java external/Java.Interop/src/Java.Interop/java/net/dot/jni/ManagedPeer.java` | Passed for all 12 fixtures at the configured Java target version. |
| Android-preprocessed source comparison against parent | Passed for 39 migrated C# files during implementation; the remaining file intentionally removes desktop-only remapping coverage and its unused helpers. Capability guards are unchanged. |
| `jar tf tests/Mono.Android-Tests/Java.Interop-Tests/Jars/Mono.Android-Test-classes-trimmable.jar` and `javap -c -classpath tests/Mono.Android-Tests/Java.Interop-Tests/Jars/Mono.Android-Test-classes-trimmable.jar net.dot.jni.test.GetThis` | Passed: one GetThis class, implements GCUserPeerable, no desktop native registration/constructor calls. |
| `git diff HEAD^ HEAD --check` | Passed during implementation. |
| `make prepare CONFIGURATION=Release` | Blocked by NU1102: `Microsoft.NETCore.App.Ref` version `10.0.13` was unavailable in the configured feeds. The corresponding nuget.org package URL also returned HTTP 404. |
| `make all CONFIGURATION=Release` | Blocked by MSB4062: missing `bin/BuildRelease/net10.0/xa-prep-tasks.dll` after preparation failed. |
| `./dotnet-local.sh build -t:Install -c Release tests/Mono.Android-Tests/Mono.Android-Tests/Mono.Android.NET-Tests.csproj -p:AndroidTypeMapImplementation=llvm-ir` | Blocked by NETSDK1147: local SDK had no Android workload because repository SDK preparation/build could not complete. |
| `./dotnet-local.sh build -t:Install -c Release tests/Mono.Android-Tests/Mono.Android-Tests/Mono.Android.NET-Tests.csproj -p:AndroidTypeMapImplementation=trimmable` | Blocked by the same NETSDK1147 prerequisite. |

No on-device tests were run because the required local SDK/install builds failed, despite a connected arm64 emulator. Mono/CoreCLR/NativeAOT runtime behavior and both typemap device lanes remain unvalidated. No desktop JVM validation is required.

----

Pull Request
[title](https://github.com/dotnet/android/blob/main/Documentation/workflow/commit-messages.md#commit-summary) and
[description](https://github.com/dotnet/android/blob/main/Documentation/workflow/commit-messages.md#commit-body)
should follow the
[`commit-messages.md` workflow documentation](https://github.com/dotnet/android/blob/main/Documentation/workflow/commit-messages.md), and in particular should include:

- [x] Useful description of *why the change is necessary*.
- [ ] Links to issues fixed
- [ ] Unit tests

No issue is closed by this relocation. Existing Android tests compile; on-device test execution is blocked by the local SDK prerequisites described above.
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