kotlinx.coroutines
kotlinx.coroutines copied to clipboard
Coroutines Exceptions Violate Serializable Interface
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
- Implement an uncaught exception handler that attempts to Serialize an uncaught exception
- 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
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.
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.
The same issue is also present in TimeoutCancellationException
and AbortFlowException
, added a generic check for throwables
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)
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?
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.
Thanks! The reason was the diagnostic exception that was itself serializable, but transitively got non-serializable StandaloneCoroutine
When is this going to be released please? Is there a temp work around?