JDK-8304814 : Cannot assign instance of java.util.CollSer to java.util.List
  • Type: Bug
  • Component: core-libs
  • Sub-Component: java.io:serialization
  • Affected Version: 20
  • Priority: P4
  • Status: Closed
  • Resolution: Duplicate
  • OS: generic
  • CPU: generic
  • Submitted: 2023-03-22
  • Updated: 2024-06-13
  • Resolved: 2024-06-13
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
tbdResolved
Related Reports
Duplicate :  
Relates :  
Description
ADDITIONAL SYSTEM INFORMATION :
java version "20" 2023-03-21
Java(TM) SE Runtime Environment (build 20+36-2344)
Java HotSpot(TM) 64-Bit Server VM (build 20+36-2344, mixed mode, sharing)


A DESCRIPTION OF THE PROBLEM :
In a specific condition, I cannot unserialize an object that has references to a List created with List.copyOf().

I have this exception while unserializing an object:

java.lang.ClassCastException: cannot assign instance of java.util.CollSer to field a.b.c.BaseClass.aField of type java.util.List in instance of a.b.c.SubClass.

At serialization, the a.b.c.BaseClass.aField does contain a List that was created like this

	return List.copyOf(originalList);

I have a “simple” workaround to fix the problem by changing the previous line to:

	return originalList.

Note that the list may be shared by multiple instances of a.b.c.BaseClass.

see also: 
https://stackoverflow.com/questions/75804762/classcastexception-when-unserializing-an-immutable-list-cannot-assign-instance
https://stackoverflow.com/questions/65613929/map-in-activemq-objectmessage-throws-classcastexception-cannot-assign-instance


STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
I can reproduce this problem systematically. Unfortunately, this is a huge object being serialized (215 classes, 900MB on disk, error stack trace is 4200 lines). This object has been growing for years. It uses unmodifiable Maps, Lists, Sets ... all over without problems. It is just à this one usage point added recently where using  List.copyOf() method causes this exception. I have tried to reproduce this problem using a small test case unsuccessfully.

Here is the code and state at the point of exception:

If I break and debug at the exception point (this is JDK 17 code) at java.base/java.io.ObjectStreamClass$FieldReflector.setObjFieldValues(ObjectStreamClass.java:2227)

   private void setObjFieldValues(Object obj, Object[] vals, boolean dryRun) {
        if (obj == null) {
            throw new NullPointerException();
        }
        for (int i = numPrimFields; i < fields.length; i++) {
            long key = writeKeys[i];
            if (key == Unsafe.INVALID_FIELD_OFFSET) {
                continue;           // discard value
            }
            switch (typeCodes[i]) {
                case 'L', '[' -> {
                    Object val = vals[offsets[i]];
                    if (val != null &&
                        !types[i - numPrimFields].isInstance(val))
                    {
                        Field f = fields[i].getField();
                        throw new ClassCastException(
                            "cannot assign instance of " +
                            val.getClass().getName() + " to field " +
                            f.getDeclaringClass().getName() + "." +
                            f.getName() + " of type " +
                            f.getType().getName() + " in instance of " +
                            obj.getClass().getName());
                    }
                    if (!dryRun)
                        unsafe.putReference(obj, key, val);
                }
                default -> throw new InternalError();
            }
        }
    }
	
In my scenario,

	obj is an instance of a.b.c.SubClass
	vals has 26 elements of which index 17 is a CollSer
	dryRun is true
	The “i” value is 20.
	typecode[i] is ‘L’
	numPrimFields is 3
	offset[i] is 17
	types[i - numPrimFields] is java.util.List
	val is a CollSer (array null, tag 1 (IMM_LIST))
	
The line:

!types[i - numPrimFields].isInstance(val)

Fails because java.util.List is not an instance of java.util.CollSer I think (not sure) CollSer should probably have already been replaced by a List by CollSer’s readResolve() method.


EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
No Exception
ACTUAL -
Caused by: java.lang.ClassCastException: cannot assign instance of java.util.CollSer to field a.b.c.BaseClass.aField of type java.util.List in instance of a.b.c.SubClass.
    at java.base/java.io.ObjectStreamClass$FieldReflector.setObjFieldValues(ObjectStreamClass.java:2227)
    at java.base/java.io.ObjectStreamClass$FieldReflector.checkObjectFieldValueTypes(ObjectStreamClass.java:2191)
    at java.base/java.io.ObjectStreamClass.checkObjFieldValueTypes(ObjectStreamClass.java:1478)
    at java.base/java.io.ObjectInputStream$FieldValues.defaultCheckFieldValues(ObjectInputStream.java:2657)
    at java.base/java.io.ObjectInputStream.readSerialData(ObjectInputStream.java:2471)
    at java.base/java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2242)
    at java.base/java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1742)
    at java.base/java.io.ObjectInputStream.readObject(ObjectInputStream.java:514)
    at java.base/java.io.ObjectInputStream.readObject(ObjectInputStream.java:472)
    … 4200 other lines
	

CUSTOMER SUBMITTED WORKAROUND :
Note that I have used Unmodifiable Maps Lists ... all over this code without problems. It is just à this one usage point where using  List.copyOf() method causes this exception at unserialisation time. Using the original modifiable List prevents the exception from being thrown.

FREQUENCY : rarely



Comments
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.
29-04-2024

Note that the information posted on 2024-04-25 in JI-9076940 is from a different person than the original submitter of the bug. It's not clear if there is any relationship between the original submission and the recent additional information. The exception thrown and the stack traces may *seem* similar but they might have completely different pathologies. Without having done much investigation, my initial impression is that the original issue was the result of a cycle in the graph of serialized objects. This interferes with the readResolve() mechanism, which is used for replacing the serialization proxy class CollSer with the actual List instance. I don't know whether this is an actual bug in the deserialization mechanism or whether it's an intractable problem with deserialization of cyclic graphs, which can expose user code to partially initialized or uninitialized objects during the deserialization process. The recent data that was posted with JI-9076940 suggests that the root cause is a missing class at deserialization time. This seems like a different scenario from the original submission. However, it seems like it results in a similar ClassCastException on CollSer. The exception handling path should be investigated to see whether exceptions have been chained properly. It may be that there's an actual bug here. Again, however, this seems like a different scenario from the original submission.
26-04-2024

The observations on Windows 11: JDK 17: Failed, ClassCastException and InvalidObjectException observed. JDK 20: Failed. JDK 22: Failed. JDK 23ea+14: Failed.
26-04-2024

Additional information from community: I'd like to provide a reproducible for JDK-8304814. Under the hood java.lang.ClassCastException: Cannot cast java.util.CollSer hides ClassNotFoundException. It can be easily reproduced with both Class or Record that reference an immutable map that contains objects of missing classes. STEPS TO FOLLOW TO REPRODUCE THE PROBLEM : 1. Serialise data to file : java TestClassCastException write 2. rm DeletedClass.class 3. Read file java TestClassCastException read EXPECTED VERSUS ACTUAL BEHAVIOR : EXPECTED - java.lang.ClassNotFoundException ACTUAL - For classes it throws Exception in thread "main" java.lang.ClassCastException: cannot assign instance of java.util.CollSer to field TestClassCastException$C.data of type java.util.Map in instance of TestClassCastException$C at java.base/java.io.ObjectStreamClass$FieldReflector.setObjFieldValues(ObjectStreamClass.java:2096) at java.base/java.io.ObjectStreamClass$FieldReflector.checkObjectFieldValueTypes(ObjectStreamClass.java:2060) at java.base/java.io.ObjectStreamClass.checkObjFieldValueTypes(ObjectStreamClass.java:1347) at java.base/java.io.ObjectInputStream$FieldValues.defaultCheckFieldValues(ObjectInputStream.java:2679) at java.base/java.io.ObjectInputStream.readSerialData(ObjectInputStream.java:2486) at java.base/java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2257) at java.base/java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1733) at java.base/java.io.ObjectInputStream.readObject(ObjectInputStream.java:509) at java.base/java.io.ObjectInputStream.readObject(ObjectInputStream.java:467) at TestClassCastException.read(TestClassCastException.java:40) at TestClassCastException.main(TestClassCastException.java:24) For records it throwsException in thread "main" java.io.InvalidObjectException: Cannot cast java.util.CollSer to java.util.Map at java.base/java.io.ObjectInputStream.readRecord(ObjectInputStream.java:2363) at java.base/java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2251) at java.base/java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1733) at java.base/java.io.ObjectInputStream.readObject(ObjectInputStream.java:509) at java.base/java.io.ObjectInputStream.readObject(ObjectInputStream.java:467) at TestClassCastException.read(TestClassCastException.java:41) at TestClassCastException.main(TestClassCastException.java:22)Caused by: java.lang.ClassCastException: Cannot cast java.util.CollSer to java.util.Map at java.base/java.lang.Class.cast(Class.java:3889) at java.base/java.io.ObjectInputStream.readRecord(ObjectInputStream.java:2361) ... 6 more ---------- BEGIN SOURCE ---------- import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.InputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.OutputStream; import java.io.Serializable; import java.util.LinkedHashMap; import java.util.Map; public class TestClassCastException { public static void main(String[] args) throws Exception { File f = new File("/tmp/write.ser"); if (args.length > 0 && args[0].equals("write")){ Map m = Map.of("key", new DeletedClass()); TestClassCastException.R r = new TestClassCastException.R(m); f.delete(); write(new FileOutputStream(f), r); } else{ read(new FileInputStream(f)); } } public record R(Map<String, Map> data) implements Serializable { } private static void read(InputStream str) throws Exception { try (ObjectInputStream ois = new ObjectInputStream(str)) { Object o = ois.readObject(); System.out.println(o); } } private static void write(OutputStream os, Object o) throws Exception { try (ObjectOutputStream os2 = new ObjectOutputStream(os)) { os2.writeObject(o); } } } import java.io.Serializable; public class DeletedClass implements Serializable { } ---------- END SOURCE ---------- FREQUENCY : always
26-04-2024

Additional information from the submitter: The problem seams to be related to the java.util.ImmutableCollections$List12 class serialization as the java.util.ImmutableCollections$ListN does not cause the problem.
18-05-2023

Probably a bad interaction between a cyclic object graph and readResolve().
23-03-2023

Hard to reproduce the issue.
23-03-2023