[Mono.Android] Handle JNI failures during CoreCLR GC bridge processing#12041
Conversation
Bridge processing in the CoreCLR/NativeAOT shared native host (`src/native/clr/host/bridge-processing.cc`) called several JNI functions without checking for failure, which could leave a Java exception pending (making subsequent JNI calls undefined) or silently mis-handle an out-of-memory condition as a collected object. Follow the pattern already used by `GCBridge::trigger_java_gc` and fail fast with a clear diagnostic instead. * `add_reference` / `clear_references`: check for a pending exception after `CallVoidMethod` for `monodroidAddReference` / `monodroidClearReferences`. The generated `monodroidAddReference` lazily allocates and appends to an `ArrayList`, so it can throw `OutOfMemoryError` under the very memory pressure that triggers a GC. Previously the exception was left pending (undefined behaviour on the next JNI call) and the keep-alive edge was silently dropped, which can lead to premature collection of a still-referenced peer. Now the exception is described, cleared, and the process aborts. * `take_weak_global_ref`: `NewWeakGlobalRef` of a valid strong global reference only returns null when the VM is out of memory. The old code stored the null and then deleted the strong reference, losing the object (a later `NewGlobalRef` of a null weak reference looks like a collected peer). Now a null result fails fast. * `take_global_ref`: a null `NewGlobalRef` result normally means the weak reference's target was collected, but it can also indicate a resource failure (null with a pending exception). Treating the latter as "collected" would tear down a live peer, so the two cases are now distinguished and a genuine failure aborts. These are fault paths (out-of-memory / a peer callback throwing); normal bridge processing is unaffected. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 8e3188a8-82e3-4a21-bf4c-257b7ad9752c
There was a problem hiding this comment.
Pull request overview
Hardens CoreCLR/NativeAOT GC bridge processing against JNI failure modes by failing fast (with diagnostics) when peer callbacks or reference promotions leave a pending Java exception or indicate an OOM/resource failure. This prevents undefined JNI behavior and avoids incorrectly treating resource failures as “object collected,” which could tear down still-live peers.
Changes:
- Add
abort_on_pending_java_exception()helper and use it aftermonodroidAddReference/monodroidClearReferencesJNI calls. - Distinguish “weak target collected” vs “JNI failure w/ pending exception” when promoting weak refs via
NewGlobalRef. - Fail fast when
NewWeakGlobalRefreturns null (OOM/resource failure) to avoid losing the object.
Show a summary per file
| File | Description |
|---|---|
| src/native/clr/include/host/bridge-processing-shared.hh | Adds a shared helper declaration for detecting/logging pending Java exceptions during bridge processing. |
| src/native/clr/host/bridge-processing.cc | Implements fail-fast exception handling and tightens null-ref handling for global/weak-global ref transitions. |
Copilot's findings
- Files reviewed: 2/2 changed files
- Comments generated: 1
…bal_ref Address PR review feedback: take_weak_global_ref open-coded the ExceptionCheck/Describe/Clear sequence instead of reusing the new abort_on_pending_java_exception helper. Route the pending-exception path through the helper so all JNI-failure abort paths log and clear exceptions consistently, and keep the unconditional abort as a fallback for the (spec-permitted but unexpected) case where NewWeakGlobalRef returns null without a pending exception. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 8e3188a8-82e3-4a21-bf4c-257b7ad9752c
|
/review |
|
✅ Android PR Reviewer completed successfully! |
There was a problem hiding this comment.
🤖 Code Review — ✅ LGTM
Verdict: ✅ LGTM · Issues: 0 ❌ · 0
This is a focused, well-reasoned hardening of the CoreCLR/NativeAOT GC bridge JNI paths. Each CallVoidMethod / NewGlobalRef / NewWeakGlobalRef failure is now handled explicitly and fails fast rather than leaving a pending exception (UB on the next JNI call) or silently mishandling an OOM as a collected peer. The code follows the existing abort_on_pending_java_exception convention, the comments explain the why clearly, and the asymmetry between take_global_ref (null may be legitimate collection) and take_weak_global_ref (null always = failure) is correct and documented.
Verified
add_reference/clear_references— exception check placed afterCallVoidMethod;ExceptionDescribe/ExceptionClearbefore abort matchesGCBridge::trigger_java_gc. ✔️take_global_ref— abort only on a genuine pending exception; genuine collection (null, no exception) still stores null → marked collected, andDeleteWeakGlobalRef(weak)still runs. No leak. ✔️take_weak_global_ref— aborts beforeDeleteGlobalRef(handle), so the strong reference is never lost on OOM. ✔️- Header:
abort_on_pending_java_exceptiondeclaredprivate,<string_view>include added. ✔️ - CI: all 42 checks green on Azure DevOps
dnceng-public.
Notes
- 2 inline 💡 suggestions posted (documentation of the null-handle "collected" convention; a robustness note on the double-abort). Both are optional.
- Fault paths (OOM / throwing peer callback) are, by nature, not exercisable without fault injection — the PR is transparent about this and the manual 915-test CoreCLR run with
debug.mono.log=gc(12 bridge invocations, no CheckJNI errors) is reasonable validation for the non-fault path.
Nice, careful work. 👍
Generated by Android PR Reviewer for #12041 · 75.2 AIC · ⌖ 13.2 AIC · ⊞ 6.8K
Comment /review to run again
Comments that could not be inline-anchored
src/native/clr/host/bridge-processing.cc:254
🤖 💡 Documentation — The new fail-fast code makes the two handle == nullptr cases explicit (genuine collection vs. resource failure), which is great. Since a reader now sees abort_on_pending_java_exception right next to context.control_block->handle = handle; where handle may legitimately be nullptr, consider a one-line comment noting that a null handle here encodes the "collected" state (what is_collected() later keys off), and that handle_type is intentionally still set to …
src/native/clr/host/bridge-processing.cc:277
🤖 💡 Native C++ — Nice defensive handling: abort_on_pending_java_exception aborts when the OOM exception is pending, and the unconditional Helpers::abort_application below covers the (spec-unlikely) case where NewWeakGlobalRef returned null without raising one. Since both branches terminate, the strong handle is never lost — correct. One small robustness thought: the unconditional abort will call abort_application with the same message even after abort_on_pending_java_exception…
Note
This pull request was created with the assistance of GitHub Copilot. The code and description were AI-generated and reviewed by the author.
Summary
Bridge processing in the CoreCLR/NativeAOT shared native host
(
src/native/clr/host/bridge-processing.cc) called several JNI functionswithout checking for failure. This could leave a Java exception pending —
making the next JNI call undefined behaviour (a CheckJNI abort on debug
builds) — or silently mis-handle an out-of-memory condition as a collected
object, tearing down a peer that is still alive. This follows the pattern
already used by
GCBridge::trigger_java_gcand fails fast with a cleardiagnostic.
Changes
add_reference/clear_references— check for a pending exceptionafter the
CallVoidMethodinvocations ofmonodroidAddReference/monodroidClearReferences. The generatedmonodroidAddReferencelazilyallocates and appends to an
ArrayList, so it can throwOutOfMemoryErrorunder the same memory pressure that triggers a GC.Previously the exception was left pending (undefined behaviour on the next
JNI call) and the keep-alive edge was silently dropped, risking premature
collection of a still-referenced peer.
take_weak_global_ref—NewWeakGlobalRefof a valid strong globalreference only returns null when the VM is out of memory. The old code
stored the null and then deleted the strong reference, losing the object
(a subsequent
NewGlobalRefof a null weak reference looks like acollected peer). A null result now fails fast.
take_global_ref— a nullNewGlobalRefresult normally means theweak reference's target was collected, but it can also indicate a resource
failure (null with a pending exception). Treating the latter as "collected"
would tear down a live peer, so the two cases are now distinguished.
These are fault paths (out-of-memory, or a peer callback throwing); normal
bridge processing is unaffected.
Testing
Built the CoreCLR
android-arm64runtime with these changes and ran the fullMono.Android.NET-Testssuite on an API 36arm64-v8aemulator with-p:UseMonoRuntime=false(CoreCLR):debug.mono.log=gc, the GC bridge ran 12 times during the suite,exercising the hardened
add_reference/clear_references/take_global_ref/take_weak_global_refpaths, with no CheckJNI / JNIerrors and none of the new fail-fast paths triggered (CheckJNI is active
for the debuggable test app).
JavaObjectTest.UnregisterFromRuntime,a registered-peer count assertion) flaked once under heavy GC-spew logging
and passed on a clean run; it is not affected by these changes.
The new abort paths are only reachable under out-of-memory / a throwing peer
callback and were not exercised by fault injection.
Note
This touches the same file as #12040 (GC bridge JNI-overhead cleanup); the two
changes are independent but edit adjacent code, so a trivial merge conflict is
expected depending on merge order.