-
Notifications
You must be signed in to change notification settings - Fork 1.1k
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
Scala 3 lazy vals are not serialization-safe #20856
Comments
This is probably impossible to test correctly on our CI. Once it is fixed, I suggest adding the reproducer repo to the community build. We can run two first steps from the readme with some predefined timeout, to test if the problem is fixed. @WojciechMazur will this require significant changes to the OCB? |
This could be simplified further to serialize to binary file but it would still require two jvms. Would that fit to CI? |
No, it should be really straightforward. All we need is to include the reproducer repo in the OpenCB config https://github.com/VirtusLab/community-build3/blob/master/coordinator/configs/custom-projects.txt |
Having it just compile will test nothing and with the current shape of the code it just hangs on one thread, there are no assertions as it's just demonstrating the problem. I think it can be greatly simplified (no akka for starters) and incorporated into the test suite of the language instead of OCB. |
//> using scala 3.3.3
//> using jvm 21
//> using dep com.softwaremill.ox::core:0.2.2
import java.io.*
import ox.*
import scala.concurrent.duration.*
case class Message(content: String):
lazy val bomb: String =
sleep(200.millis)
"BOMB: " + content
def serialize(obj: Message): Array[Byte] =
val byteStream = ByteArrayOutputStream()
val objectStream = ObjectOutputStream(byteStream)
try
objectStream.writeObject(obj)
byteStream.toByteArray
finally
objectStream.close()
byteStream.close()
def deserialize(bytes: Array[Byte]): Message =
val byteStream = ByteArrayInputStream(bytes)
val objectStream = ObjectInputStream(byteStream)
try
objectStream.readObject().asInstanceOf[Message]
finally
objectStream.close()
byteStream.close()
@main def main =
val bytes = supervised:
val msg = Message("test")
fork:
msg.bomb // start evaluation before serialization
sleep(50.millis) // give some time for the fork to start lazy val rhs eval
serialize(msg) // serialize in the meantime so that we capture Waiting state
val deserializedMsg = deserialize(bytes)
unsupervised:
@volatile var msg = ""
val f = forkCancellable:
msg = deserializedMsg.bomb
sleep(1000.millis)
if !msg.isBlank then println(s"succeeded: $msg")
else
f.cancel()
println("failed to read bomb in 1s!") |
arguably can be done without ox and jdk 21 :D one second please |
//> using scala 3.3.3
import java.io.*
case class Message(content: String):
lazy val bomb: String =
Thread.sleep(200)
"BOMB: " + content
def serialize(obj: Message): Array[Byte] =
val byteStream = ByteArrayOutputStream()
val objectStream = ObjectOutputStream(byteStream)
try
objectStream.writeObject(obj)
byteStream.toByteArray
finally
objectStream.close()
byteStream.close()
def deserialize(bytes: Array[Byte]): Message =
val byteStream = ByteArrayInputStream(bytes)
val objectStream = ObjectInputStream(byteStream)
try
objectStream.readObject().asInstanceOf[Message]
finally
objectStream.close()
byteStream.close()
@main def main =
val bytes = locally:
val msg = Message("test")
val touch = Thread(() => {
msg.bomb // start evaluation before serialization
()
})
touch.start()
Thread.sleep(50) // give some time for the fork to start lazy val rhs eval
serialize(msg) // serialize in the meantime so that we capture Waiting state
val deserializedMsg = deserialize(bytes)
@volatile var msg = ""
@volatile var started = false
val read = Thread(() => {
started = true
msg = deserializedMsg.bomb
()
})
read.start()
Thread.sleep(1000)
if !started then throw Exception("wtf")
if !msg.isBlank then println(s"succeeded: $msg")
else
read.interrupt()
println("failed to read bomb in 1s!") |
This even shows the nice stack trace from Exception in thread "Thread-1" java.lang.InterruptedException
at java.base/java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireSharedInterruptibly(AbstractQueuedSynchronizer.java:1100)
at java.base/java.util.concurrent.CountDownLatch.await(CountDownLatch.java:230)
at Message.bomb$lzyINIT1(main.scala:8)
at Message.bomb(main.scala:7)
at main$package$.$anonfun$2(main.scala:49)
at java.base/java.lang.Thread.run(Thread.java:1583)
failed to read bomb in 1s! |
This strategy ensures the "serializability" condition of parallel programs--not to be confused with the data being `java.io.Serializable`. Indeed, if thread A is evaluating the lazy val while thread B attempts to serialize its owner object, there is also an alternative schedule where thread B serializes the owner object *before* A starts evaluating the lazy val. Therefore, forcing B to see the non-evaluating state is correct.
This strategy ensures the "serializability" condition of parallel programs--not to be confused with the data being `java.io.Serializable`. Indeed, if thread A is evaluating the lazy val while thread B attempts to serialize its owner object, there is also an alternative schedule where thread B serializes the owner object *before* A starts evaluating the lazy val. Therefore, forcing B to see the non-evaluating state is correct.
This strategy ensures the "serializability" condition of parallel programs--not to be confused with the data being `java.io.Serializable`. Indeed, if thread A is evaluating the lazy val while thread B attempts to serialize its owner object, there is also an alternative schedule where thread B serializes the owner object *before* A starts evaluating the lazy val. Therefore, forcing B to see the non-evaluating state is correct.
This strategy ensures the "serializability" condition of parallel programs--not to be confused with the data being `java.io.Serializable`. Indeed, if thread A is evaluating the lazy val while thread B attempts to serialize its owner object, there is also an alternative schedule where thread B serializes the owner object *before* A starts evaluating the lazy val. Therefore, forcing B to see the non-evaluating state is correct. [Cherry-picked e242753]
This strategy ensures the "serializability" condition of parallel programs--not to be confused with the data being `java.io.Serializable`. Indeed, if thread A is evaluating the lazy val while thread B attempts to serialize its owner object, there is also an alternative schedule where thread B serializes the owner object *before* A starts evaluating the lazy val. Therefore, forcing B to see the non-evaluating state is correct. [Cherry-picked e242753]
Description
I'm reporting a vulnerability in Scala 3's lazy val implementation concerning Java serialization. JVM can serialize a lazy val field in the "Waiting" state should
rhs
evaluation take too long. However, because the thread that calls .countDown() isn't present on the recipient machine (the one deserializing the value), the lazy val remains set toWaiting
forever, blocking all threads accessing the lazy val field onCountDownLatch#await()
call:Compiler version
Scala 3.3.1+, probably Scala 3.3.0 too
Minimized code
https://github.com/VirtusLab/scala-3-lazy-val-vs-java-serialization
Workaround
It is possible to resolve this by annotating the
lazy val
field with@transient
.Expected outcome
We think that it would be a good idea for the compiler to generate all lazy val fields with transient modifier as this implementation will always be prone to this problem, especially given that
LazyValControlState
extendsSerializable
since #16806.The text was updated successfully, but these errors were encountered: