kotlinx.coroutines icon indicating copy to clipboard operation
kotlinx.coroutines copied to clipboard

Coroutines Exceptions Violate Serializable Interface

Open ccjernigan opened this issue 2 years ago • 7 comments

OVERVIEW The Java Throwable interface implements Serializable. However, certain coroutines classes such as kotlinx.coroutines.CoroutinesInternalError do not correctly implement Serializable and will cause an exception to be thrown if serialization is attempted.

STEPS TO REPRODUCE

  1. Implement an uncaught exception handler that attempts to Serialize an uncaught exception
  2. Throw an exception from within a coroutine, e.g.
lifecycleScope.launch {
    throw RuntimeException("I will blow up when you try to serialize later")
}

RESULTS Expected Serialization succeeds

Actual Serialization fails because kotlinx.coroutines.CoroutinesInternalError does not correctly implement Serializable and will cause an exception to be thrown if serialization is attempted. (See #76).

The use case where I found this issue is implementing an application-wide UncaughtExceptionHandler under the JVM. In my particular case, I was using the Serializable interface on Throwable to send the exception across process boundaries for logging.

Example stack trace:

     Caused by: java.lang.RuntimeException: Parcelable encountered IOException writing serializable object (name = kotlinx.coroutines.CoroutinesInternalError)
        at android.os.Parcel.writeSerializable(Parcel.java:2165)
        at android.os.Parcel.writeValue(Parcel.java:1931)
        at android.os.Parcel.writeArrayMapInternal(Parcel.java:1023)
        at android.os.BaseBundle.writeToParcelInner(BaseBundle.java:1620)
        at android.os.Bundle.writeToParcel(Bundle.java:1304)
        at android.os.Parcel.writeBundle(Parcel.java:1092)
        at android.content.Intent.writeToParcel(Intent.java:11100)
        at android.app.IActivityManager$Stub$Proxy.broadcastIntentWithFeature(IActivityManager.java:5630)
        at android.app.ContextImpl.sendBroadcast(ContextImpl.java:1177)
2022-06-18 11:01:48.952 20994-20994/com.twofortyfouram.locale.x E/AndroidRuntime:     at android.content.ContextWrapper.sendBroadcast(ContextWrapper.java:479)
        at com.twofortyfouram.analytics.internal.LoggingExceptionHandler.uncaughtException(LoggingExceptionHandler.kt:46)
        at java.lang.ThreadGroup.uncaughtException(ThreadGroup.java:1073)
        at java.lang.ThreadGroup.uncaughtException(ThreadGroup.java:1068)
        at kotlinx.coroutines.CoroutineExceptionHandlerImplKt.handleCoroutineExceptionImpl(CoroutineExceptionHandlerImpl.kt:61)
        at kotlinx.coroutines.CoroutineExceptionHandlerKt.handleCoroutineException(CoroutineExceptionHandler.kt:33)
        at kotlinx.coroutines.DispatchedTask.handleFatalException(DispatchedTask.kt:146)
        at kotlinx.coroutines.internal.DispatchedContinuationKt.resumeCancellableWith(DispatchedContinuation.kt:383)
        at kotlinx.coroutines.intrinsics.CancellableKt.startCoroutineCancellable(Cancellable.kt:30)
        	... 33 more
     Caused by: java.io.NotSerializableException: kotlinx.coroutines.StandaloneCoroutine
        at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1240)
        at java.io.ObjectOutputStream.writeArray(ObjectOutputStream.java:1434)
        at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1230)
        at java.io.ObjectOutputStream.defaultWriteFields(ObjectOutputStream.java:1604)
        at java.io.ObjectOutputStream.writeSerialData(ObjectOutputStream.java:1565)
        at java.io.ObjectOutputStream.writeOrdinaryObject(ObjectOutputStream.java:1488)
        at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1234)
        at java.io.ObjectOutputStream.defaultWriteFields(ObjectOutputStream.java:1604)
        at java.io.ObjectOutputStream.writeSerialData(ObjectOutputStream.java:1565)
        at java.io.ObjectOutputStream.writeOrdinaryObject(ObjectOutputStream.java:1488)
        at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1234)
        at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:354)
        at java.util.ArrayList.writeObject(ArrayList.java:762)
        at java.lang.reflect.Method.invoke(Native Method)
        at java.io.ObjectStreamClass.invokeWriteObject(ObjectStreamClass.java:1036)
        at java.io.ObjectOutputStream.writeSerialData(ObjectOutputStream.java:1552)
        at java.io.ObjectOutputStream.writeOrdinaryObject(ObjectOutputStream.java:1488)
        at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1234)
        at java.io.ObjectOutputStream.defaultWriteFields(ObjectOutputStream.java:1604)
        at java.io.ObjectOutputStream.defaultWriteObject(ObjectOutputStream.java:463)
        at java.lang.Throwable.writeObject(Throwable.java:1027)
        at java.lang.reflect.Method.invoke(Native Method)
        at java.io.ObjectStreamClass.invokeWriteObject(ObjectStreamClass.java:1036)
        at java.io.ObjectOutputStream.writeSerialData(ObjectOutputStream.java:1552)
        at java.io.ObjectOutputStream.writeOrdinaryObject(ObjectOutputStream.java:1488)
        at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1234)
        at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:354)
        at android.os.Parcel.writeSerializable(Parcel.java:2160)
        	... 50 more

NOTES Reproduced with 1.6.2

ccjernigan avatar Jun 18 '22 15:06 ccjernigan

Could not reproduce. The test:

import kotlinx.coroutines.*
import org.junit.jupiter.api.*
import java.io.*
import java.util.concurrent.*

class Reproducer3328 {

    @Test
    fun testSerialization() {
        val cdl = CountDownLatch(1)
        val bos = ByteArrayOutputStream()
        val lifecycleScope = CoroutineScope(CoroutineExceptionHandler { coroutineContext, throwable ->
            ObjectOutputStream(bos).use {
                it.writeObject(throwable)
                cdl.countDown()
            }
        })
        lifecycleScope.launch {
            throw RuntimeException("I will blow up when you try to serialize later")
        }
        cdl.await()
        println(bos)
    }
}

The output for me is the expected byte soup with some plausibly-looking strings embedded:

�� sr java.lang.RuntimeException�_G
detailMessaget Ljava/lang/String;[ 
stackTracet [Ljava/lang/StackTraceElement;L suppressedExceptionst Ljava/util/List;xpq ~ t .I will blow up when you try to serialize laterur [Ljava.lang.StackTraceElement;F*<<�"9  xp   sr java.lang.StackTraceElementa	Ś&6݅B formatI 
lineNumberL classLoaderNameq ~ L declaringClassq ~ LfileNameq ~ L 
methodNameq ~ L 
t 3kotlin.coroutines.jvm.internal.BaseContinuationImplt ContinuationImpl.ktt 
q ~ q ~ q ~ ppsr java.util.Collections$EmptyListz��<���  xpx

The version is 1.6.2.

dkhalanskyjb avatar Jun 20 '22 09:06 dkhalanskyjb

It is trickier to reproduce. You need a wrapped exception. The root cause of the problem is that JobCancellationException keeps a reference to a Job which is, in general, not serializable. I think that the proper fix would be to make this reference to a Job transient, since it is only used by the local exception propagation code and would not be ever needed if this exception was serialized and deserialized.

elizarov avatar Jun 20 '22 09:06 elizarov

The same issue is also present in TimeoutCancellationException and AbortFlowException, added a generic check for throwables

qwwdfsad avatar Jun 22 '22 08:06 qwwdfsad

Thanks for looking into this issue so quickly!

I tested the 1.6.4 release, and I don't think the issue is completely resolved. I managed to generate this exception trying to serialize an exception. The project is open source, so I created a branch that will reproduce the issue. The two links show where the exception is generated and where it tries to serialize it.

https://github.com/zcash/secant-android-wallet/tree/coroutines-serialization-bug

https://github.com/zcash/secant-android-wallet/blob/c77cd39062d1e837efdd023bb3d6397bee176fb9/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/home/view/HomeView.kt#L152

https://github.com/zcash/secant-android-wallet/blob/c77cd39062d1e837efdd023bb3d6397bee176fb9/crash-android-lib/src/main/kotlin/co/electriccoin/zcash/crash/android/internal/AndroidUncaughtExceptionHandler.kt#L27

java.lang.RuntimeException: Parcelable encountered IOException writing serializable object (name = kotlinx.coroutines.CoroutinesInternalError)
	at android.os.Parcel.writeSerializable(Parcel.java:1833)
	at android.os.Parcel.writeValue(Parcel.java:1780)
	at android.os.Parcel.writeArrayMapInternal(Parcel.java:928)
	at android.os.BaseBundle.writeToParcelInner(BaseBundle.java:1584)
	at android.os.Bundle.writeToParcel(Bundle.java:1253)
	at android.os.Parcel.writeBundle(Parcel.java:997)
	at android.content.Intent.writeToParcel(Intent.java:10495)
	at android.app.IActivityManager$Stub$Proxy.broadcastIntent(IActivityManager.java:4828)
	at android.app.ContextImpl.sendBroadcast(ContextImpl.java:1049)
	at android.content.ContextWrapper.sendBroadcast(ContextWrapper.java:448)
	at co.electriccoin.zcash.crash.android.internal.AndroidUncaughtExceptionHandler.uncaughtException(AndroidUncaughtExceptionHandler.kt:27)
	at java.lang.ThreadGroup.uncaughtException(ThreadGroup.java:1073)
	at java.lang.ThreadGroup.uncaughtException(ThreadGroup.java:1068)
	at kotlinx.coroutines.CoroutineExceptionHandlerImplKt.handleCoroutineExceptionImpl(CoroutineExceptionHandlerImpl.kt:61)
	at kotlinx.coroutines.CoroutineExceptionHandlerKt.handleCoroutineException(CoroutineExceptionHandler.kt:33)
	at kotlinx.coroutines.DispatchedTask.handleFatalException(DispatchedTask.kt:146)
	at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:115)
	at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:570)
	at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:750)
	at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:677)
	at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:664)
Caused by: java.io.NotSerializableException: kotlinx.coroutines.StandaloneCoroutine
	at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1240)
	at java.io.ObjectOutputStream.writeArray(ObjectOutputStream.java:1434)
	at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1230)
	at java.io.ObjectOutputStream.defaultWriteFields(ObjectOutputStream.java:1604)
	at java.io.ObjectOutputStream.writeSerialData(ObjectOutputStream.java:1565)
	at java.io.ObjectOutputStream.writeOrdinaryObject(ObjectOutputStream.java:1488)
	at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1234)
	at java.io.ObjectOutputStream.defaultWriteFields(ObjectOutputStream.java:1604)
	at java.io.ObjectOutputStream.writeSerialData(ObjectOutputStream.java:1565)
	at java.io.ObjectOutputStream.writeOrdinaryObject(ObjectOutputStream.java:1488)
	at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1234)
	at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:354)
	at java.util.ArrayList.writeObject(ArrayList.java:762)
	at java.lang.reflect.Method.invoke(Native Method)
	at java.io.ObjectStreamClass.invokeWriteObject(ObjectStreamClass.java:1036)
	at java.io.ObjectOutputStream.writeSerialData(ObjectOutputStream.java:1552)
	at java.io.ObjectOutputStream.writeOrdinaryObject(ObjectOutputStream.java:1488)
	at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1234)
	at java.io.ObjectOutputStream.defaultWriteFields(ObjectOutputStream.java:1604)
	at java.io.ObjectOutputStream.defaultWriteObject(ObjectOutputStream.java:463)
	at java.lang.Throwable.writeObject(Throwable.java:1027)
	at java.lang.reflect.Method.invoke(Native Method)
	at java.io.ObjectStreamClass.invokeWriteObject(ObjectStreamClass.java:1036)
	at java.io.ObjectOutputStream.writeSerialData(ObjectOutputStream.java:1552)
	at java.io.ObjectOutputStream.writeOrdinaryObject(ObjectOutputStream.java:1488)
	at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1234)
	at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:354)
	at android.os.Parcel.writeSerializable(Parcel.java:1828)

ccjernigan avatar Jul 14 '22 12:07 ccjernigan

Could you please point out the test which I should run in order to reproduce the crash? Or should I launch the app and click somewhere?

qwwdfsad avatar Jul 14 '22 17:07 qwwdfsad

You'd have to run the app from that branch.

If you open the debug build, there's a overflow menu during onboarding to prefill in a seed phrase. Then once you're at the app's Home Screen, there's another debug menu with an option to trigger an uncaught exception. Do you want to give that a try?

I can trigger our CI server to create the APK, but if you want to check out the project and build it that works too.

ccjernigan avatar Jul 14 '22 21:07 ccjernigan

Thanks! The reason was the diagnostic exception that was itself serializable, but transitively got non-serializable StandaloneCoroutine

qwwdfsad avatar Jul 15 '22 13:07 qwwdfsad

When is this going to be released please? Is there a temp work around?

SunnyBe avatar Feb 19 '23 20:02 SunnyBe