scala / bug

Scala 2 bug reports only. Please, no questions — proper bug reports only.
https://scala-lang.org
230 stars 21 forks source link

A `writeReplace` in a superclass is able to override serialization of an extending singleton object #12954

Closed noresttherein closed 3 months ago

noresttherein commented 3 months ago

Scala version: 2.13.12

abstract class Saboteur extends Serializable {
    protected[this] def writeReplace :AnyRef = Ninja
}
object Victim extends Saboteur {
//  override protected def lengthImpl :Int = 0
}
object Ninja extends Serializable

private object Playground extends App {
    val out = new ByteArrayOutputStream()
    val obj = new ObjectOutputStream(out)
    val col = Cat.empty
    obj.writeObject(Victim)
    obj.close()
    val in   = new ObjectInputStream(new ByteArrayInputStream(out.toByteArray))
    val copy = in.readObject()
    println(copy + ": " + copy.getClass.getName)
}

Problem

Ninja$@59494225: Ninja

Explain how the above behavior isn't what you expected. No one expects a ninja.

Removing explicit AnyRef return type fixes the issue, but given that I had to try to minimize it, I suspect it's a bug.

lrytz commented 3 months ago

writeReplace needs to have Object / AnyRef return type to work, so that part is expected (https://docs.oracle.com/en/java/javase/11/docs/specs/serialization/output.html#the-writereplace-method).

Why do you think an inherited writeReplace should not affect serialization?

lrytz commented 3 months ago

For the record, we're relying on this behavior for collections: https://github.com/scala/scala/blob/v2.13.12/src/library/scala/collection/generic/DefaultSerializationProxy.scala#L71-L78

noresttherein commented 3 months ago

writeReplace needs to have Object / AnyRef return type to work, so that part is expected

Oh sh.... You are right. Now I need to review my whole codebase, because while I had tests checking that serialization works, I did not look in detail into exactly how the objects get serialized; and, as most of the time, writeReplace is private, I kind of left the return type out half of the time. My bad.

Of course, inherited writeReplace works in subclasses. I was surprised that it works also on singleton objects. I expected that singleton's default behaviouor will override inherited one - both because inherited methods have lower precedence, the singletons being 'special', and because their serialization is already optimal. I simply couldn't imagine a scenario were it would work differently. . But, of course, my report stems from the fact that I found out this seemingly minor difference to matter, not realising that otherwise writeReplace does not work, period.