JDK-8024931 : Deserialization fails with cyclic object graph and serial proxy
  • Type: Bug
  • Component: core-libs
  • Sub-Component: java.io:serialization
  • Affected Version: 1.4.2,8
  • Priority: P4
  • Status: Open
  • Resolution: Unresolved
  • OS: linux
  • Submitted: 2013-09-17
  • Updated: 2024-06-14
The Version table provides details related to the release that this issue/RFE will be addressed.

Unresolved : Release in which this issue/RFE will be addressed.
Resolved: Release in which this issue/RFE has been resolved.
Fixed : Release in which this issue/RFE has been fixed. The release containing this fix may be available for download as an Early Access Release or a General Availability Release.

To download the current JDK release, click here.
Other
tbdUnresolved
Related Reports
Duplicate :  
Relates :  
Relates :  
Description
FULL PRODUCT VERSION :
1.8.0-ea-b106

A DESCRIPTION OF THE PROBLEM :
A lambda that captures a value that refers back to that lambda cannot be properly deserialized. This is caused by the usage of readResolve() in java.lang.invoke.SerializedLambda.


ACTUAL -
The cyclic reference is not resolved, an instance of java.lang.invoke.SerializedLambda is used where a lambda is expected.

ERROR MESSAGES/STACK TRACES THAT OCCUR :
first example:
java.lang.ClassCastException: cannot assign instance of java.lang.invoke.SerializedLambda to field SerialLambda.supplier of type SerialLambda$SerializableIntSupplier in instance of SerialLambda
at java.io.ObjectStreamClass$FieldReflector.setObjFieldValues(ObjectStreamClass.java:2089)
...
at java.io.ObjectInputStream.readObject(ObjectInputStream.java:371)
at SerialLambda.serialCopy(SerialLambda.java:32)
at SerialLambda.go(SerialLambda.java:19)

second example:
Exception in thread "main" java.lang.ClassCastException: java.lang.invoke.SerializedLambda cannot be cast to SerialLambda$SerializableIntSupplier
at SerialLambda.lambda$go2$9b75$0(SerialLambda.java:24)
at SerialLambda$$Lambda$2.getAsInt(Unknown Source)
at SerialLambda.go2(SerialLambda.java:26)

REPRODUCIBILITY :
This bug can be reproduced always.

---------- BEGIN SOURCE ----------
import java.io.*;
import java.util.*;
import java.util.function.IntSupplier;

/** @author Wouter Coekaerts */
public class SerialLambda implements Serializable {
  interface SerializableIntSupplier extends IntSupplier, Serializable {}

  public static void main(String[] args) throws Exception {
    new SerialLambda().go();
    go2();
  }

  SerializableIntSupplier supplier;

  void go() throws Exception {
    supplier = this::hashCode;
    System.out.println(serialCopy(this).supplier.getAsInt());
    System.out.println(serialCopy(this.supplier).getAsInt());
  }

  static void go2() throws Exception {
    List<SerializableIntSupplier> list = new ArrayList<>();
    list.add(() -> list.get(0).hashCode());
    System.out.println(serialCopy(list).get(0).getAsInt());
    System.out.println(serialCopy(list.get(0)).getAsInt());
  }

  @SuppressWarnings("unchecked")
  static <T> T serialCopy(T o) throws Exception {
    ByteArrayOutputStream ba = new ByteArrayOutputStream();
    new ObjectOutputStream(ba).writeObject(o);
    return (T) new ObjectInputStream(new ByteArrayInputStream(ba.toByteArray())).readObject();
  }
}
---------- END SOURCE ----------
Comments
The pathology described in this bug report differs from other bugs reports about deserialization and cyclic object graphs in that this one involves serial proxies. The mechanism at issue is writeReplace/readResolve. Briefly, the problem is that when the serial proxy is deserialized, any objects with references to it will retain references to the proxy, even after the proxy's readResolve() method is called to provide the "real" object. If the object returned by readResolve() is of a compatible type, deserialization will complete but other objects will refer to the "wrong" object (the proxy) instead of the object returned from readResolve(). If the types are incompatible, this will result in a ClassCastException as illustrated here. This is purely an issue with serialization; it has nothing to do with lambdas. This problem can occur if unmodifiable collections (List.of et al) are used in serializable, cyclic data structures. The symptom is a ClassCastException attempting to assign an instance of java.util.CollSer to a field of a class being deserialized. (See JDK-8304814 for an example.) See also the explanation in the following comment.
14-06-2024

Copying in this comment since it's significant: https://bugs.openjdk.org/browse/JDK-8304814?focusedId=14668985&page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel#comment-14668985 Roger Riggs added a comment - 2024-04-29 08:03 During the time that a serialization proxy is being be deserialized, the handle table contains a reference to the proxy object. At the start of deserializing the proxy object, the handle is assigned and bound to the proxy object. If a cycle contains a reference to the original object, the binding of the handle refers to the proxy instance. After the proxy is deserialized (the call of the class's readObject() or the equivalent defaultReadObject() returns) then the proxy's `readResolve()` method is called. When it returns, the binding of the handle is updated to refer to the newly created copy of the original object. This is an artifact of the design of serialization proxies. (not a bug) The Immutable collections use serialization proxies, hence this problem may arise in combination with cycles. The original List is likely an ArrayList, not using serialization proxies, and this condition will not arise.
13-06-2024

Reduced to P4 and removed fix-version of 8. This is a long-standing problem and the linked bugs are also P4.
16-10-2013

Seems related to JDK-6208166 and JDK-4957674, which both deal with the serialization mechanism's inability to deal with certain cases involving circularity.
07-10-2013

What we have is a serializable object X which holds a reference to another serializable object Y, where Y is serialized through writeReplace/readResolve, and intermediate/replaced version of Y holds a reference to X. If we serialize and deserialize X, everything works fine. But if we only serialize/deserialize Y, serialization tries to write the intermediate result back to the field of X.
07-10-2013

This is not a lambda-specific issue. I've reproduced this with no lambda involved in the following TestNG test case, and it gets the same CCE: import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable; import java.util.function.IntSupplier; import org.testng.annotations.Test; import static org.testng.Assert.assertEquals; /** * SerTest * * @author Brian Goetz */ @Test public class SerTest { public void testHasher() throws Exception { Hasher h = new Hasher(new Integer(3)); assertEquals(3, h.getAsInt()); assertEquals(3, serialCopy(h).getAsInt()); } public void testHolder() throws Exception { Hasher h = new Hasher(new Integer(4)); HasherHolder hh = new HasherHolder(); hh.supplier = h; assertEquals(4, hh.supplier.getAsInt()); assertEquals(4, serialCopy(hh).supplier.getAsInt()); assertEquals(4, serialCopy(hh.supplier).getAsInt()); } public void testHolderSelfRef() throws Exception { HasherHolder hh = new HasherHolder(); int result = hh.hashCode(); Hasher h = new Hasher(hh); hh.supplier = h; serialCopy(hh).supplier.getAsInt(); serialCopy(hh.supplier).getAsInt(); } @SuppressWarnings("unchecked") static <T> T serialCopy(T o) throws Exception { ByteArrayOutputStream ba = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(ba); oos.writeObject(o); oos.close(); byte[] bytes = ba.toByteArray(); return (T) new ObjectInputStream(new ByteArrayInputStream(bytes)).readObject(); } } class HasherHolder implements Serializable { IntSupplier supplier; } class Hasher implements Serializable, IntSupplier { private final Object o; Hasher(Object o) { this.o = o; } @Override public int getAsInt() { return o.hashCode(); } private Object writeReplace() { return new HasherSerRep(o); } private static class HasherSerRep implements Serializable { private final Object o; private HasherSerRep(Object o) { this.o = o; } private Object readResolve() { return new Hasher(o); } } }
07-10-2013