From eef70d38f5a9411259efe514807631e9a282bbac Mon Sep 17 00:00:00 2001 From: Dimo Date: Thu, 8 Jan 2026 12:24:32 +0100 Subject: [PATCH] Fix recursive ExceptionObjHolderImpl crash in exception reporting Add re-entrancy guards and exception handling to prevent crashes when exceptions are thrown during exception conversion or Crashlytics reporting. - Add atomic guard in unhandled exception hook wrapper - Add re-entrancy protection in Crashlytics reporter - Add try-catch fallback in exception conversion to NSException - Prevent recursive termination handler invocation Fixes crash: Fatal Exception: (anonymous namespace)::ExceptionObjHolderImpl --- .../CrashlyticsNSExceptionKtReporter.swift | 16 ++++++++++++++++ .../kmp/nsexceptionkt/core/NSException.kt | 10 +++++----- .../nsexceptionkt/core/UnhandledExceptionHook.kt | 10 ++++++++-- 3 files changed, 29 insertions(+), 7 deletions(-) diff --git a/NSExceptionKtCrashlytics/CrashlyticsNSExceptionKtReporter.swift b/NSExceptionKtCrashlytics/CrashlyticsNSExceptionKtReporter.swift index 5d5a7da..6b52e0e 100644 --- a/NSExceptionKtCrashlytics/CrashlyticsNSExceptionKtReporter.swift +++ b/NSExceptionKtCrashlytics/CrashlyticsNSExceptionKtReporter.swift @@ -24,6 +24,8 @@ private class CrashlyticsNSExceptionKtReporter: NSExceptionKtReporter { private let crashlytics: Crashlytics private let causedByStrategy: CausedByStrategy + private static var isReporting = false + private static let lock = NSLock() public var requiresMergedException: Bool { causedByStrategy == .append } @@ -33,6 +35,20 @@ private class CrashlyticsNSExceptionKtReporter: NSExceptionKtReporter { } public func reportException(_ exceptions: [NSException]) { + Self.lock.lock() + guard !Self.isReporting else { + Self.lock.unlock() + return + } + Self.isReporting = true + Self.lock.unlock() + + defer { + Self.lock.lock() + Self.isReporting = false + Self.lock.unlock() + } + if causedByStrategy == .logNonFatal { for exception in exceptions.reversed().dropLast() { crashlytics.record(exceptionModel: .init(exception)) diff --git a/nsexception-kt-core/src/commonMain/kotlin/com/rickclephas/kmp/nsexceptionkt/core/NSException.kt b/nsexception-kt-core/src/commonMain/kotlin/com/rickclephas/kmp/nsexceptionkt/core/NSException.kt index b9b8740..9e574e2 100644 --- a/nsexception-kt-core/src/commonMain/kotlin/com/rickclephas/kmp/nsexceptionkt/core/NSException.kt +++ b/nsexception-kt-core/src/commonMain/kotlin/com/rickclephas/kmp/nsexceptionkt/core/NSException.kt @@ -38,7 +38,7 @@ public fun addReporter( * of the [causes][Throwable.cause] will be appended, else causes are ignored. */ @OptIn(UnsafeNumber::class, ExperimentalForeignApi::class) -internal fun Throwable.asNSException(appendCausedBy: Boolean = false): NSException { +internal fun Throwable.asNSException(appendCausedBy: Boolean = false): NSException = try { val returnAddresses = getFilteredStackTraceAddresses().let { addresses -> if (!appendCausedBy) return@let addresses addresses.toMutableList().apply { @@ -46,10 +46,10 @@ internal fun Throwable.asNSException(appendCausedBy: Boolean = false): NSExcepti addAll(cause.getFilteredStackTraceAddresses(true, addresses)) } } - }.map { - NSNumber(unsignedInteger = it.convert()) - } - return ThrowableNSException(name, getReason(appendCausedBy), returnAddresses) + }.map { NSNumber(unsignedInteger = it.convert()) } + ThrowableNSException(name, getReason(appendCausedBy), returnAddresses) +} catch (e: Throwable) { + ThrowableNSException(name, message, emptyList()) } /** diff --git a/nsexception-kt-core/src/commonMain/kotlin/com/rickclephas/kmp/nsexceptionkt/core/UnhandledExceptionHook.kt b/nsexception-kt-core/src/commonMain/kotlin/com/rickclephas/kmp/nsexceptionkt/core/UnhandledExceptionHook.kt index b44a522..0724a9b 100644 --- a/nsexception-kt-core/src/commonMain/kotlin/com/rickclephas/kmp/nsexceptionkt/core/UnhandledExceptionHook.kt +++ b/nsexception-kt-core/src/commonMain/kotlin/com/rickclephas/kmp/nsexceptionkt/core/UnhandledExceptionHook.kt @@ -2,6 +2,7 @@ package com.rickclephas.kmp.nsexceptionkt.core import kotlin.experimental.ExperimentalNativeApi import kotlin.concurrent.AtomicReference +import kotlin.concurrent.AtomicInt /** * Wraps the unhandled exception hook such that the provided [hook] is invoked @@ -13,9 +14,14 @@ import kotlin.concurrent.AtomicReference @OptIn(ExperimentalNativeApi::class) internal fun wrapUnhandledExceptionHook(hook: (Throwable) -> Unit) { val prevHook = AtomicReference(null) + val isReporting = AtomicInt(0) val wrappedHook: ReportUnhandledExceptionHook = { - hook(it) - prevHook.value?.invoke(it) + if (isReporting.compareAndSet(0, 1)) { + try { + hook(it) + } catch (e: Throwable) {} + prevHook.value?.invoke(it) + } terminateWithUnhandledException(it) } prevHook.value = setUnhandledExceptionHook(wrappedHook)