Skip to content

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

Open
simonrozsival wants to merge 3 commits into
mainfrom
simonrozsival-flaky-android-test
Open

simonrozsival wants to merge 3 commits into
mainfrom
simonrozsival-flaky-android-test

Conversation

@simonrozsival

@simonrozsival simonrozsival commented Sep 12, 2026

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. When a distinct registered peer wins, release the unreturned alias's JNI reference with DisposeUnlessReferenced() before returning the winner. 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, and returned typed aliases remain usable.

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 normal peer registration, incompatible registered types, and concurrent array marshaling returning the registered winner. Retain both constructed peers in the reentrant and concurrent regressions and assert that only the returned winner has a valid JNI reference before test cleanup. The low-level concurrent GetPeer() regression verifies that both returned aliases remain usable.

Validation

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

  • Original identity repro: 10/10 failures before the identity fix; 10/10 passes after, at the same identity assertion.
  • New alias-release assertion fails on the original PR commit: the unreturned alias still has a valid JNI reference. It passes with the cleanup fix.
  • Current CoreCLR (llvm-ir): 10 passed, 3 trimmable-only tests skipped.
  • Current CoreCLR (trimmable): 10 passed, 3 reflection-only tests skipped.
  • Current Mono: 9 passed.
  • Current NativeAOT (trimmable): 10 passed, 3 reflection-only tests skipped.

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.

Comment thread src/Mono.Android/Java.Lang/Object.cs Outdated

@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

Comment thread src/Mono.Android/Java.Lang/Object.cs Outdated
A competing registered peer can replace the result of GetObject(),
leaving the newly constructed alias holding a global JNI reference
until finalization.  Release the distinct unreturned alias with
DisposeUnlessReferenced() before returning the compatible winner.

Retain created peers in the reentrant and concurrent regressions and
assert that only the returned winner keeps a valid JNI reference.
Cover normal registration, requested-type aliases, and low-level
GetPeer() results to preserve the lifetime of peers still returned.

Addresses both discarded-alias review threads on #12772.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@simonrozsival

Copy link
Copy Markdown
Member Author

/review

@github-actions

github-actions Bot commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

Android PR Reviewer completed successfully!

Warning

Firewall blocked 1 domain

The following domain was blocked by the firewall during workflow execution:

  • azcliprod.blob.core.windows.net

To allow these domains, add them to the network.allowed list in your workflow frontmatter:

network:
  allowed:
    - defaults
    - "azcliprod.blob.core.windows.net"

See Network Configuration for more information.

Generated by Android PR Reviewer for #12772

@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 post-creation recheck now preserves managed identity and promptly releases a discarded alias, and the regression coverage exercises reentrant, concurrent, normal, and incompatible-type paths. One behavioral regression remains: the new compatible-peer fast path bypasses the value manager's disposed-state guard.

CI is green: all reported dotnet-android jobs and license/cla completed successfully.

Warning

Firewall blocked 1 domain

The following domain was blocked by the firewall during workflow execution:

  • azcliprod.blob.core.windows.net

To allow these domains, add them to the network.allowed list in your workflow frontmatter:

network:
  allowed:
    - defaults
    - "azcliprod.blob.core.windows.net"

See Network Configuration for more information.

Generated by Android PR Reviewer for #12772 · copilot · gpt56 · 112.2 AIC · ⌖ 11.2 AIC · ⊞ 21K
Comment /review to run again

return null;

var r = JniEnvironment.Runtime.ValueManager.GetPeer (new JniObjectReference (handle), type);
var peer = PeekObject (handle, type);

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.

🤖 ⚠️ Error handling — Replacing JniValueManager.GetPeer() with PeekObject() removes the EnsureNotDisposed() check that GetPeer() performed before every lookup. On the compatible-peer fast path, GetObject() can now return a peer after the value manager/runtime has been disposed, while the miss path still throws from CreatePeer(). Please preserve the disposed-state guard before this first peek without adding a second registry lookup to the fast path.

Rule: Preserve disposed-state checks

GetObject() now looks up peers directly instead of going through the
GetPeer() disposal check.  Guard PeekPeer() in all three Android value
managers so the fast path still rejects a disposed manager before
accessing its registry, without adding another registry lookup.

Use the runtime factory to create isolated managers for cache-hit,
cache-miss, and invalid-reference disposal coverage.  Keep the active
runtime untouched and verify that its registered peer remains usable.

The two disposal cases fail before this change and pass with the fix.
Focused peer and constructor coverage passes on CoreCLR llvm-ir,
CoreCLR trimmable, Mono, and NativeAOT.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.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.

2 participants