commit 975867c77a43530175e82133cfbe13df7c8ab600
parent c35192456a26bb94d66cd75be9f3c32d5db4ddf0
Author: Ted Campbell <tcampbell@mozilla.com>
Date: Thu, 27 Nov 2025 20:08:02 +0000
Bug 2002706 - Don't crash Fenix crashreporter on unserializable exceptions r=android-reviewers,Roger
Not all Throwable are Serializable which was causing the A-C CrashReporter to
itself crash when stashing a crash report in local Room storage. Instead, create
a placeholder Throwable with same stack but encode type as string.
simpler crash recovery
Differential Revision: https://phabricator.services.mozilla.com/D274249
Diffstat:
2 files changed, 58 insertions(+), 4 deletions(-)
diff --git a/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/db/CrashEntity.kt b/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/db/CrashEntity.kt
@@ -12,6 +12,7 @@ import mozilla.components.lib.crash.Crash
import mozilla.components.support.base.ext.getStacktraceAsString
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
+import java.io.NotSerializableException
import java.io.ObjectInputStream
import java.io.ObjectOutputStream
import mozilla.components.concept.base.crash.Breadcrumb as CrashBreadcrumb
@@ -176,11 +177,21 @@ internal fun Crash.toEntity(): CrashEntity {
}
private fun Throwable.serialize(): ByteArray {
- val byteArrayOutputStream = ByteArrayOutputStream()
- ObjectOutputStream(byteArrayOutputStream).use { oos ->
- oos.writeObject(this)
+ return try {
+ val byteArrayOutputStream = ByteArrayOutputStream()
+ ObjectOutputStream(byteArrayOutputStream).use { oos ->
+ oos.writeObject(this)
+ }
+ byteArrayOutputStream.toByteArray()
+ } catch (e: NotSerializableException) {
+ // If throwable isn't serializable, then use a placeholder Throwable with
+ // the same stack and include basic name / message data. This gives us
+ // at least some data to understand these crashes in the wild.
+ val innerMessage = "${javaClass.name}: $message"
+ val altThrowable = CrashReporterUnableToRestoreException(innerMessage)
+ altThrowable.stackTrace = stackTrace.clone()
+ altThrowable.serialize()
}
- return byteArrayOutputStream.toByteArray()
}
private fun ByteArray.deserializeThrowable(): Throwable {
diff --git a/mobile/android/android-components/components/lib/crash/src/test/java/mozilla/components/lib/crash/CrashReporterTest.kt b/mobile/android/android-components/components/lib/crash/src/test/java/mozilla/components/lib/crash/CrashReporterTest.kt
@@ -15,7 +15,10 @@ import mozilla.components.concept.base.crash.Breadcrumb
import mozilla.components.lib.crash.db.CrashDao
import mozilla.components.lib.crash.db.CrashDatabase
import mozilla.components.lib.crash.db.CrashEntity
+import mozilla.components.lib.crash.db.CrashReporterUnableToRestoreException
import mozilla.components.lib.crash.db.CrashType
+import mozilla.components.lib.crash.db.toCrash
+import mozilla.components.lib.crash.db.toEntity
import mozilla.components.lib.crash.service.CrashReporterService
import mozilla.components.lib.crash.service.CrashTelemetryService
import mozilla.components.support.test.any
@@ -27,6 +30,7 @@ import mozilla.components.support.test.robolectric.testContext
import mozilla.components.support.test.rule.MainCoroutineRule
import mozilla.components.support.test.rule.runTestOnMain
import org.junit.After
+import org.junit.Assert.assertArrayEquals
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotEquals
@@ -1433,6 +1437,31 @@ class CrashReporterTest {
assertEquals(crashReporter.findCrashReports(crashIDs).get(0).uuid, crashEntity1.uuid)
assertEquals(crashReporter.findCrashReports(crashIDs).get(1).uuid, crashEntity3.uuid)
}
+
+ @Test
+ fun `Round-trip through CrashEntity preserves (serializable) Throwable info`() {
+ val crash = createUncaughtExceptionCrash()
+
+ val entity = crash.toEntity()
+ val otherCrash = entity.toCrash() as Crash.UncaughtExceptionCrash
+
+ assertEquals(crash.throwable.javaClass, otherCrash.throwable.javaClass)
+ assertEquals(crash.throwable.message, otherCrash.throwable.message)
+ assertArrayEquals(crash.throwable.stackTrace, otherCrash.throwable.stackTrace)
+ }
+
+ @Test
+ fun `Round-trip through CrashEntity for unserializable Throwable preserves stack`() {
+ val crash = createUnserializableUncaughtExceptionCrash()
+ val expectedMessage = "${crash.throwable.javaClass.name}: This exception has a bad field!"
+
+ val entity = crash.toEntity()
+ val otherCrash = entity.toCrash() as Crash.UncaughtExceptionCrash
+
+ assertTrue(otherCrash.throwable is CrashReporterUnableToRestoreException)
+ assertEquals(expectedMessage, otherCrash.throwable.message)
+ assertArrayEquals(crash.throwable.stackTrace, otherCrash.throwable.stackTrace)
+ }
}
private fun createUncaughtExceptionCrash(): Crash.UncaughtExceptionCrash {
@@ -1442,3 +1471,17 @@ private fun createUncaughtExceptionCrash(): Crash.UncaughtExceptionCrash {
ArrayList(),
)
}
+
+private class UnserializableException(
+ @Suppress("unused")
+ val badField: Thread = Thread.currentThread(),
+) : Exception("This exception has a bad field!")
+
+private fun createUnserializableUncaughtExceptionCrash(): Crash.UncaughtExceptionCrash {
+ val throwable = UnserializableException()
+ return Crash.UncaughtExceptionCrash(
+ 0,
+ throwable,
+ ArrayList(),
+ )
+}