[Mono.Android] Handle JNI failures during CoreCLR GC bridge processing#12041
Open
simonrozsival wants to merge 2 commits into
Open
[Mono.Android] Handle JNI failures during CoreCLR GC bridge processing#12041simonrozsival wants to merge 2 commits into
simonrozsival wants to merge 2 commits into
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
Contributor
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.