Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Coroutines Exceptions Violate Serializable Interface #3328

Closed
ccjernigan opened this issue Jun 18, 2022 · 8 comments
Closed

Coroutines Exceptions Violate Serializable Interface #3328

ccjernigan opened this issue Jun 18, 2022 · 8 comments
Assignees

Comments

@ccjernigan
Copy link

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

@dkhalanskyjb
Copy link
Collaborator

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.

@elizarov
Copy link
Contributor

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.

@qwwdfsad
Copy link
Member

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

@ccjernigan
Copy link
Author

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)

@qwwdfsad qwwdfsad reopened this Jul 14, 2022
@qwwdfsad
Copy link
Member

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?

@ccjernigan
Copy link
Author

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.

@qwwdfsad
Copy link
Member

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

@SunnyBe
Copy link

SunnyBe commented Feb 19, 2023

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

csadilek added a commit to csadilek/firefox-android that referenced this issue Apr 4, 2023
csadilek added a commit to csadilek/firefox-android that referenced this issue Apr 4, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

5 participants