CORDA-1213 - Explicitly disallow serialization of non static nested classes (#2824)

* CORDA-1213 - Explicitly disallow serialization of non static nested classes

WIP

* Review comments
This commit is contained in:
Katelyn Baker
2018-03-27 10:06:46 +01:00
committed by GitHub
parent 91cdcc6752
commit 0f99efa768
8 changed files with 431 additions and 39 deletions

View File

@ -0,0 +1,7 @@
package net.corda.nodeapi.internal.serialization.amqp
import java.io.NotSerializableException
import java.lang.reflect.Type
class SyntheticParameterException(val type: Type) : NotSerializableException("Type '${type.typeName} has synthetic "
+ "fields and is likely a nested inner class. This is not support by the Corda AMQP serialization framework")

View File

@ -1,7 +1,6 @@
package net.corda.nodeapi.internal.serialization.amqp
import net.corda.core.utilities.contextLogger
import net.corda.core.utilities.debug
import net.corda.core.utilities.trace
import net.corda.nodeapi.internal.serialization.amqp.SerializerFactory.Companion.nameForType
import org.apache.qpid.proton.amqp.Symbol
@ -58,13 +57,22 @@ open class ObjectSerializer(val clazz: Type, factory: SerializerFactory) : AMQPS
data: Data,
type: Type,
output: SerializationOutput,
debugIndent: Int) = ifThrowsAppend({ clazz.typeName }) {
debugIndent: Int) = ifThrowsAppend({ clazz.typeName }
) {
if (propertySerializers.size != javaConstructor?.parameterCount &&
javaConstructor?.parameterCount ?: 0 > 0
) {
throw NotSerializableException("Serialization constructor for class $type expects "
+ "${javaConstructor?.parameterCount} parameters but we have ${propertySerializers.size} "
+ "properties to serialize.")
}
// Write described
data.withDescribed(typeNotation.descriptor) {
// Write list
withList {
propertySerializers.serializationOrder.forEach { property ->
property.getter.writeProperty(obj, this, output, debugIndent+1)
property.getter.writeProperty(obj, this, output, debugIndent + 1)
}
}
}
@ -92,23 +100,23 @@ open class ObjectSerializer(val clazz: Type, factory: SerializerFactory) : AMQPS
private fun readObjectBuildViaConstructor(
obj: List<*>,
schemas: SerializationSchemas,
input: DeserializationInput) : Any = ifThrowsAppend({ clazz.typeName }){
input: DeserializationInput): Any = ifThrowsAppend({ clazz.typeName }) {
logger.trace { "Calling construction based construction for ${clazz.typeName}" }
return construct (propertySerializers.serializationOrder
return construct(propertySerializers.serializationOrder
.zip(obj)
.map { Pair(it.first.initialPosition, it.first.getter.readProperty(it.second, schemas, input)) }
.sortedWith(compareBy({it.first}))
.sortedWith(compareBy({ it.first }))
.map { it.second })
}
private fun readObjectBuildViaSetters(
obj: List<*>,
schemas: SerializationSchemas,
input: DeserializationInput) : Any = ifThrowsAppend({ clazz.typeName }){
input: DeserializationInput): Any = ifThrowsAppend({ clazz.typeName }) {
logger.trace { "Calling setter based construction for ${clazz.typeName}" }
val instance : Any = javaConstructor?.newInstance() ?: throw NotSerializableException (
val instance: Any = javaConstructor?.newInstance() ?: throw NotSerializableException(
"Failed to instantiate instance of object $clazz")
// read the properties out of the serialised form, since we're invoking the setters the order we
@ -136,7 +144,13 @@ open class ObjectSerializer(val clazz: Type, factory: SerializerFactory) : AMQPS
fun construct(properties: List<Any?>): Any {
logger.trace { "Calling constructor: '$javaConstructor' with properties '$properties'" }
return javaConstructor?.newInstance(*properties.toTypedArray()) ?:
throw NotSerializableException("Attempt to deserialize an interface: $clazz. Serialized form is invalid.")
if (properties.size != javaConstructor?.parameterCount) {
throw NotSerializableException("Serialization constructor for class $type expects "
+ "${javaConstructor?.parameterCount} parameters but we have ${properties.size} "
+ "serialized properties.")
}
return javaConstructor?.newInstance(*properties.toTypedArray())
?: throw NotSerializableException("Attempt to deserialize an interface: $clazz. Serialized form is invalid.")
}
}

View File

@ -17,6 +17,7 @@ import kotlin.reflect.KParameter
import kotlin.reflect.full.findAnnotation
import kotlin.reflect.full.primaryConstructor
import kotlin.reflect.jvm.isAccessible
import kotlin.reflect.jvm.javaConstructor
import kotlin.reflect.jvm.javaType
/**
@ -33,6 +34,7 @@ internal fun constructorForDeserialization(type: Type): KFunction<Any>? {
var annotatedCount = 0
val kotlinConstructors = clazz.kotlin.constructors
val hasDefault = kotlinConstructors.any { it.parameters.isEmpty() }
for (kotlinConstructor in kotlinConstructors) {
if (preferredCandidate == null && kotlinConstructors.size == 1) {
preferredCandidate = kotlinConstructor
@ -158,7 +160,7 @@ fun Class<out Any?>.propertyDescriptors(): Map<String, PropertyDescriptor> {
PropertyDescriptorsRegex.re.find(func.name)?.apply {
// matching means we have an func getX where the property could be x or X
// so having pre-loaded all of the properties we try to match to either case. If that
// fails the getter doesn't refer to a property directly, but may to a cosntructor
// fails the getter doesn't refer to a property directly, but may refer to a constructor
// parameter that shadows a property
val properties =
classProperties[groups[2]!!.value] ?:
@ -219,19 +221,26 @@ internal fun <T : Any> propertiesForSerializationFromConstructor(
val classProperties = clazz.propertyDescriptors()
// Annoyingly there isn't a better way to ascertain that the constructor for the class
// has a synthetic parameter inserted to capture the reference to the outer class. You'd
// think you could inspect the parameter and check the isSynthetic flag but that is always
// false so given the naming convention is specified by the standard we can just check for
// this
if (kotlinConstructor.javaConstructor?.parameterCount ?: 0 > 0 &&
kotlinConstructor.javaConstructor?.parameters?.get(0)?.name == "this$0"
) {
throw SyntheticParameterException(type)
}
if (classProperties.isNotEmpty() && kotlinConstructor.parameters.isEmpty()) {
return propertiesForSerializationFromSetters(classProperties, type, factory)
}
return mutableListOf<PropertyAccessor>().apply {
kotlinConstructor.parameters.withIndex().forEach { param ->
// If a parameter doesn't have a name *at all* then chances are it's a synthesised
// one. A good example of this is non static nested classes in Java where instances
// of the nested class require access to the outer class without breaking
// encapsulation. Thus a parameter is inserted into the constructor that passes a
// reference to the enclosing class. In this case we can't do anything with
// it so just ignore it as it'll be supplied at runtime anyway on invocation
val name = param.value.name ?: return@forEach
// name cannot be null, if it is then this is a synthetic field and we will have bailed
// out prior to this
val name = param.value.name!!
// We will already have disambiguated getA for property A or a but we still need to cope
// with the case we don't know the case of A when the parameter doesn't match a property

View File

@ -0,0 +1,224 @@
package net.corda.nodeapi.internal.serialization.amqp;
import com.google.common.collect.ImmutableList;
import kotlin.Suppress;
import net.corda.core.contracts.ContractState;
import net.corda.core.identity.AbstractParty;
import net.corda.core.serialization.SerializedBytes;
import net.corda.nodeapi.internal.serialization.AllWhitelist;
import org.assertj.core.api.Assertions;
import org.jetbrains.annotations.NotNull;
import org.junit.Test;
import java.io.NotSerializableException;
import java.util.List;
class OuterClass1 {
protected SerializationOutput ser;
DeserializationInput desExisting;
DeserializationInput desRegen;
OuterClass1() {
SerializerFactory factory1 = new SerializerFactory(AllWhitelist.INSTANCE, ClassLoader.getSystemClassLoader(),
new EvolutionSerializerGetter(),
new SerializerFingerPrinter());
SerializerFactory factory2 = new SerializerFactory(AllWhitelist.INSTANCE, ClassLoader.getSystemClassLoader(),
new EvolutionSerializerGetter(),
new SerializerFingerPrinter());
this.ser = new SerializationOutput(factory1);
this.desExisting = new DeserializationInput(factory1);
this.desRegen = new DeserializationInput(factory2);
}
class DummyState implements ContractState {
@Override @NotNull public List<AbstractParty> getParticipants() {
return ImmutableList.of();
}
}
public void run() throws NotSerializableException {
SerializedBytes b = ser.serialize(new DummyState());
desExisting.deserialize(b, DummyState.class);
desRegen.deserialize(b, DummyState.class);
}
}
class Inherator1 extends OuterClass1 {
public void iRun() throws NotSerializableException {
SerializedBytes b = ser.serialize(new DummyState());
desExisting.deserialize(b, DummyState.class);
desRegen.deserialize(b, DummyState.class);
}
}
class OuterClass2 {
protected SerializationOutput ser;
DeserializationInput desExisting;
DeserializationInput desRegen;
OuterClass2() {
SerializerFactory factory1 = new SerializerFactory(AllWhitelist.INSTANCE, ClassLoader.getSystemClassLoader(),
new EvolutionSerializerGetter(),
new SerializerFingerPrinter());
SerializerFactory factory2 = new SerializerFactory(AllWhitelist.INSTANCE, ClassLoader.getSystemClassLoader(),
new EvolutionSerializerGetter(),
new SerializerFingerPrinter());
this.ser = new SerializationOutput(factory1);
this.desExisting = new DeserializationInput(factory1);
this.desRegen = new DeserializationInput(factory2);
}
protected class DummyState implements ContractState {
private Integer count;
DummyState(Integer count) {
this.count = count;
}
@Override @NotNull public List<AbstractParty> getParticipants() {
return ImmutableList.of();
}
}
public void run() throws NotSerializableException {
SerializedBytes b = ser.serialize(new DummyState(12));
desExisting.deserialize(b, DummyState.class);
desRegen.deserialize(b, DummyState.class);
}
}
class Inherator2 extends OuterClass2 {
public void iRun() throws NotSerializableException {
SerializedBytes b = ser.serialize(new DummyState(12));
desExisting.deserialize(b, DummyState.class);
desRegen.deserialize(b, DummyState.class);
}
}
// Make the base class abstract
abstract class AbstractClass2 {
protected SerializationOutput ser;
AbstractClass2() {
SerializerFactory factory = new SerializerFactory(AllWhitelist.INSTANCE, ClassLoader.getSystemClassLoader(),
new EvolutionSerializerGetter(),
new SerializerFingerPrinter());
this.ser = new SerializationOutput(factory);
}
protected class DummyState implements ContractState {
@Override @NotNull public List<AbstractParty> getParticipants() {
return ImmutableList.of();
}
}
}
class Inherator4 extends AbstractClass2 {
public void run() throws NotSerializableException {
ser.serialize(new DummyState());
}
}
abstract class AbstractClass3 {
protected class DummyState implements ContractState {
@Override @NotNull public List<AbstractParty> getParticipants() {
return ImmutableList.of();
}
}
}
class Inherator5 extends AbstractClass3 {
public void run() throws NotSerializableException {
SerializerFactory factory = new SerializerFactory(AllWhitelist.INSTANCE, ClassLoader.getSystemClassLoader(),
new EvolutionSerializerGetter(),
new SerializerFingerPrinter());
SerializationOutput ser = new SerializationOutput(factory);
ser.serialize(new DummyState());
}
}
class Inherator6 extends AbstractClass3 {
public class Wrapper {
//@Suppress("UnusedDeclaration"])
private ContractState cState;
Wrapper(ContractState cState) {
this.cState = cState;
}
}
public void run() throws NotSerializableException {
SerializerFactory factory = new SerializerFactory(AllWhitelist.INSTANCE, ClassLoader.getSystemClassLoader(),
new EvolutionSerializerGetter(),
new SerializerFingerPrinter());
SerializationOutput ser = new SerializationOutput(factory);
ser.serialize(new Wrapper(new DummyState()));
}
}
public class JavaNestedClassesTests {
@Test
public void publicNested() {
Assertions.assertThatThrownBy(() -> new OuterClass1().run()).isInstanceOf(
NotSerializableException.class).hasMessageContaining(
"has synthetic fields and is likely a nested inner class");
}
@Test
public void privateNested() {
Assertions.assertThatThrownBy(() -> new OuterClass2().run()).isInstanceOf(
NotSerializableException.class).hasMessageContaining(
"has synthetic fields and is likely a nested inner class");
}
@Test
public void publicNestedInherited() {
Assertions.assertThatThrownBy(() -> new Inherator1().run()).isInstanceOf(
NotSerializableException.class).hasMessageContaining(
"has synthetic fields and is likely a nested inner class");
Assertions.assertThatThrownBy(() -> new Inherator1().iRun()).isInstanceOf(
NotSerializableException.class).hasMessageContaining(
"has synthetic fields and is likely a nested inner class");
}
@Test
public void protectedNestedInherited() {
Assertions.assertThatThrownBy(() -> new Inherator2().run()).isInstanceOf(
NotSerializableException.class).hasMessageContaining(
"has synthetic fields and is likely a nested inner class");
Assertions.assertThatThrownBy(() -> new Inherator2().iRun()).isInstanceOf(
NotSerializableException.class).hasMessageContaining(
"has synthetic fields and is likely a nested inner class");
}
@Test
public void abstractNested() {
Assertions.assertThatThrownBy(() -> new Inherator4().run()).isInstanceOf(
NotSerializableException.class).hasMessageContaining(
"has synthetic fields and is likely a nested inner class");
}
@Test
public void abstractNestedFactoryOnNested() {
Assertions.assertThatThrownBy(() -> new Inherator5().run()).isInstanceOf(
NotSerializableException.class).hasMessageContaining(
"has synthetic fields and is likely a nested inner class");
}
@Test
public void abstractNestedFactoryOnNestedInWrapper() {
Assertions.assertThatThrownBy(() -> new Inherator6().run()).isInstanceOf(
NotSerializableException.class).hasMessageContaining(
"has synthetic fields and is likely a nested inner class");
}
}

View File

@ -0,0 +1,70 @@
package net.corda.nodeapi.internal.serialization.amqp;
import com.google.common.collect.ImmutableList;
import net.corda.core.contracts.ContractState;
import net.corda.core.identity.AbstractParty;
import net.corda.nodeapi.internal.serialization.AllWhitelist;
import org.assertj.core.api.Assertions;
import org.jetbrains.annotations.NotNull;
import org.junit.Test;
import java.io.NotSerializableException;
import java.util.List;
abstract class JavaNestedInheritenceTestsBase {
class DummyState implements ContractState {
@Override @NotNull public List<AbstractParty> getParticipants() {
return ImmutableList.of();
}
}
}
class Wrapper {
private ContractState cs;
Wrapper(ContractState cs) { this.cs = cs; }
}
class TemplateWrapper<T> {
public T obj;
TemplateWrapper(T obj) { this.obj = obj; }
}
public class JavaNestedInheritenceTests extends JavaNestedInheritenceTestsBase {
@Test
public void serializeIt() {
SerializerFactory factory = new SerializerFactory(AllWhitelist.INSTANCE, ClassLoader.getSystemClassLoader(),
new EvolutionSerializerGetter(),
new SerializerFingerPrinter());
SerializationOutput ser = new SerializationOutput(factory);
Assertions.assertThatThrownBy(() -> ser.serialize(new DummyState())).isInstanceOf(
NotSerializableException.class).hasMessageContaining(
"has synthetic fields and is likely a nested inner class");
}
@Test
public void serializeIt2() {
SerializerFactory factory = new SerializerFactory(AllWhitelist.INSTANCE, ClassLoader.getSystemClassLoader(),
new EvolutionSerializerGetter(),
new SerializerFingerPrinter());
SerializationOutput ser = new SerializationOutput(factory);
Assertions.assertThatThrownBy(() -> ser.serialize(new Wrapper (new DummyState()))).isInstanceOf(
NotSerializableException.class).hasMessageContaining(
"has synthetic fields and is likely a nested inner class");
}
@Test
public void serializeIt3() throws NotSerializableException {
SerializerFactory factory1 = new SerializerFactory(AllWhitelist.INSTANCE, ClassLoader.getSystemClassLoader(),
new EvolutionSerializerGetter(),
new SerializerFingerPrinter());
SerializationOutput ser = new SerializationOutput(factory1);
Assertions.assertThatThrownBy(() -> ser.serialize(new TemplateWrapper<ContractState> (new DummyState()))).isInstanceOf(
NotSerializableException.class).hasMessageContaining(
"has synthetic fields and is likely a nested inner class");
}
}

View File

@ -8,6 +8,7 @@ import net.corda.nodeapi.internal.serialization.AllWhitelist;
import net.corda.core.serialization.SerializedBytes;
import org.apache.qpid.proton.codec.DecoderImpl;
import org.apache.qpid.proton.codec.EncoderImpl;
import org.jetbrains.annotations.NotNull;
import org.junit.Test;
import javax.annotation.Nonnull;
@ -111,10 +112,12 @@ public class JavaSerializationOutputTests {
this.count = count;
}
@SuppressWarnings("unused")
public String getFred() {
return fred;
}
@SuppressWarnings("unused")
public Integer getCount() {
return count;
}
@ -242,24 +245,4 @@ public class JavaSerializationOutputTests {
BoxedFooNotNull obj = new BoxedFooNotNull("Hello World!", 123);
serdes(obj);
}
protected class DummyState implements ContractState {
@Override
public List<AbstractParty> getParticipants() {
return ImmutableList.of();
}
}
@Test
public void dummyStateSerialize() throws NotSerializableException {
SerializerFactory factory1 = new SerializerFactory(
AllWhitelist.INSTANCE,
ClassLoader.getSystemClassLoader(),
new EvolutionSerializerGetter(),
new SerializerFingerPrinter());
SerializationOutput serializer = new SerializationOutput(factory1);
serializer.serialize(new DummyState());
}
}

View File

@ -1231,7 +1231,86 @@ class SerializationOutputTests(private val compression: CordaSerializationEncodi
val testExcp = TestException("hello", Throwable().apply { stackTrace = Thread.currentThread().stackTrace })
val factory = testDefaultFactoryNoEvolution()
SerializationOutput(factory).serialize(testExcp)
}
@Test
fun nestedInner() {
class C(val a: Int) {
inner class D(val b: Int)
fun serialize() {
val factory = testDefaultFactoryNoEvolution()
SerializationOutput(factory).serialize(D(4))
}
}
// By the time we escape the serializer we should just have a general
// NotSerializable Exception
assertThatExceptionOfType(NotSerializableException::class.java).isThrownBy {
C(12).serialize()
}.withMessageContaining("has synthetic fields and is likely a nested inner class")
}
@Test
fun nestedNestedInner() {
class C(val a: Int) {
inner class D(val b: Int) {
inner class E(val c: Int)
fun serialize() {
val factory = testDefaultFactoryNoEvolution()
SerializationOutput(factory).serialize(E(4))
}
}
fun serializeD() {
val factory = testDefaultFactoryNoEvolution()
SerializationOutput(factory).serialize(D(4))
}
fun serializeE() {
D(1).serialize()
}
}
// By the time we escape the serializer we should just have a general
// NotSerializable Exception
assertThatExceptionOfType(NotSerializableException::class.java).isThrownBy {
C(12).serializeD()
}.withMessageContaining("has synthetic fields and is likely a nested inner class")
assertThatExceptionOfType(NotSerializableException::class.java).isThrownBy {
C(12).serializeE()
}.withMessageContaining("has synthetic fields and is likely a nested inner class")
}
@Test
fun multiNestedInner() {
class C(val a: Int) {
inner class D(val b: Int)
inner class E(val c: Int)
fun serializeD() {
val factory = testDefaultFactoryNoEvolution()
SerializationOutput(factory).serialize(D(4))
}
fun serializeE() {
val factory = testDefaultFactoryNoEvolution()
SerializationOutput(factory).serialize(E(4))
}
}
// By the time we escape the serializer we should just have a general
// NotSerializable Exception
assertThatExceptionOfType(NotSerializableException::class.java).isThrownBy {
C(12).serializeD()
}.withMessageContaining("has synthetic fields and is likely a nested inner class")
assertThatExceptionOfType(NotSerializableException::class.java).isThrownBy {
C(12).serializeE()
}.withMessageContaining("has synthetic fields and is likely a nested inner class")
}
}