Skip to content

[Mono.Android] Return registered peers after activation - #12772

Open
simonrozsival wants to merge 1 commit into
mainfrom
simonrozsival-flaky-android-test
Open

simonrozsival wants to merge 1 commit into
mainfrom
simonrozsival-flaky-android-test

Conversation

@simonrozsival

Copy link
Copy Markdown
Member

Related to #10973; follows the deterministic reproduction and investigation.

Java.Lang.Object.GetObject() can return an unregistered alias when activation reenters lookup and registers another compatible peer first. A caller can cache that alias while subsequent array marshaling returns the registered peer: the Java object is the same, but managed reference identity differs.

Recheck for a compatible registered peer after creation. The already-registered fast path still performs one lookup, and the shared JniValueManager.GetPeer() / CreatePeer() implementations and their alias contracts remain unchanged. A registered peer incompatible with the requested managed type is not substituted.

Regression coverage

The new GetObject_ReentrantActivation_PreservesRoundtripIdentity test uses one plain Java object and a synchronous, test-owned activation callback to force the ordering. It needs no worker threads, sleeps, forced GC, or application startup. The existing GetObjectArray identity assertion is not weakened.

Also cover incompatible registered types and update concurrent array marshaling to require the registered winner. Retain both constructed peers in that test so finalization of the unreturned alias cannot race its disposal assertion.

Validation

Source-built Release SDK, API 35 arm64-v8a emulator:

  • Deterministic repro: 10/10 failures before the fix; 10/10 passes after, at the same identity assertion.
  • CoreCLR (llvm-ir): 9 passed, 3 trimmable-only tests skipped.
  • CoreCLR (trimmable): 9 passed, 3 reflection-only tests skipped.
  • Mono: 8 passed.
  • NativeAOT (trimmable): 9 passed, 3 reflection-only tests skipped.
  • After tightening concurrent-test cleanup, all six JNIObjectArray tests passed on both trimmable CoreCLR and NativeAOT.

This fixes a demonstrated mechanism producing the reported symptom. Keep #10973 open: the exact startup interleaving in CI build 1589170 has not been established, and that failure was x64 rather than the locally tested arm64.


  • Useful description of why the change is necessary.
  • Links to issues fixed (related tracker; intentionally not auto-closing).
  • Unit tests

Java.Lang.Object.GetObject() can return an unregistered alias when
activation reenters lookup and registers another compatible peer first.
A caller can cache that alias while array marshaling returns the
registered peer, breaking managed reference identity.

Recheck the compatible registered peer after creation without adding a
lookup to the already-registered fast path.  Preserve the low-level
GetPeer()/CreatePeer() alias contracts and type-specific aliases.

Add a deterministic, single-threaded regression and incompatible-type
coverage.  Update concurrent array coverage to require the registered
winner and retain both created peers for deterministic cleanup.

Related to #10973.  The precise x64 CI startup interleaving
still needs confirmation.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI lite review requested due to automatic review settings September 12, 2026 08:38
@simonrozsival

Copy link
Copy Markdown
Member Author

/review

@github-actions

github-actions Bot commented Sep 12, 2026

Copy link
Copy Markdown
Contributor

Android PR Reviewer completed successfully!

Generated by Android PR Reviewer for #12772

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

🟡 Changes recommended

Unresolved peer disposal and disposed-state handling issues remain.

Get a fresh assessment by requesting another Copilot review.

Review tier: Lite
Findings: 1 Medium severity

Open findings (1)
What changed in this PR

This PR fixes JNI peer identity mismatches caused by reentrant activation during object lookup.

Changes:

  • Rechecks registered peers after peer creation.
  • Adds reentrant and incompatible-type regression tests.
  • Tightens concurrent marshaling assertions and cleanup.

Review findings:

  • Object.csmoderate, 2 votes: dispose the losing alias before replacement.
  • Object.csmoderate, 1 vote: preserve the disposed-state guard.
  • PeerIdentityTests.csnit, 1 vote: reorder assertions for better diagnostics.
File Summary
tests/​Mono.Android-Tests/​Mono.Android-Tests/​Mono.Android.NET-Tests.csproj Includes the new test source.
tests/​Mono.Android-Tests/​Mono.Android-Tests/​java/​net/​dot/​android/​test/​ReentrantLookupPeer.java Adds the Java reentrant-lookup fixture.
tests/​Mono.Android-Tests/​Mono.Android-Tests/​Java.Interop/​TrimmableTypeMapRuntimeCoverageTests.cs Tracks constructed peers for cleanup.
tests/​Mono.Android-Tests/​Mono.Android-Tests/​Java.Interop/​PeerIdentityTests.cs Adds peer identity and incompatible-type coverage.
tests/​Mono.Android-Tests/​Mono.Android-Tests/​Android.Runtime/​JnienvArrayMarshaling.cs Tightens concurrent identity assertions and cleanup.
src/​Mono.Android/​Java.Lang/​Object.cs Rechecks registered peers after activation.

peer = JniEnvironment.Runtime.ValueManager.CreatePeer (ref reference, JniObjectReferenceOptions.Copy, type);
if (peer != null) {
// Activation can register a competing peer before this one is constructed.
peer = PeekObject (handle, type) ?? peer;

@github-actions github-actions Bot 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.

⚠️ Needs Changes

Findings: 0 errors · 1 warning · 0 suggestions

The re-peek correctly preserves managed identity for both re-entrant and concurrent activation, and the incompatible-type regression coverage protects the intended type-filtering behavior. The newly created losing alias still owns a global JNI reference when it is discarded, however, so its cleanup should be explicit rather than left to finalization.

CI build 1594059 is still running; all completed checks reported success at review time.

Generated by Android PR Reviewer for #12772 · copilot · gpt56 · 110 AIC · ⌖ 7.12 AIC · ⊞ 22.6K
Comment /review to run again

peer = JniEnvironment.Runtime.ValueManager.CreatePeer (ref reference, JniObjectReferenceOptions.Copy, type);
if (peer != null) {
// Activation can register a competing peer before this one is constructed.
peer = PeekObject (handle, type) ?? peer;

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.

🤖 ⚠️ JNI references — When this second peek finds a competing registered peer, assigning it to peer drops the only managed reference to the freshly created alias. ConstructPeer has already promoted that alias to a global JNI reference, while registration deliberately rejected it, so the global remains alive until finalization. Re-entrant or concurrent activation can therefore retain Java object graphs and consume global references nondeterministically. Please keep the created peer in a separate local and call DisposeUnlessReferenced() before returning the distinct registered winner, then assert that cleanup in the regression test.

Rule: JNI reference lifecycle

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.

2 participants