retronym commented on PR #1161:
URL: https://github.com/apache/fury/pull/1161#issuecomment-2702541425
I've been looking at this issue in the context of normal JDK serialization
and Scala collections, which as-of Scala 2.13, use the serialization proxy
pattern much more widely and are now prone to corrupt deserialized object
graphs.
I wrote a few tests to show that the underlying issue affect a small number
of Java collections, too. I then tried the same test cases with Fury, both with
and without custom Scala serializers registered.
```java
package repro;
import org.apache.commons.lang3.SerializationUtils;
import org.apache.fury.Fury;
import org.apache.fury.config.Language;
import org.junit.Test;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class JavaCollectionCircularSerialization {
@Test
public void testJavaArrayList() {
testJavaSerialization(new ArrayListFactory());
}
@Test
public void testJavaAsList() {
testJavaSerialization(new AsListFactory());
}
@Test
public void testJavaListOf() {
// java.lang.ClassCastException: class java.util.CollSer cannot be
cast to class java.util.List (java.util.CollSer and java.util.List are in
module java.base of loader 'bootstrap')
testJavaSerialization(new ListOfFactory());
}
@Test
public void testFuryArrayList() {
testFurySerialization(new ArrayListFactory());
}
@Test
public void testFuryAsList() {
// Cannot invoke "java.util.List.getClass()" because "o" is null
testFurySerialization(new AsListFactory());
}
@Test
public void testFuryListOf() {
// Cannot invoke "java.util.List.getClass()" because "o" is null
testFurySerialization(new ListOfFactory());
}
interface ListFactory {
<T> List<T> newList(T elem);
}
static class ArrayListFactory implements ListFactory {
@Override
public <T> List<T> newList(T elem) {
ArrayList<T> os = new ArrayList<>();
os.add(elem);
return os;
}
}
static class AsListFactory implements ListFactory {
@Override
public <T> List<T> newList(T elem) {
return Arrays.asList(elem);
}
}
static class ListOfFactory implements ListFactory {
@Override
public <T> List<T> newList(T elem) {
return List.of(elem);
}
}
static <T> void testJavaSerialization(ListFactory factory) {
var c1 = new ArrayList<List<?>>();
var c2 = factory.newList(c1);
c1.add(c2);
List<List<List<?>>> clone = (List<List<List<?>>>)
SerializationUtils.clone((Serializable) c2);
var o = clone.get(0).get(0);
System.out.println(o.getClass()); // CollSer
if (o.size() != 1) {
throw new AssertionError("size not preserved: " + o.size());
}
if (o != clone) {
throw new AssertionError("circular ref not preserved");
}
}
static <T> void testFurySerialization(ListFactory factory) {
var c1 = new ArrayList<List<?>>();
var c2 = factory.newList(c1);
c1.add(c2);
Fury fury = Fury.builder()
.withLanguage(Language.JAVA)
.requireClassRegistration(false)
.withRefTracking(true)
.build();
List<ArrayList<List<?>>> clone = (List<ArrayList<List<?>>>)
fury.deserialize(fury.serialize(c2));
var o = clone.get(0).get(0);
System.out.println(o.getClass()); // CollSer
if (o.size() != 1) {
throw new AssertionError("size not preserved: " + o.size());
}
if (o != clone) {
throw new AssertionError("circular ref not preserved");
}
}
}
```
```java
package repro;
import org.apache.commons.lang3.SerializationUtils;
import org.apache.fury.Fury;
import org.apache.fury.config.Language;
import org.apache.fury.serializer.Serializer;
import org.apache.fury.serializer.scala.ScalaSerializers;
import org.junit.Test;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
// Scala 2.13 uses the serialization proxy pattern for collections which had
run into
// the limitation of Java serialization + readResolve + circular references.
public class ScalaCollectionCircularSerialization {
@Test
public void testJavaDefaultSeq() {
// java.lang.ClassCastException: class
scala.collection.generic.DefaultSerializationProxy cannot be cast to class
scala.collection.Seq (scala.collection.generic.DefaultSerializationProxy and
scala.collection.Seq are in unnamed module of loader 'app')
testJavaSerialization(new DefaultSeqFactory());
}
@Test
public void testFuryDefaultSeq() {
// java.lang.ClassCastException: class
scala.collection.generic.DefaultSerializationProxy cannot be cast to class
scala.collection.Seq (scala.collection.generic.DefaultSerializationProxy and
scala.collection.Seq are in unnamed module of loader 'app')
testFurySerialization(new DefaultSeqFactory(), false);
}
@Test
public void testFuryWithCustomScalaSerializersDefaultSeq() {
// java.lang.NullPointerException: Cannot invoke
"scala.collection.Seq.getClass()" because "o" is null
testFurySerialization(new DefaultSeqFactory(), true);
}
interface SeqFactory {
<T> scala.collection.Seq<T> newSeq(T elem);
}
static class DefaultSeqFactory implements SeqFactory {
@Override
public <T> scala.collection.Seq<T> newSeq(T elem) {
var builder = scala.collection.Seq$.MODULE$.newBuilder();
builder.addOne(elem);
return (scala.collection.Seq<T>) builder.result();
}
}
static <T> void testJavaSerialization(SeqFactory factory) {
var c1 = new ArrayList<scala.collection.Seq<?>>();
var c2 = factory.newSeq(c1);
c1.add(c2);
scala.collection.Seq<List<scala.collection.Seq<?>>> clone =
(scala.collection.Seq<List<scala.collection.Seq<?>>>)
SerializationUtils.clone((Serializable) c2);
var o = clone.apply(0).get(0);
System.out.println(o.getClass()); // CollSer
if (o.size() != 1) {
throw new AssertionError("size not preserved: " + o.size());
}
if (o != clone) {
throw new AssertionError("circular ref not preserved");
}
}
static <T> void testFurySerialization(SeqFactory factory, boolean
useCustomScalaSerializer) {
var c1 = new ArrayList<scala.collection.Seq<?>>();
var c2 = factory.newSeq(c1);
c1.add(c2);
Fury fury = Fury.builder()
.withLanguage(Language.JAVA)
.requireClassRegistration(false)
.withRefTracking(true)
.build();
if (useCustomScalaSerializer) {
ScalaSerializers.registerSerializers(fury);
}
scala.collection.Seq<List<scala.collection.Seq<?>>> clone =
(scala.collection.Seq<List<scala.collection.Seq<?>>>)
fury.deserialize(fury.serialize(c2));
var o = clone.apply(0).get(0);
System.out.println(o.getClass()); // CollSer
if (o.size() != 1) {
throw new AssertionError("size not preserved: " + o.size());
}
if (o != clone) {
throw new AssertionError("circular ref not preserved");
}
}
}
```
https//github.com/retronym/circular-serialization.git
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]