From df69229739e83e37a2f72b2c1e6d495e79dc651a Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Sun, 12 Jul 2026 00:05:30 +0200 Subject: [PATCH 1/2] [Mono.Android] Handle JNI failures during CoreCLR GC bridge processing 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 --- src/native/clr/host/bridge-processing.cc | 28 +++++++++++++++++++ .../include/host/bridge-processing-shared.hh | 6 ++++ 2 files changed, 34 insertions(+) diff --git a/src/native/clr/host/bridge-processing.cc b/src/native/clr/host/bridge-processing.cc index b4ea077a081..5b7e82020db 100644 --- a/src/native/clr/host/bridge-processing.cc +++ b/src/native/clr/host/bridge-processing.cc @@ -174,6 +174,7 @@ bool BridgeProcessingShared::add_reference (jobject from, jobject to) noexcept env->DeleteLocalRef (java_class); env->CallVoidMethod (from, add_method_id, to); + abort_on_pending_java_exception ("A Java exception was thrown by monodroidAddReference during GC bridge processing"sv); return true; } @@ -217,6 +218,18 @@ void BridgeProcessingShared::clear_references (jobject handle) noexcept env->DeleteLocalRef (java_class); env->CallVoidMethod (handle, clear_method_id); + abort_on_pending_java_exception ("A Java exception was thrown by monodroidClearReferences during GC bridge processing"sv); +} + +void BridgeProcessingShared::abort_on_pending_java_exception (std::string_view message) noexcept +{ + if (!env->ExceptionCheck ()) [[likely]] { + return; + } + + env->ExceptionDescribe (); + env->ExceptionClear (); + Helpers::abort_application (LOG_GC, message); } void BridgeProcessingShared::take_global_ref (HandleContext &context) noexcept @@ -230,6 +243,11 @@ void BridgeProcessingShared::take_global_ref (HandleContext &context) noexcept log_weak_to_gref (weak, handle); if (handle == nullptr) { + // A null result normally means the weak reference's target was collected by the Java GC. + // However, NewGlobalRef can also fail (returning null with a pending exception) when the VM + // is out of memory or the global reference table is full. Treating that as "collected" would + // tear down a live peer, so distinguish the two and fail fast on a genuine failure. + abort_on_pending_java_exception ("Failed to promote a weak global reference to a global reference during GC bridge processing"sv); log_weak_ref_collected (weak); } @@ -249,6 +267,16 @@ void BridgeProcessingShared::take_weak_global_ref (const HandleContext &context) log_take_weak_global_ref (handle); jobject weak = env->NewWeakGlobalRef (handle); + if (weak == nullptr) [[unlikely]] { + // `handle` is a valid strong global reference, so NewWeakGlobalRef only returns null when the + // VM is out of memory. Continuing would delete the strong reference below and lose the object + // (a later NewGlobalRef of a null weak reference would look like a collected peer), so fail fast. + if (env->ExceptionCheck ()) { + env->ExceptionDescribe (); + env->ExceptionClear (); + } + Helpers::abort_application (LOG_GC, "Failed to create a weak global reference during GC bridge processing"sv); + } log_weak_gref_new (handle, weak); context.control_block->handle = weak; diff --git a/src/native/clr/include/host/bridge-processing-shared.hh b/src/native/clr/include/host/bridge-processing-shared.hh index 0e0a9a6244c..7d051dc1aa1 100644 --- a/src/native/clr/include/host/bridge-processing-shared.hh +++ b/src/native/clr/include/host/bridge-processing-shared.hh @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include @@ -51,6 +52,11 @@ private: void clear_references_if_needed (const HandleContext &context) noexcept; void clear_references (jobject handle) noexcept; + // If a Java exception is pending on `env`, describe it, clear it, and abort. Bridge + // processing has no safe way to recover from an exception thrown by a peer's reference + // callbacks, and leaving an exception pending would make subsequent JNI calls undefined. + void abort_on_pending_java_exception (std::string_view message) noexcept; + void log_missing_add_references_method (jclass java_class) noexcept; void log_missing_clear_references_method (jclass java_class) noexcept; void log_weak_to_gref (jobject weak, jobject handle) noexcept; From d5e5e4037be11c1bd43b3273461274fdd23dc22a Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Sun, 12 Jul 2026 00:12:21 +0200 Subject: [PATCH 2/2] [Mono.Android] Reuse abort_on_pending_java_exception in take_weak_global_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 --- src/native/clr/host/bridge-processing.cc | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/native/clr/host/bridge-processing.cc b/src/native/clr/host/bridge-processing.cc index 5b7e82020db..7029b391c8a 100644 --- a/src/native/clr/host/bridge-processing.cc +++ b/src/native/clr/host/bridge-processing.cc @@ -271,11 +271,10 @@ void BridgeProcessingShared::take_weak_global_ref (const HandleContext &context) // `handle` is a valid strong global reference, so NewWeakGlobalRef only returns null when the // VM is out of memory. Continuing would delete the strong reference below and lose the object // (a later NewGlobalRef of a null weak reference would look like a collected peer), so fail fast. - if (env->ExceptionCheck ()) { - env->ExceptionDescribe (); - env->ExceptionClear (); - } - Helpers::abort_application (LOG_GC, "Failed to create a weak global reference during GC bridge processing"sv); + // The OOM failure raises a pending exception; abort unconditionally in case it somehow did not. + constexpr std::string_view failure = "Failed to create a weak global reference during GC bridge processing"sv; + abort_on_pending_java_exception (failure); + Helpers::abort_application (LOG_GC, failure); } log_weak_gref_new (handle, weak);