tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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:
Mmobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/db/CrashEntity.kt | 19+++++++++++++++----
Mmobile/android/android-components/components/lib/crash/src/test/java/mozilla/components/lib/crash/CrashReporterTest.kt | 43+++++++++++++++++++++++++++++++++++++++++++
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(), + ) +}