Ignore platform finalizer-watchdog crashes in crash reports

BinderInternal$GcWatcher.finalize() TimeoutExceptions are a known
AOSP issue raised by the finalizer watchdog on the FinalizerDaemon
thread. They contain no app frames and cannot be caught or prevented
by app code, so the report only nags users with an unactionable
"send crash report?" prompt.

Extend the existing OOM junk filter in UnexpectedCrashSaver to drop
TimeoutExceptions whose message or stack matches the finalizer
watchdog path. App-thrown TimeoutExceptions and all other crashes
are still reported.
This commit is contained in:
greenart7c3
2026-07-31 06:55:48 -03:00
parent 0985d4171c
commit 86ab6e86e6
2 changed files with 73 additions and 2 deletions
@@ -20,6 +20,7 @@
*/
package com.greenart7c3.nostrsigner.service.crashreports
import java.util.concurrent.TimeoutException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
@@ -33,12 +34,29 @@ class UnexpectedCrashSaver(
t: Thread,
e: Throwable,
) {
if (e !is OutOfMemoryError) {
// OOM reports are junk
if (!isJunkReport(e)) {
scope.launch {
cache.writeReport(ReportAssembler().buildReport(e))
}
}
defaultUEH!!.uncaughtException(t, e)
}
companion object {
/**
* Crashes we never prompt the user to report because nobody can act on them:
* - OutOfMemoryError: junk reports.
* - Platform finalizer-watchdog timeouts (e.g.
* `BinderInternal$GcWatcher.finalize() timed out after 10 seconds`): a known
* AOSP issue raised on the FinalizerDaemon thread with no app frames. The OS
* kills the process regardless; app code cannot catch, prevent, or fix it.
*/
fun isJunkReport(e: Throwable): Boolean = e is OutOfMemoryError || isPlatformFinalizerTimeout(e)
private fun isPlatformFinalizerTimeout(e: Throwable): Boolean = e is TimeoutException &&
(
e.message?.contains("finalize() timed out") == true ||
e.stackTrace.any { it.className.startsWith("java.lang.Daemons\$FinalizerDaemon") }
)
}
}
@@ -0,0 +1,53 @@
package com.greenart7c3.nostrsigner.service.crashreports
import java.util.concurrent.TimeoutException
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
class UnexpectedCrashSaverTest {
private fun finalizerWatchdogCrash(): TimeoutException {
val e = TimeoutException("com.android.internal.os.BinderInternal\$GcWatcher.finalize() timed out after 10 seconds")
e.stackTrace =
arrayOf(
StackTraceElement("com.android.internal.os.BinderInternal\$GcWatcher", "finalize", "BinderInternal.java", 64),
StackTraceElement("java.lang.Daemons\$FinalizerDaemon", "doFinalize", "Daemons.java", 389),
StackTraceElement("java.lang.Daemons\$FinalizerDaemon", "processReference", "Daemons.java", 369),
StackTraceElement("java.lang.Daemons\$FinalizerDaemon", "runInternal", "Daemons.java", 354),
StackTraceElement("java.lang.Daemons\$Daemon", "run", "Daemons.java", 135),
StackTraceElement("java.lang.Thread", "run", "Thread.java", 1564),
)
return e
}
@Test
fun `binder gc watcher finalizer timeout is junk`() {
assertTrue(UnexpectedCrashSaver.isJunkReport(finalizerWatchdogCrash()))
}
@Test
fun `finalizer daemon stack is junk even without the message`() {
val e = TimeoutException(null as String?)
e.stackTrace =
arrayOf(
StackTraceElement("android.os.BinderProxy", "finalize", "BinderProxy.java", 100),
StackTraceElement("java.lang.Daemons\$FinalizerDaemon", "doFinalize", "Daemons.java", 389),
)
assertTrue(UnexpectedCrashSaver.isJunkReport(e))
}
@Test
fun `out of memory is junk`() {
assertTrue(UnexpectedCrashSaver.isJunkReport(OutOfMemoryError()))
}
@Test
fun `app timeout exception is not junk`() {
assertFalse(UnexpectedCrashSaver.isJunkReport(TimeoutException("socket timed out")))
}
@Test
fun `regular crash is not junk`() {
assertFalse(UnexpectedCrashSaver.isJunkReport(IllegalStateException("boom")))
}
}