Hacktoberfest 2026:メンテナが10月に向けて印を付けた、オープンで初心者向けの issue。 Hacktoberfest の issue を見る

Investigate JNI reference ownership leaks and GC-bridge leak-check reliability

オープン
#13,010 コメント 0 件 リアクション 0 件 担当者 0 名 GitHub で見る

メンテナーはふだん 1 日以内に返信

まだ誰も着手していません。

評価

難易度
4/5
見積もり時間
3〜5日
初心者へのやさしさ
50/100
issue の種類
バグ
明瞭さ
おおむね明確
活発さ
活発
技術スタック
csharp
領域
backend

調査の方向性

Start by examining finalization order in JavaMarshalRegisteredPeers.cs, JavaObject.cs, and JavaException.cs to ensure native control blocks are freed. Then review exception-path cleanup in Object.SetHandle(), Throwable.SetHandle(), and generated proxy activation to fix transferred-reference leaks. Add regression tests for each scenario and verify GC-bridge processing completion in TrimmableTypeMapValueManager.cs.

索引モデルが issue の本文から書いたものです。

説明

needs-triage
Android framework version

net11.0-android (Preview)

Affected platform version

Source audit of dotnet/android main at commit 2a20d9989a (2026-10-06). Current Android initialization uses TrimmableTypeMapValueManager for CoreCLR/NativeAOT. Findings in the shared reflection implementation are listed separately and are not assumed to explain current Android failures.

Description

Intermittent JNI global-reference leak-check failures remain a concern following recent improvements. This issue tracks source-level ownership findings and measurement concerns, to be investigated and fixed in separate, non-stacked PRs.

Evidence level: This is a source audit, not a runtime reproduction of the intermittent failure. The findings below must not be treated as one established root cause. In particular, native control-block leakage is distinct from GREF leakage, and incomplete bridge processing is a possible measurement problem rather than proof that a failure is harmless.

1. Finalization does not free the native peer control block

JavaMarshalRegisteredPeers.FinalizePeer() clears the peer reference before calling value.Finalized() in both branches:

Finalized() sets the internal Disposed state. However, SetPeerReference(default) frees the NativeMemory.AllocZeroed control block only when that state is already set. The finalization sequence therefore leaves the block allocated, whereas explicit disposal marks the peer disposed before clearing it. The block is 16 bytes on 64-bit targets. An initially invalid peer can also allocate a block while its reference is cleared during finalization.

Classification: Source-backed unmanaged-memory lifetime defect; not itself an unreleased GREF and not visible in the GREF count. Validate actual finalization and callback ordering before changing it; blindly swapping calls could change Dispose(false) behavior.

2. Transferred-reference cleanup is skipped on exceptions

These APIs delete the transferred input handle only after fallible work, without a finally:

An exception can strand an input passed with TransferGlobalRef; collection of a partially constructed wrapper releases its own reference, not necessarily that original input. GetPeer() calls CreatePeer() with Copy, so the latter's transfer cleanup does not consume the raw transferred input. Internal typemap creation normally passes DoNotTransfer | DoNotRegister; distinguish that path from explicitly transferring through the proxy API.

Classification: Source-backed exception-path ownership gaps; no evidence yet that successful intermittent activation loops take these paths. Fix ownership boundaries consistently and avoid double deletion through nested layers.

3. Leak checks do not establish completed GC-bridge processing

The baseline uses CollectPeers() while the post-batch sample uses only CollectGarbage(). The value manager's WaitForGCBridgeProcessing() is intentionally a no-op; CollectPeers() only drains already-collected contexts. Native bridge work runs on a separate thread and temporarily converts strong globals to weak globals before promoting survivors.

Classification: Credible collection/measurement timing concern, not a reproduced explanation. Existing JavaMarshalGCBridgeTests already wait for a changed BridgeProcessingGeneration and an expected outcome. Establish completed processing/eventual convergence without weakening the real-leak assertion or adding unbounded waits.

4. Reflection activation failure suppresses cleanup of a potentially constructed peer

ReflectionJniValueManager.TryCreatePeerInstance() suppresses finalization whenever construction does not complete. A derived activation constructor can throw after its base constructor has acquired a GREF, but that failure path does not dispose the peer before suppressing its finalizer.

Classification: Shared reflection/host-JVM exception-path leak candidate. Current Android initialization selects the trimmable value manager, so this is not assumed to cause current Android leak-test failures. Verify with a throwing activation constructor and host-JVM reference accounting.

Additional registration discrepancy: Throwable ignores DoNotRegister

Throwable.SetHandle() always uses Copy rather than preserving JniHandleOwnership.DoNotRegister, unlike Object.SetHandle(). The typemap's RegisterCreatedPeer() relies on withholding registration until the Replaceable state is set. This discrepancy needs Throwable alias/reentrant-activation coverage before its consequences can be attributed to leaks.

Classification: Source-backed registration-contract discrepancy; no demonstrated permanent GREF leak.

Additional test-only native reference retention

Both Android timing helper and Java.Interop timing helper overwrite static Object_class with NewGlobalRef() during foo_init() without releasing a previous value. Repeated initialization would lose the previous reference. These raw JNI calls bypass managed reference accounting and should not be confused with a failing managed GREF-count assertion.

Scope and related work

The first pass covered peer construction/disposal/finalization, registry reconciliation, generated proxy/UCO activation, direct runtime GREF acquisition, class/constructor/remapping caches, array marshaling, proxies, and native hosts. It is not a completed whole-codebase retention audit.

Normal constructor-cache publication disposes losing candidates, and normal existing-reference construction releases the old global when replacing it. Those paths were not identified as another unconditional duplicate-GREF acquisition.

Related context: #10989, #11101, #11201, #10973; recent activation/alias work #12772 and leak-test isolation #12716 and #12709. Historical fixes should not be taken as proof of the current failure mechanism.

Steps to Reproduce

No end-to-end reproduction of the intermittent failure was performed during this audit. Proposed focused regression scenarios:

  1. Exercise actual finalization of JavaObject and JavaException peers, including initially invalid/partially constructed peers; verify native control-block release independently of GREF deletion.
  2. Warm caches, transfer a fresh global reference into a lookup or activation that throws, and verify the original acquisition is paired with deletion. Cover Object, Throwable, and supported generated-proxy entry points.
  3. Repeatedly run isolated Java-side activation/class-lookup leak checks while recording strong/weak reference counts, bridge generations, and matched acquisition/deletion logs. Distinguish sustained growth from delayed release and in-progress bridge transitions.
  4. On the reflection/host-JVM path, activate a peer whose constructor throws after base construction; verify GREF/registry cleanup after collection.
  5. Add Throwable registration-order coverage and determine whether repeated native timing initialization is reachable before treating the additional observations as runtime leak causes.

Each fix should have a regression that fails before the change, preserve successful-path ownership/activation semantics, and be validated on the relevant runtime. Follow-up PRs should reference this issue without automatically closing the broader investigation while unrelated findings remain unresolved.

Did you find any workaround?

No general workaround established. Explicit disposal normally releases peer references and control blocks, but it does not establish that exceptional transferred-input ownership is correct. Do not relax leak thresholds or assume all failures are GC timing noise.

Relevant log output

N/A — no new runtime reproduction or GREF logs collected during this source audit.

主要言語
C#
スター
2.1k
フォーク
581
平均マージ
1日 17時間
マージ済み PR(30日)
237

環境構築

  • Dockerfile・Docker Compose ファイルなし
  • プルリクエストのテンプレートあり
  • コントリビューションガイドなし

はじめの一歩

  1. issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
  2. 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
  3. リポジトリをフォークし、ブランチを切って変更します。
  4. issue 番号を参照したプルリクエストを送ります。

dotnet/android のほかの issue

dotnet/android の issue をすべて見る

似ている issue

C# の issue をもっと見る

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。