diff --git a/compiler/src/jdk.internal.vm.compiler/src/org/graalvm/compiler/java/LambdaUtils.java b/compiler/src/jdk.internal.vm.compiler/src/org/graalvm/compiler/java/LambdaUtils.java index 1e30a50ea103..fbda3673e36a 100644 --- a/compiler/src/jdk.internal.vm.compiler/src/org/graalvm/compiler/java/LambdaUtils.java +++ b/compiler/src/jdk.internal.vm.compiler/src/org/graalvm/compiler/java/LambdaUtils.java @@ -176,4 +176,8 @@ public static String digest(String value) { throw new JVMCIError(ex); } } + + public static String capturingClass(String className) { + return className.split(LambdaUtils.SERIALIZATION_TEST_LAMBDA_CLASS_SPLIT_PATTERN)[0]; + } } diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/MissingRegistrationUtils.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/MissingRegistrationUtils.java index 695d6e8ad042..ec60a51a9e77 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/MissingRegistrationUtils.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/MissingRegistrationUtils.java @@ -34,6 +34,8 @@ public final class MissingRegistrationUtils { + public static final String ERROR_EMPHASIS_INDENT = " "; + public static boolean throwMissingRegistrationErrors() { return SubstrateOptions.ThrowMissingRegistrationErrors.hasBeenSet(); } diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/hub/DynamicHub.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/hub/DynamicHub.java index 11c64d298ec5..fc0871a2fab6 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/hub/DynamicHub.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/hub/DynamicHub.java @@ -257,6 +257,8 @@ public final class DynamicHub implements AnnotatedElement, java.lang.reflect.Typ /** Is the class a proxy class according to {@link java.lang.reflect.Proxy#isProxyClass}? */ private static final int IS_PROXY_CLASS_BIT = 2; + private static final int IS_REGISTERED_FOR_SERIALIZATION = 3; + /** * The {@link Modifier modifiers} of this class. */ @@ -436,7 +438,8 @@ public void setClassInitializationInfo(ClassInitializationInfo classInitializati @Platforms(Platform.HOSTED_ONLY.class) public void setData(int layoutEncoding, int typeID, int monitorOffset, int optionalIdentityHashOffset, short typeCheckStart, short typeCheckRange, short typeCheckSlot, - short[] typeCheckSlots, CFunctionPointer[] vtable, long referenceMapIndex, boolean isInstantiated, boolean canInstantiateAsInstance, boolean isProxyClass) { + short[] typeCheckSlots, CFunctionPointer[] vtable, long referenceMapIndex, boolean isInstantiated, boolean canInstantiateAsInstance, boolean isProxyClass, + boolean isRegisteredForSerialization) { assert this.vtable == null : "Initialization must be called only once"; assert !(!isInstantiated && canInstantiateAsInstance); if (LayoutEncoding.isPureInstance(layoutEncoding)) { @@ -462,7 +465,8 @@ public void setData(int layoutEncoding, int typeID, int monitorOffset, int optio this.referenceMapIndex = (int) referenceMapIndex; this.additionalFlags = NumUtil.safeToUByte(makeFlag(IS_INSTANTIATED_BIT, isInstantiated) | makeFlag(CAN_INSTANTIATE_AS_INSTANCE_BIT, canInstantiateAsInstance) | - makeFlag(IS_PROXY_CLASS_BIT, isProxyClass)); + makeFlag(IS_PROXY_CLASS_BIT, isProxyClass) | + makeFlag(IS_REGISTERED_FOR_SERIALIZATION, isRegisteredForSerialization)); } @Platforms(Platform.HOSTED_ONLY.class) @@ -883,6 +887,10 @@ public boolean isLambdaFormHidden() { return isFlagSet(flags, IS_LAMBDA_FORM_HIDDEN_BIT); } + public boolean isRegisteredForSerialization() { + return isFlagSet(additionalFlags, IS_REGISTERED_FOR_SERIALIZATION); + } + @KeepOriginal private native boolean isLocalClass(); diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/JavaIOSubstitutions.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/JavaIOSubstitutions.java index 358bb5c18a51..85d73f59deaa 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/JavaIOSubstitutions.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/JavaIOSubstitutions.java @@ -25,10 +25,17 @@ package com.oracle.svm.core.jdk; import java.io.Closeable; +import java.io.ObjectStreamClass; +import java.io.Serializable; import java.lang.ref.ReferenceQueue; +import java.lang.reflect.Proxy; +import java.util.Arrays; import java.util.List; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; +import java.util.stream.Collectors; + +import org.graalvm.compiler.java.LambdaUtils; import com.oracle.svm.core.annotate.Alias; import com.oracle.svm.core.annotate.RecomputeFieldValue; @@ -38,6 +45,7 @@ import com.oracle.svm.core.annotate.TargetElement; import com.oracle.svm.core.fieldvaluetransformer.NewInstanceFieldValueTransformer; import com.oracle.svm.core.hub.DynamicHub; +import com.oracle.svm.core.reflect.serialize.MissingSerializationRegistrationUtils; @TargetClass(java.io.FileDescriptor.class) final class Target_java_io_FileDescriptor { @@ -53,16 +61,47 @@ final class Target_java_io_ObjectStreamClass { private static boolean hasStaticInitializer(Class cl) { return DynamicHub.fromClass(cl).getClassInitializationInfo().hasInitializer(); } + + @Substitute + static ObjectStreamClass lookup(Class cl, boolean all) { + if (!(all || Serializable.class.isAssignableFrom(cl))) { + return null; + } + + if (Serializable.class.isAssignableFrom(cl)) { + if (!DynamicHub.fromClass(cl).isRegisteredForSerialization()) { + boolean isLambda = cl.getTypeName().contains(LambdaUtils.LAMBDA_CLASS_NAME_SUBSTRING); + boolean isProxy = Proxy.isProxyClass(cl); + if (isProxy || isLambda) { + var interfaceList = Arrays.stream(cl.getInterfaces()) + .map(Class::getTypeName) + .collect(Collectors.joining(", ", "[", "]")); + if (isProxy) { + MissingSerializationRegistrationUtils.missingSerializationRegistration(cl, "proxy type implementing interfaces: " + interfaceList); + } else { + MissingSerializationRegistrationUtils.missingSerializationRegistration(cl, + "lambda declared in: " + LambdaUtils.capturingClass(cl.getTypeName()), + "extending interfaces: " + interfaceList); + } + } else { + MissingSerializationRegistrationUtils.missingSerializationRegistration(cl, "type " + cl.getTypeName()); + } + } + } + + return Target_java_io_ObjectStreamClass_Caches.localDescs0.get(cl); + } + } @TargetClass(value = java.io.ObjectStreamClass.class, innerClass = "Caches") final class Target_java_io_ObjectStreamClass_Caches { - @TargetElement(onlyWith = JavaIOClassCachePresent.class, name = "localDescs") @Alias @RecomputeFieldValue(kind = Kind.Custom, declClass = NewInstanceFieldValueTransformer.class) static Target_java_io_ClassCache localDescs0; + @TargetElement(onlyWith = JavaIOClassCachePresent.class, name = "localDescs") @Alias @RecomputeFieldValue(kind = Kind.Custom, declClass = NewInstanceFieldValueTransformer.class) static Target_java_io_ClassCache localDescs0; - @TargetElement(onlyWith = JavaIOClassCachePresent.class, name = "reflectors") @Alias @RecomputeFieldValue(kind = Kind.Custom, declClass = NewInstanceFieldValueTransformer.class) static Target_java_io_ClassCache reflectors0; + @TargetElement(onlyWith = JavaIOClassCachePresent.class, name = "reflectors") @Alias @RecomputeFieldValue(kind = Kind.Custom, declClass = NewInstanceFieldValueTransformer.class) static Target_java_io_ClassCache reflectors0; - @TargetElement(onlyWith = JavaIOClassCacheAbsent.class) @Alias @RecomputeFieldValue(kind = Kind.NewInstance, declClass = ConcurrentHashMap.class) static ConcurrentMap localDescs; + @TargetElement(onlyWith = JavaIOClassCacheAbsent.class) @Alias @RecomputeFieldValue(kind = Kind.NewInstance, declClass = ConcurrentHashMap.class) static ConcurrentMap localDescs; @TargetElement(onlyWith = JavaIOClassCacheAbsent.class) @Alias @RecomputeFieldValue(kind = Kind.NewInstance, declClass = ConcurrentHashMap.class) static ConcurrentMap reflectors; @@ -72,7 +111,9 @@ final class Target_java_io_ObjectStreamClass_Caches { } @TargetClass(className = "java.io.ClassCache", onlyWith = JavaIOClassCachePresent.class) -final class Target_java_io_ClassCache { +final class Target_java_io_ClassCache { + @Alias + native T get(Class cl); } /** Dummy class to have a class with the file's name. */ diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/reflect/serialize/MissingSerializationRegistrationError.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/reflect/serialize/MissingSerializationRegistrationError.java new file mode 100644 index 000000000000..0fa28f1231f5 --- /dev/null +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/reflect/serialize/MissingSerializationRegistrationError.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.core.reflect.serialize; + +import java.io.Serial; + +/** + * Error thrown when types are not registered + * for serialization or deserialization. + *

+ * The purpose of this exception is to easily discover unregistered elements and to assure that all + * serialization or deserialization operations have expected behavior. + */ +public final class MissingSerializationRegistrationError extends Error { + @Serial private static final long serialVersionUID = 2764341882856270641L; + private final Class culprit; + + public MissingSerializationRegistrationError(String message, Class cl) { + super(message); + this.culprit = cl; + } + + /** + * Returns the class that was not registered for serialization or deserialization. + */ + public Class getCulprit() { + return culprit; + } +} diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/reflect/serialize/MissingSerializationRegistrationUtils.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/reflect/serialize/MissingSerializationRegistrationUtils.java new file mode 100644 index 000000000000..049df6208a9d --- /dev/null +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/reflect/serialize/MissingSerializationRegistrationUtils.java @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.core.reflect.serialize; + +import static com.oracle.svm.core.MissingRegistrationUtils.ERROR_EMPHASIS_INDENT; + +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.ObjectStreamClass; +import java.util.Arrays; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import com.oracle.svm.core.MissingRegistrationUtils; + +public final class MissingSerializationRegistrationUtils { + + public static void missingSerializationRegistration(Class cl, String... msg) { + report(new MissingSerializationRegistrationError(errorMessage(msg), cl)); + } + + private static String errorMessage(String... type) { + var typeStr = Arrays.stream(type).collect(Collectors.joining(System.lineSeparator(), ERROR_EMPHASIS_INDENT, "")); + return """ + The program tried to serialize or deserialize + + %s + + without it being registered for serialization. Add this class to the serialization metadata to solve this problem. + See https://www.graalvm.org/latest/reference-manual/native-image/metadata/#serialization for help + """.replaceAll("\n", System.lineSeparator()) + .formatted(typeStr); + } + + private static void report(MissingSerializationRegistrationError exception) { + StackTraceElement responsibleClass = getResponsibleClass(exception); + MissingRegistrationUtils.report(exception, responsibleClass); + } + + /* + * This is a list of all public JDK methods that end up potentially throwing missing + * registration errors. This should be implemented using wrapping substitutions once they are + * available. + */ + private static final Map> serializationEntryPoints = Map.of( + ObjectOutputStream.class.getTypeName(), Set.of("writeObject", "writeUnshared"), + ObjectInputStream.class.getTypeName(), Set.of("readObject", "readUnshared"), + ObjectStreamClass.class.getTypeName(), Set.of("lookup"), + "sun.reflect.ReflectionFactory", Set.of("newConstructorForSerialization"), + "jdk.internal.reflect.ReflectionFactory", Set.of("newConstructorForSerialization")); + + private static StackTraceElement getResponsibleClass(MissingSerializationRegistrationError t) { + StackTraceElement[] stackTrace = t.getStackTrace(); + boolean previous = false; + StackTraceElement lastElem = null; + for (StackTraceElement stackTraceElement : stackTrace) { + if (previous) { + previous = false; + lastElem = stackTraceElement; + } + if (serializationEntryPoints.getOrDefault(stackTraceElement.getClassName(), Set.of()) + .contains(stackTraceElement.getMethodName())) { + previous = true; + } + } + return lastElem; + } +} diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/reflect/serialize/SerializationRegistry.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/reflect/serialize/SerializationRegistry.java index 40134807fc71..6547a178e55b 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/reflect/serialize/SerializationRegistry.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/reflect/serialize/SerializationRegistry.java @@ -27,6 +27,8 @@ public interface SerializationRegistry { + boolean isRegisteredForSerialization(Class cl); + Object getSerializationConstructorAccessor(Class serializationTargetClass, Class targetConstructorClass); } diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/reflect/serialize/SerializationSupport.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/reflect/serialize/SerializationSupport.java index d83019ae3248..8c3a2577f17e 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/reflect/serialize/SerializationSupport.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/reflect/serialize/SerializationSupport.java @@ -31,14 +31,13 @@ import java.lang.reflect.Modifier; import java.util.Map; import java.util.Objects; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import org.graalvm.compiler.java.LambdaUtils; import org.graalvm.nativeimage.Platform; import org.graalvm.nativeimage.Platforms; -import com.oracle.svm.core.util.VMError; - public class SerializationSupport implements SerializationRegistry { /** @@ -125,6 +124,19 @@ public Object addConstructorAccessor(Class declaringClass, Class targetCon return constructorAccessors.putIfAbsent(key, constructorAccessor); } + @Platforms(Platform.HOSTED_ONLY.class) private final Set> classes = ConcurrentHashMap.newKeySet(); + + @Platforms(Platform.HOSTED_ONLY.class) + public void registerSerializationTargetClass(Class serializationTargetClass) { + classes.add(serializationTargetClass); + } + + @Override + @Platforms(Platform.HOSTED_ONLY.class) + public boolean isRegisteredForSerialization(Class cl) { + return classes.contains(cl); + } + @Override public Object getSerializationConstructorAccessor(Class rawDeclaringClass, Class rawTargetConstructorClass) { Class declaringClass = rawDeclaringClass; @@ -140,9 +152,9 @@ public Object getSerializationConstructorAccessor(Class rawDeclaringClass, Cl return constructorAccessor; } else { String targetConstructorClassName = targetConstructorClass.getName(); - throw VMError.unsupportedFeature("SerializationConstructorAccessor class not found for declaringClass: " + declaringClass.getName() + - " (targetConstructorClass: " + targetConstructorClassName + "). Usually adding " + declaringClass.getName() + - " to serialization-config.json fixes the problem."); + MissingSerializationRegistrationUtils.missingSerializationRegistration(declaringClass, + "type " + declaringClass.getName() + " with target constructor class: " + targetConstructorClassName); + return null; } } } diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/meta/UniverseBuilder.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/meta/UniverseBuilder.java index 812b61c95bac..39baf9f87627 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/meta/UniverseBuilder.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/meta/UniverseBuilder.java @@ -89,6 +89,7 @@ import com.oracle.svm.core.meta.MethodPointer; import com.oracle.svm.core.reflect.SubstrateConstructorAccessor; import com.oracle.svm.core.reflect.SubstrateMethodAccessor; +import com.oracle.svm.core.reflect.serialize.SerializationRegistry; import com.oracle.svm.core.util.VMError; import com.oracle.svm.hosted.FeatureImpl.BeforeAnalysisAccessImpl; import com.oracle.svm.hosted.HostedConfiguration; @@ -1071,8 +1072,10 @@ private void buildHubs() { boolean isProxyClass = ImageSingletons.lookup(DynamicProxyRegistry.class).isProxyClass(type.getJavaClass()); DynamicHub hub = type.getHub(); + SerializationRegistry s = ImageSingletons.lookup(SerializationRegistry.class); hub.setData(layoutHelper, type.getTypeID(), monitorOffset, optionalIdHashOffset, type.getTypeCheckStart(), type.getTypeCheckRange(), - type.getTypeCheckSlot(), type.getTypeCheckSlots(), vtable, referenceMapIndex, type.isInstantiated(), canInstantiateAsInstance, isProxyClass); + type.getTypeCheckSlot(), type.getTypeCheckSlots(), vtable, referenceMapIndex, type.isInstantiated(), canInstantiateAsInstance, isProxyClass, + s.isRegisteredForSerialization(type.getJavaClass())); } } diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/reflect/serialize/SerializationFeature.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/reflect/serialize/SerializationFeature.java index 3f1d719a883a..c665eeff3124 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/reflect/serialize/SerializationFeature.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/reflect/serialize/SerializationFeature.java @@ -34,10 +34,10 @@ import java.lang.invoke.SerializedLambda; import java.lang.reflect.Constructor; import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Member; import java.lang.reflect.Method; import java.lang.reflect.Modifier; -import java.lang.reflect.Proxy; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; @@ -84,7 +84,6 @@ import com.oracle.svm.hosted.ConfigurationTypeResolver; import com.oracle.svm.hosted.FallbackFeature; import com.oracle.svm.hosted.FeatureImpl; -import com.oracle.svm.hosted.FeatureImpl.BeforeAnalysisAccessImpl; import com.oracle.svm.hosted.ImageClassLoader; import com.oracle.svm.hosted.config.ConfigurationParserUtils; import com.oracle.svm.hosted.reflect.RecordUtils; @@ -197,35 +196,30 @@ private static Class getLambdaClassFromConstantNode(ConstantNode constantNode return lambdaClass.getName().contains(LambdaUtils.LAMBDA_CLASS_NAME_SUBSTRING) ? lambdaClass : null; } - private static void registerLambdasFromConstantNodesInGraph(StructuredGraph graph) { + private static void registerLambdasFromConstantNodesInGraph(StructuredGraph graph, SerializationBuilder serializationBuilder) { NodeIterable constantNodes = ConstantNode.getConstantNodes(graph); for (ConstantNode cNode : constantNodes) { Class lambdaClass = getLambdaClassFromConstantNode(cNode); if (lambdaClass != null && Serializable.class.isAssignableFrom(lambdaClass)) { - try { - Method serializeLambdaMethod = lambdaClass.getDeclaredMethod("writeReplace"); - RuntimeReflection.register(serializeLambdaMethod); - } catch (NoSuchMethodException e) { - throw VMError.shouldNotReachHere("Serializable lambda class must contain the writeReplace method."); - } + RuntimeReflection.register(ReflectionUtil.lookupMethod(lambdaClass, "writeReplace")); + SerializationBuilder.registerSerializationUIDElements(lambdaClass, false); + serializationBuilder.serializationSupport.registerSerializationTargetClass(lambdaClass); } } } @SuppressWarnings("try") - private static void registerLambdasFromMethod(ResolvedJavaMethod method, OptionValues options) { + private static void registerLambdasFromMethod(ResolvedJavaMethod method, SerializationBuilder serializationBuilder, OptionValues options) { GraphBuilderPhase lambdaParserPhase = new GraphBuilderPhase(buildLambdaParserConfig()); StructuredGraph graph = createMethodGraph(method, lambdaParserPhase, options); - registerLambdasFromConstantNodesInGraph(graph); + registerLambdasFromConstantNodesInGraph(graph, serializationBuilder); } @Override public void beforeAnalysis(BeforeAnalysisAccess access) { serializationBuilder.flushConditionalConfiguration(access); - /* Ensure SharedSecrets.javaObjectInputStreamAccess is initialized before scanning. */ - ((BeforeAnalysisAccessImpl) access).ensureInitialized("java.io.ObjectInputStream"); } @Override @@ -245,7 +239,7 @@ public void duringAnalysis(DuringAnalysisAccess access) { .map(metaAccess::lookupJavaType) .flatMap(SerializationFeature::allExecutablesDeclaredInClass) .filter(m -> m.getCode() != null) - .forEach(m -> registerLambdasFromMethod(m, options)); + .forEach(m -> registerLambdasFromMethod(m, serializationBuilder, options)); capturingClasses.clear(); } @@ -341,19 +335,24 @@ final class SerializationBuilder extends ConditionalConfigurationRegistry implem private final Field descField; private final Method getDataLayoutMethod; - private final SerializationSupport serializationSupport; + final SerializationSupport serializationSupport; private final SerializationDenyRegistry denyRegistry; private final ConfigurationTypeResolver typeResolver; private final FeatureImpl.DuringSetupAccessImpl access; + private final Method disableSerialConstructorChecks; + private final Method superHasAccessibleConstructor; private boolean sealed; private final ProxyRegistry proxyRegistry; SerializationBuilder(SerializationDenyRegistry serializationDenyRegistry, FeatureImpl.DuringSetupAccessImpl access, ConfigurationTypeResolver typeResolver, ProxyRegistry proxyRegistry) { this.access = access; Class classDataSlotClazz = access.findClassByName("java.io.ObjectStreamClass$ClassDataSlot"); - descField = ReflectionUtil.lookupField(classDataSlotClazz, "desc"); - getDataLayoutMethod = ReflectionUtil.lookupMethod(ObjectStreamClass.class, "getClassDataLayout"); - stubConstructor = newConstructorForSerialization(SerializationSupport.StubForAbstractClass.class, null); + this.descField = ReflectionUtil.lookupField(classDataSlotClazz, "desc"); + this.getDataLayoutMethod = ReflectionUtil.lookupMethod(ObjectStreamClass.class, "getClassDataLayout"); + this.stubConstructor = newConstructorForSerialization(SerializationSupport.StubForAbstractClass.class, null); + this.disableSerialConstructorChecks = ReflectionUtil.lookupMethod(true, ReflectionFactory.class, "disableSerialConstructorChecks"); + this.superHasAccessibleConstructor = ReflectionUtil.lookupMethod(ReflectionFactory.class, "superHasAccessibleConstructor", Class.class); + this.denyRegistry = serializationDenyRegistry; this.typeResolver = typeResolver; this.proxyRegistry = proxyRegistry; @@ -436,20 +435,20 @@ public void registerLambdaCapturingClass(ConfigurationCondition condition, Strin return; } - Class serializationTargetClass = typeResolver.resolveType(lambdaCapturingClassName); - if (serializationTargetClass == null || serializationTargetClass.isPrimitive() || serializationTargetClass.isArray()) { + Class lambdaCapturingClass = typeResolver.resolveType(lambdaCapturingClassName); + if (lambdaCapturingClass == null || lambdaCapturingClass.isPrimitive() || lambdaCapturingClass.isArray()) { return; } - if (ReflectionUtil.lookupMethod(true, serializationTargetClass, "$deserializeLambda$", SerializedLambda.class) == null) { - LogUtils.warning("Could not register %s for lambda serialization as it does not capture any serializable lambda.", serializationTargetClass); + if (ReflectionUtil.lookupMethod(true, lambdaCapturingClass, "$deserializeLambda$", SerializedLambda.class) == null) { + LogUtils.warning("Could not register %s for lambda serialization as it does not capture any serializable lambda.", lambdaCapturingClass); return; } registerConditionalConfiguration(condition, () -> { - ImageSingletons.lookup(SerializationFeature.class).capturingClasses.add(serializationTargetClass); - RuntimeReflection.register(serializationTargetClass); - RuntimeReflection.register(ReflectionUtil.lookupMethod(serializationTargetClass, "$deserializeLambda$", SerializedLambda.class)); + ImageSingletons.lookup(SerializationFeature.class).capturingClasses.add(lambdaCapturingClass); + RuntimeReflection.register(lambdaCapturingClass); + RuntimeReflection.register(ReflectionUtil.lookupMethod(lambdaCapturingClass, "$deserializeLambda$", SerializedLambda.class)); }); } @@ -517,30 +516,106 @@ public void registerWithTargetConstructorClass(ConfigurationCondition condition, Optional.ofNullable(addConstructorAccessor(serializationTargetClass, customTargetConstructorClass)) .map(ReflectionUtil::lookupConstructor) .ifPresent(RuntimeReflection::register); + + Class superclass = serializationTargetClass.getSuperclass(); + if (superclass != null) { + RuntimeReflection.registerAllDeclaredConstructors(superclass); + RuntimeReflection.registerMethodLookup(superclass, "writeReplace"); + RuntimeReflection.registerMethodLookup(superclass, "readResolve"); + } + registerForSerialization(serializationTargetClass); registerForDeserialization(serializationTargetClass); }); } } - private static void registerForSerialization(Class serializationTargetClass) { + private static void registerQueriesForInheritableMethod(Class clazz, String methodName, Class... args) { + Class iter = clazz; + while (iter != null) { + RuntimeReflection.registerMethodLookup(iter, methodName, args); + Method method = ReflectionUtil.lookupMethod(true, clazz, methodName, args); + if (method != null) { + RuntimeReflection.register(method); + break; + } + iter = iter.getSuperclass(); + } + } + + private static void registerMethod(Class clazz, String methodName, Class... args) { + Method method = ReflectionUtil.lookupMethod(true, clazz, methodName, args); + if (method != null) { + RuntimeReflection.register(method); + } else { + RuntimeReflection.registerMethodLookup(clazz, methodName, args); + } + } + + private void registerForSerialization(Class serializationTargetClass) { - /* Proxy classes have special treatment so no registration needed */ - if (Serializable.class.isAssignableFrom(serializationTargetClass) && !Proxy.isProxyClass(serializationTargetClass)) { + if (Serializable.class.isAssignableFrom(serializationTargetClass)) { /* * ObjectStreamClass.computeDefaultSUID is always called at runtime to verify * serialization class consistency, so need to register all constructors, methods and * fields. */ + registerSerializationUIDElements(serializationTargetClass, true); + + /* + * Required by jdk.internal.reflect.ReflectionFactory.newConstructorForSerialization + */ + Class initCl = serializationTargetClass; + boolean initClValid = true; + while (Serializable.class.isAssignableFrom(initCl)) { + Class prev = initCl; + RuntimeReflection.registerAllDeclaredConstructors(initCl); + try { + if ((initCl = initCl.getSuperclass()) == null || + (!(boolean) disableSerialConstructorChecks.invoke(null) && + !prev.isArray() && + !(Boolean) superHasAccessibleConstructor.invoke(ReflectionFactory.getReflectionFactory(), prev))) { + initClValid = false; + break; + } + } catch (InvocationTargetException | IllegalAccessException e) { + throw VMError.shouldNotReachHere(e); + } + } + + if (initClValid) { + RuntimeReflection.registerAllDeclaredConstructors(initCl); + } + + Class iter = serializationTargetClass; + while (iter != null) { + Arrays.stream(iter.getDeclaredFields()).map(Field::getType).forEach(type -> { + RuntimeReflection.registerAllDeclaredMethods(type); + RuntimeReflection.registerAllDeclaredFields(type); + RuntimeReflection.registerAllDeclaredConstructors(type); + }); + iter = iter.getSuperclass(); + } + } + + registerQueriesForInheritableMethod(serializationTargetClass, "writeReplace"); + registerQueriesForInheritableMethod(serializationTargetClass, "readResolve"); + registerMethod(serializationTargetClass, "writeObject", ObjectOutputStream.class); + registerMethod(serializationTargetClass, "readObjectNoData"); + registerMethod(serializationTargetClass, "readObject", ObjectInputStream.class); + } + + static void registerSerializationUIDElements(Class serializationTargetClass, boolean fullyRegister) { + RuntimeReflection.registerAllDeclaredConstructors(serializationTargetClass); + RuntimeReflection.registerAllDeclaredMethods(serializationTargetClass); + RuntimeReflection.registerAllDeclaredFields(serializationTargetClass); + if (fullyRegister) { + /* This is here a legacy that we can't remove as it is a breaking change */ RuntimeReflection.register(serializationTargetClass.getDeclaredConstructors()); RuntimeReflection.register(serializationTargetClass.getDeclaredMethods()); RuntimeReflection.register(serializationTargetClass.getDeclaredFields()); } - - Optional.ofNullable(ReflectionUtil.lookupMethod(true, serializationTargetClass, "writeReplace")) - .ifPresent(RuntimeReflection::register); - Optional.ofNullable(ReflectionUtil.lookupMethod(true, serializationTargetClass, "writeObject", ObjectOutputStream.class)) - .ifPresent(RuntimeReflection::register); + RuntimeReflection.registerFieldLookup(serializationTargetClass, "serialPersistentFields"); } public void afterAnalysis() { @@ -561,14 +636,11 @@ private static void registerForDeserialization(Class serializationTargetClass RuntimeReflection.registerAllRecordComponents(serializationTargetClass); RuntimeReflection.register(RecordUtils.getRecordComponentAccessorMethods(serializationTargetClass)); } else if (Externalizable.class.isAssignableFrom(serializationTargetClass)) { - Optional.ofNullable(ReflectionUtil.lookupConstructor(true, serializationTargetClass, (Class[]) null)) - .ifPresent(RuntimeReflection::register); + RuntimeReflection.registerConstructorLookup(serializationTargetClass); } - Optional.ofNullable(ReflectionUtil.lookupMethod(true, serializationTargetClass, "readObject", ObjectInputStream.class)) - .ifPresent(RuntimeReflection::register); - Optional.ofNullable(ReflectionUtil.lookupMethod(true, serializationTargetClass, "readResolve")) - .ifPresent(RuntimeReflection::register); + registerMethod(serializationTargetClass, "readObject", ObjectInputStream.class); + registerMethod(serializationTargetClass, "readResolve"); } private static Constructor newConstructorForSerialization(Class serializationTargetClass, Constructor customConstructorToCall) { @@ -596,6 +668,7 @@ private static Constructor getExternalizableConstructor(Class serializatio } Class addConstructorAccessor(Class serializationTargetClass, Class customTargetConstructorClass) { + serializationSupport.registerSerializationTargetClass(serializationTargetClass); if (serializationTargetClass.isArray() || Enum.class.isAssignableFrom(serializationTargetClass)) { return null; } diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/snippets/SubstrateGraphBuilderPlugins.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/snippets/SubstrateGraphBuilderPlugins.java index 90eada4913f5..202090effd98 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/snippets/SubstrateGraphBuilderPlugins.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/snippets/SubstrateGraphBuilderPlugins.java @@ -329,7 +329,7 @@ private static void parsePatternAndRegister(String pattern) { if (!negate) { if (className.endsWith(LambdaUtils.SERIALIZATION_TEST_LAMBDA_CLASS_SUBSTRING)) { try { - String lambdaHolderName = className.split(LambdaUtils.SERIALIZATION_TEST_LAMBDA_CLASS_SPLIT_PATTERN)[0]; + String lambdaHolderName = LambdaUtils.capturingClass(className); RuntimeSerialization.registerLambdaCapturingClass(Class.forName(lambdaHolderName, false, Thread.currentThread().getContextClassLoader())); } catch (ClassNotFoundException e) { // no class, no registration @@ -338,13 +338,17 @@ private static void parsePatternAndRegister(String pattern) { } } } else { - final String name = p.substring(poffset); + String name = p.substring(poffset); if (name.isEmpty()) { return; } // Pattern is a class name if (!negate) { try { + /* Support arrays of non-primitive types */ + if (name.startsWith("[") && name.contains("[L") && !name.endsWith(";")) { + name += ";"; + } RuntimeSerialization.register(Class.forName(name, false, Thread.currentThread().getContextClassLoader())); } catch (ClassNotFoundException e) { // no class, no registration diff --git a/truffle/src/com.oracle.truffle.api.test/src/META-INF/native-image/reflect-config.json b/truffle/src/com.oracle.truffle.api.test/src/META-INF/native-image/reflect-config.json index fbd3c0258b07..6a4000639f9d 100644 --- a/truffle/src/com.oracle.truffle.api.test/src/META-INF/native-image/reflect-config.json +++ b/truffle/src/com.oracle.truffle.api.test/src/META-INF/native-image/reflect-config.json @@ -178,7 +178,8 @@ { "name": "getName", "parameterTypes": [] }, { "name": "getSuperclass", "parameterTypes": [] }, { "name": "getDeclaredField", "parameterTypes": ["java.lang.String"] }, - { "name": "isInstance", "parameterTypes": ["java.lang.Object"] } + { "name": "isInstance", "parameterTypes": ["java.lang.Object"] }, + { "name": "toString", "parameterTypes": [] } ] }, {