diff --git a/docs/reference-manual/native-image/ReachabilityMetadata.md b/docs/reference-manual/native-image/ReachabilityMetadata.md index fd20c38b8e5a..31ac83047959 100644 --- a/docs/reference-manual/native-image/ReachabilityMetadata.md +++ b/docs/reference-manual/native-image/ReachabilityMetadata.md @@ -442,6 +442,41 @@ The schema also includes further details and explanations how this configuration ] ``` +## Strict Metadata Mode + +Native Image's strict metadata mode helps ensure the correctness and composability of the Native Image metadata, by strengthening the metadata requirements for reflection queries. +This mode can be activated with the `-H:ThrowMissingRegistrationErrors=` option and requires the following additional registrations over the default: + +### Reflection + +* If a reflectively-accessed element (`Class`, `Field`, `Method`, etc.) is not present on the image class- or module-path, it still needs to be registered to ensure the correct exception (`ClassNotFoundException` or similar) is thrown. + If an element is queried at run-time without having been registered, regardless of whether it is present on the class- or module-path, this query will throw a `MissingReflectionRegistrationError`. + This change ensures that the error is not ambiguous between a non-existent element and one that was not registered for reflection in the image; +* This rationale also requires that any query that returns a collection of class members (`Class.getMethods()` or similar) has to be registered in full (with `"queryAllPublicMethods"` in this case) to succeed at run-time. + This additionally ensures that any of the registered elements can be queried individually, and non-existent elements of that type will throw the correct exception without having to be registered. + However, this means that `Class.getMethods()` does not return the subset of methods that were registered, but throws a `MissingReflectionRegistrationError` if `"queryAllPublicMethods"` is missing. + +### Resources + +* If a resource or resource bundle is not present on the image class- or module-path, it still needs to be registered to ensure the correct return value (`null`). + If a resource is queried at run-time without having been registered, regardless of whether it is present on the class- or module-path, this query will throw a `MissingResourceRegistrationError`. + This change ensures that the program behavior is not ambiguous between a non-existent resource and one that was not registered for run-time access; + +The Native Image agent does not support custom implementations of `ResourceBundle$Control` or `Bundles$Strategy` and requires manual registrations for the reflection and resource queries that they will perform. + +### Transition tools + +This mode will be made the default behavior of Native Image in a future release. We encourage you to start transitioning your code as soon as possible. +The [Native Image agent](AutomaticMetadataCollection.md) outputs JSON files that conform to both the default and strict modes of operation. +The following options are useful for debugging issues during the transition to the strict mode: + +* `-H:ThrowMissingRegistrationErrors=`: limits `MissingReflectionRegistrationError` to be thrown from a defined list of packages. + This is useful when using some library code that has not been ported to the new mode yet; +* `-H:MissingRegistrationReportingMode`: sets how `MissingReflectionRegistrationError` is reported: + * `Throw` is the default. The error is simply thrown as a Java exception; + * `Warn` outputs a small stack trace for every error encountered, which results in a report of all the places the tested code is going to throw when the strict mode is enabled; + * `Exit` exits the program when encountering the error. This is useful to detect blanket `catch (Throwable t) {` blocks that would otherwise silence the error. + ### Further Reading * [Metadata Collection with the Tracing Agent](AutomaticMetadataCollection.md) diff --git a/docs/reference-manual/native-image/Reflection.md b/docs/reference-manual/native-image/Reflection.md index 5d88751facd8..ac39321ff0f4 100644 --- a/docs/reference-manual/native-image/Reflection.md +++ b/docs/reference-manual/native-image/Reflection.md @@ -138,6 +138,13 @@ Code may also write non-static final fields like `String.value` in this example, More than one configuration can be used by specifying multiple paths for `ReflectionConfigurationFiles` and separating them with `,`. Also, `-H:ReflectionConfigurationResources` can be specified to load one or several configuration files from the build's class path, such as from a JAR file. +### Elements and queries registered by default + +Querying the methods and constructor of `java.lang.Object` does not require configuration. The Java access rules still apply. +Likewise, when using the [strict metadata mode](#strict-metadata-mode), it is possible to query the public or declared fields, methods and constructors of `java.lang.Object`, primitive classes and array classes without requiring a configuration entry. +These queries return empty arrays in most cases, except for `java.lang.Object` methods and constructors and array public methods (all inherited from `java.lang.Object`). The image size impact of this inclusion is therefore minimal. +On the other hand, it is necessary to register these methods and constructors if they need to be reflectively invoked at run-time, via `Method.invoke()` or `Constructor.newInstance()`. + ## Conditional Configuration With conditional configuration, a class configuration entry is applied only if a provided `condition` is satisfied. diff --git a/substratevm/CHANGELOG.md b/substratevm/CHANGELOG.md index 9c02446034b2..85625f6e6358 100644 --- a/substratevm/CHANGELOG.md +++ b/substratevm/CHANGELOG.md @@ -12,6 +12,8 @@ This changelog summarizes major changes to GraalVM Native Image. * (GR-39407) Add support for the `NATIVE_IMAGE_OPTIONS` environment variable, which allows users and tools to pass additional arguments via the environment. Similar to `JAVA_TOOL_OPTIONS`, the value of the environment variable is prepended to the options supplied to `native-image`. * (GR-20827): Introduce a dedicated caller-saved branch target register for software CFI implementations. * (GR-47937) Make the lambda-class name format in Native-Image consistent with the JDK name format. +* (GR-45651) Methods, fields and constructors of `Object`, primitive classes and array classes are now registered by default for reflection. +* (GR-45651) The Native Image agent now tracks calls to `ClassLoader.findSystemClass`, `ObjectInputStream.resolveClass` and `Bundles.of`, and registers resource bundles as bundle name-locale pairs. ## GraalVM for JDK 21 (Internal Version 23.1.0) * (GR-35746) Lower the default aligned chunk size from 1 MB to 512 KB for the serial and epsilon GCs, reducing memory usage and image size in many cases. diff --git a/substratevm/mx.substratevm/mx_substratevm_benchmark.py b/substratevm/mx.substratevm/mx_substratevm_benchmark.py index 38c5a39c996c..c00124b38943 100644 --- a/substratevm/mx.substratevm/mx_substratevm_benchmark.py +++ b/substratevm/mx.substratevm/mx_substratevm_benchmark.py @@ -27,14 +27,14 @@ import os import re -from glob import glob import tempfile - import zipfile +from glob import glob + import mx import mx_benchmark -import mx_sdk_benchmark import mx_java_benchmarks +import mx_sdk_benchmark import mx_sdk_vm_impl _suite = mx.suite("substratevm") @@ -316,12 +316,12 @@ def _empty_file(): } _DACAPO_EXTRA_IMAGE_BUILD_ARGS = { - 'h2' : ['--allow-incomplete-classpath'], - 'pmd': ['--allow-incomplete-classpath'], + 'h2' : [], + 'pmd': [], # org.apache.crimson.parser.Parser2 is force initialized at build-time due to non-determinism in class initialization # order that can lead to runtime issues. See GR-26324. 'xalan': ['--report-unsupported-elements-at-runtime', - '--initialize-at-build-time=org.apache.crimson.parser.Parser2,org.apache.crimson.parser.Parser2$Catalog,org.apache.crimson.parser.Parser2$NullHandler'], + '--initialize-at-build-time=org.apache.crimson.parser.Parser2,org.apache.crimson.parser.Parser2$Catalog,org.apache.crimson.parser.Parser2$NullHandler,org.apache.xml.utils.res.CharArrayWrapper'], # There are two main issues with fop: # 1. LoggingFeature is enabled by default, causing the LogManager configuration to be parsed at build-time. However # DaCapo Harness sets the `java.util.logging.config.file` property at run-time. Therefore, we set @@ -330,11 +330,10 @@ def _empty_file(): # not exist and would fail the benchmark when assertions are enabled. # 2. Native-image picks a different service provider than the JVM for javax.xml.transform.TransformerFactory. # We can simply remove the jar containing that provider as it is not required for the benchmark to run. - 'fop': ['--allow-incomplete-classpath', - '--report-unsupported-elements-at-runtime', + 'fop': ['--report-unsupported-elements-at-runtime', f"-Djava.util.logging.config.file={_empty_file()}", '--initialize-at-run-time=org.apache.fop.render.rtf.rtflib.rtfdoc.RtfList'], - 'batik': ['--allow-incomplete-classpath'] + 'batik': [] } ''' diff --git a/substratevm/src/com.oracle.svm.agent/src/com/oracle/svm/agent/BreakpointInterceptor.java b/substratevm/src/com.oracle.svm.agent/src/com/oracle/svm/agent/BreakpointInterceptor.java index c875efe773a2..3e8b5be55a58 100644 --- a/substratevm/src/com.oracle.svm.agent/src/com/oracle/svm/agent/BreakpointInterceptor.java +++ b/substratevm/src/com.oracle.svm.agent/src/com/oracle/svm/agent/BreakpointInterceptor.java @@ -61,7 +61,6 @@ import java.util.concurrent.locks.ReentrantLock; import java.util.function.Supplier; -import jdk.graal.compiler.core.common.NumUtil; import org.graalvm.nativeimage.StackValue; import org.graalvm.nativeimage.UnmanagedMemory; import org.graalvm.nativeimage.c.function.CEntryPoint; @@ -103,6 +102,8 @@ import com.oracle.svm.jvmtiagentbase.jvmti.JvmtiInterface; import com.oracle.svm.jvmtiagentbase.jvmti.JvmtiLocationFormat; +import jdk.graal.compiler.core.common.NumUtil; + /** * Intercepts events of interest via breakpoints in Java code. *

@@ -679,18 +680,30 @@ private static boolean getBundleImpl(JNIEnvironment jni, JNIObjectHandle thread, callerMethod = state.getCallerMethod(3); } JNIObjectHandle callerClass = getMethodDeclaringClass(callerMethod); - JNIObjectHandle callerModule = getObjectArgument(thread, 0); - JNIObjectHandle module = getObjectArgument(thread, 1); JNIObjectHandle baseName = getObjectArgument(thread, 2); JNIObjectHandle locale = getObjectArgument(thread, 3); - JNIObjectHandle control = getObjectArgument(thread, 4); - JNIObjectHandle result = Support.callStaticObjectMethodLLLLL(jni, bp.clazz, bp.method, callerModule, module, baseName, locale, control); - BundleInfo bundleInfo = BundleInfo.NONE; - if (!clearException(jni)) { - bundleInfo = extractBundleInfo(jni, result); - } - traceReflectBreakpoint(jni, nullHandle(), nullHandle(), callerClass, "getBundleImpl", true, state.getFullStackTraceOrNull(), - Tracer.UNKNOWN_VALUE, Tracer.UNKNOWN_VALUE, fromJniString(jni, baseName), Tracer.UNKNOWN_VALUE, Tracer.UNKNOWN_VALUE, bundleInfo.classNames, bundleInfo.locales); + traceReflectBreakpoint(jni, nullHandle(), nullHandle(), callerClass, bp.specification.methodName, true, state.getFullStackTraceOrNull(), + Tracer.UNKNOWN_VALUE, Tracer.UNKNOWN_VALUE, fromJniString(jni, baseName), readLocaleTag(jni, locale), Tracer.UNKNOWN_VALUE); + return true; + } + + /* + * Bundles.putBundleInCache is the single point through which all bundles queried through + * sun.util.resources.Bundles go + */ + private static boolean putBundleInCache(JNIEnvironment jni, JNIObjectHandle thread, Breakpoint bp, InterceptedState state) { + JNIObjectHandle callerClass = state.getDirectCallerClass(); + JNIObjectHandle cacheKey = getObjectArgument(thread, 0); + JNIObjectHandle baseName = Support.callObjectMethod(jni, cacheKey, agent.handles().sunUtilResourcesBundlesCacheKeyGetName); + if (clearException(jni)) { + baseName = nullHandle(); + } + JNIObjectHandle locale = Support.callObjectMethod(jni, cacheKey, agent.handles().sunUtilResourcesBundlesCacheKeyGetLocale); + if (clearException(jni)) { + locale = nullHandle(); + } + traceReflectBreakpoint(jni, nullHandle(), nullHandle(), callerClass, bp.specification.methodName, true, state.getFullStackTraceOrNull(), + Tracer.UNKNOWN_VALUE, Tracer.UNKNOWN_VALUE, fromJniString(jni, baseName), readLocaleTag(jni, locale), Tracer.UNKNOWN_VALUE); return true; } @@ -703,59 +716,6 @@ private static String readLocaleTag(JNIEnvironment jni, JNIObjectHandle locale) return fromJniString(jni, languageTag); } - private static final class BundleInfo { - - static final BundleInfo NONE = new BundleInfo(new String[0], new String[0]); - - final String[] classNames; - final String[] locales; - - BundleInfo(String[] classNames, String[] locales) { - this.classNames = classNames; - this.locales = locales; - } - } - - /** - * Traverses the bundle parent chain and collects classnames and locales of all encountered - * bundles. - * - */ - private static BundleInfo extractBundleInfo(JNIEnvironment jni, JNIObjectHandle bundle) { - List locales = new ArrayList<>(); - List classNames = new ArrayList<>(); - JNIObjectHandle curr = bundle; - while (curr.notEqual(nullHandle())) { - JNIObjectHandle locale = Support.callObjectMethod(jni, curr, agent.handles().getJavaUtilResourceBundleGetLocale(jni)); - if (clearException(jni)) { - return BundleInfo.NONE; - } - String localeTag = readLocaleTag(jni, locale); - if (localeTag.equals("und")) { - /*- Root locale is serialized into "und" */ - localeTag = ""; - } - JNIObjectHandle clazz = Support.callObjectMethod(jni, curr, agent.handles().javaLangObjectGetClass); - if (!clearException(jni)) { - JNIObjectHandle classNameHandle = Support.callObjectMethod(jni, clazz, agent.handles().javaLangClassGetName); - if (!clearException(jni)) { - classNames.add(fromJniString(jni, classNameHandle)); - locales.add(localeTag); - } - } - curr = getResourceBundleParent(jni, curr); - } - return new BundleInfo(classNames.toArray(new String[0]), locales.toArray(new String[0])); - } - - private static JNIObjectHandle getResourceBundleParent(JNIEnvironment jni, JNIObjectHandle bundle) { - JNIObjectHandle parent = Support.readObjectField(jni, bundle, agent.handles().getJavaUtilResourceBundleParentField(jni)); - if (!clearException(jni)) { - return parent; - } - return nullHandle(); - } - private static boolean loadClass(JNIEnvironment jni, JNIObjectHandle thread, Breakpoint bp, InterceptedState state) { assert experimentalClassLoaderSupport; /* @@ -787,6 +747,13 @@ private static boolean loadClass(JNIEnvironment jni, JNIObjectHandle thread, Bre return true; } + private static boolean findSystemClass(JNIEnvironment jni, JNIObjectHandle thread, Breakpoint bp, InterceptedState state) { + JNIObjectHandle callerClass = state.getDirectCallerClass(); + JNIObjectHandle className = getObjectArgument(thread, 1); + traceReflectBreakpoint(jni, bp.clazz, nullHandle(), callerClass, bp.specification.methodName, true, state.getFullStackTraceOrNull(), fromJniString(jni, className)); + return true; + } + private static boolean isLoadClassInvocation(JNIObjectHandle clazz, JNIMethodId method, int bci, String methodName, String signature) { CIntPointer lengthPtr = StackValue.get(CIntPointer.class); CCharPointerPointer bytecodesPtr = StackValue.get(CCharPointerPointer.class); @@ -1012,6 +979,18 @@ private static boolean serializedLambdaReadResolve(JNIEnvironment jni, JNIObject return true; } + private static boolean readClassDescriptor(JNIEnvironment jni, JNIObjectHandle thread, @SuppressWarnings("unused") Breakpoint bp, InterceptedState state) { + JNIObjectHandle desc = getObjectArgument(thread, 1); + JNIMethodId descriptor = agent.handles().getJavaIoObjectStreamClassGetName(jni); + var name = Support.callObjectMethod(jni, desc, descriptor); + if (clearException(jni)) { + name = nullHandle(); + } + var className = fromJniString(jni, name); + traceSerializeBreakpoint(jni, "ObjectInputStream.readClassDescriptor", true, state.getFullStackTraceOrNull(), className, null); + return true; + } + private static boolean objectStreamClassConstructor(JNIEnvironment jni, JNIObjectHandle thread, Breakpoint bp, InterceptedState state) { JNIObjectHandle serializeTargetClass = getObjectArgument(thread, 1); @@ -1544,6 +1523,9 @@ private interface BreakpointHandler { brk("java/lang/reflect/Array", "newInstance", "(Ljava/lang/Class;I)Ljava/lang/Object;", BreakpointInterceptor::newArrayInstance), brk("java/lang/reflect/Array", "newInstance", "(Ljava/lang/Class;[I)Ljava/lang/Object;", BreakpointInterceptor::newArrayInstanceMulti), + brk("java/lang/ClassLoader", "findSystemClass", "(Ljava/lang/String;)Ljava/lang/Class;", + BreakpointInterceptor::findSystemClass), + brk("jdk/internal/loader/BuiltinClassLoader", "findResource", "(Ljava/lang/String;Ljava/lang/String;)Ljava/net/URL;", BreakpointInterceptor::findResource), brk("jdk/internal/loader/BuiltinClassLoader", "findResourceAsStream", "(Ljava/lang/String;Ljava/lang/String;)Ljava/io/InputStream;", BreakpointInterceptor::findResource), brk("jdk/internal/loader/Loader", "findResource", "(Ljava/lang/String;Ljava/lang/String;)Ljava/net/URL;", BreakpointInterceptor::findResource), @@ -1562,6 +1544,7 @@ private interface BreakpointHandler { "(Ljava/lang/ClassLoader;[Ljava/lang/Class;Ljava/lang/reflect/InvocationHandler;)Ljava/lang/Object;", BreakpointInterceptor::newProxyInstance), brk("java/lang/invoke/SerializedLambda", "readResolve", "()Ljava/lang/Object;", BreakpointInterceptor::serializedLambdaReadResolve), + brk("java/io/ObjectInputStream", "resolveClass", "(Ljava/io/ObjectStreamClass;)Ljava/lang/Class;", BreakpointInterceptor::readClassDescriptor), brk("java/io/ObjectStreamClass", "", "(Ljava/lang/Class;)V", BreakpointInterceptor::objectStreamClassConstructor), brk("jdk/internal/reflect/ReflectionFactory", "newConstructorForSerialization", @@ -1570,6 +1553,8 @@ private interface BreakpointHandler { "getBundleImpl", "(Ljava/lang/Module;Ljava/lang/Module;Ljava/lang/String;Ljava/util/Locale;Ljava/util/ResourceBundle$Control;)Ljava/util/ResourceBundle;", BreakpointInterceptor::getBundleImpl), + brk("sun/util/resources/Bundles", "putBundleInCache", "(Lsun/util/resources/Bundles$CacheKey;Ljava/util/ResourceBundle;)Ljava/util/ResourceBundle;", + BreakpointInterceptor::putBundleInCache), // In Java 9+, these are Java methods that call private methods optionalBrk("jdk/internal/misc/Unsafe", "objectFieldOffset", "(Ljava/lang/Class;Ljava/lang/String;)J", BreakpointInterceptor::objectFieldOffsetByName), diff --git a/substratevm/src/com.oracle.svm.agent/src/com/oracle/svm/agent/NativeImageAgentJNIHandleSet.java b/substratevm/src/com.oracle.svm.agent/src/com/oracle/svm/agent/NativeImageAgentJNIHandleSet.java index bf59f3a3f239..defd3436ffee 100644 --- a/substratevm/src/com.oracle.svm.agent/src/com/oracle/svm/agent/NativeImageAgentJNIHandleSet.java +++ b/substratevm/src/com.oracle.svm.agent/src/com/oracle/svm/agent/NativeImageAgentJNIHandleSet.java @@ -67,7 +67,9 @@ public class NativeImageAgentJNIHandleSet extends JNIHandleSet { private JNIMethodId javaUtilResourceBundleGetBundleImplSLCC; private boolean queriedJavaUtilResourceBundleGetBundleImplSLCC; + private JNIObjectHandle javaIoObjectStreamClass; private JNIMethodId javaIoObjectStreamClassForClass; + private JNIMethodId javaIoObjectStreamClassGetName; private JNIMethodId javaIoObjectStreamClassGetClassDataLayout0; private JNIObjectHandle javaIOObjectStreamClassClassDataSlot; private JNIFieldId javaIOObjectStreamClassClassDataSlotDesc; @@ -84,6 +86,11 @@ public class NativeImageAgentJNIHandleSet extends JNIHandleSet { final JNIFieldId javaLangInvokeSerializedLambdaCapturingClass; + final JNIMethodId sunUtilResourcesBundlesCacheKeyGetName; + final JNIMethodId sunUtilResourcesBundlesCacheKeyGetLocale; + + final JNIMethodId javaLangModuleGetName; + NativeImageAgentJNIHandleSet(JNIEnvironment env) { super(env); javaLangClass = newClassGlobalRef(env, "java/lang/Class"); @@ -120,6 +127,13 @@ public class NativeImageAgentJNIHandleSet extends JNIHandleSet { JNIObjectHandle serializedLambda = findClass(env, "java/lang/invoke/SerializedLambda"); javaLangInvokeSerializedLambdaCapturingClass = getFieldId(env, serializedLambda, "capturingClass", "Ljava/lang/Class;", false); + + JNIObjectHandle sunUtilResourcesBundlesCacheKey = findClass(env, "sun/util/resources/Bundles$CacheKey"); + sunUtilResourcesBundlesCacheKeyGetName = getMethodId(env, sunUtilResourcesBundlesCacheKey, "getName", "()Ljava/lang/String;", false); + sunUtilResourcesBundlesCacheKeyGetLocale = getMethodId(env, sunUtilResourcesBundlesCacheKey, "getLocale", "()Ljava/util/Locale;", false); + + JNIObjectHandle javaLangModule = findClass(env, "java/lang/Module"); + javaLangModuleGetName = getMethodId(env, javaLangModule, "getName", "()Ljava/lang/String;", false); } JNIMethodId getJavaLangReflectExecutableGetParameterTypes(JNIEnvironment env) { @@ -156,6 +170,13 @@ JNIMethodId tryGetJavaUtilResourceBundleGetBundleImplSLCC(JNIEnvironment env) { return javaUtilResourceBundleGetBundleImplSLCC; } + JNIObjectHandle getJavaIOObjectStreamClass(JNIEnvironment env) { + if (javaIoObjectStreamClass.equal(nullHandle())) { + javaIoObjectStreamClass = findClass(env, "java/io/ObjectStreamClass"); + } + return javaIoObjectStreamClass; + } + JNIMethodId getJavaIoObjectStreamClassForClass(JNIEnvironment env, JNIObjectHandle javaIoObjectStreamClass) { if (javaIoObjectStreamClassForClass.equal(nullHandle())) { javaIoObjectStreamClassForClass = getMethodId(env, javaIoObjectStreamClass, "forClass", "()Ljava/lang/Class;", false); @@ -163,6 +184,13 @@ JNIMethodId getJavaIoObjectStreamClassForClass(JNIEnvironment env, JNIObjectHand return javaIoObjectStreamClassForClass; } + JNIMethodId getJavaIoObjectStreamClassGetName(JNIEnvironment env) { + if (javaIoObjectStreamClassGetName.equal(nullHandle())) { + javaIoObjectStreamClassGetName = getMethodId(env, getJavaIOObjectStreamClass(env), "getName", "()Ljava/lang/String;", false); + } + return javaIoObjectStreamClassGetName; + } + JNIMethodId getJavaIoObjectStreamClassGetClassDataLayout0(JNIEnvironment env, JNIObjectHandle javaIoObjectStreamClass) { if (javaIoObjectStreamClassGetClassDataLayout0.equal(nullHandle())) { javaIoObjectStreamClassGetClassDataLayout0 = getMethodId(env, javaIoObjectStreamClass, "getClassDataLayout0", "()[Ljava/io/ObjectStreamClass$ClassDataSlot;", false); diff --git a/substratevm/src/com.oracle.svm.configure/src/com/oracle/svm/configure/config/ResourceConfiguration.java b/substratevm/src/com.oracle.svm.configure/src/com/oracle/svm/configure/config/ResourceConfiguration.java index 6a561ee133e7..c3aae5d266a8 100644 --- a/substratevm/src/com.oracle.svm.configure/src/com/oracle/svm/configure/config/ResourceConfiguration.java +++ b/substratevm/src/com.oracle.svm.configure/src/com/oracle/svm/configure/config/ResourceConfiguration.java @@ -27,7 +27,6 @@ import java.io.IOException; import java.util.Collection; import java.util.Comparator; -import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; @@ -48,8 +47,6 @@ public final class ResourceConfiguration extends ConfigurationBase { - private static final String PROPERTY_BUNDLE = "java.util.PropertyResourceBundle"; - public static class ParserAdapter implements ResourcesRegistry { private final ResourceConfiguration configuration; @@ -195,18 +192,9 @@ private void addClassResourceBundle(ConfigurationCondition condition, String bas getOrCreateBundleConfig(condition, basename).classNames.add(className); } - public void addBundle(ConfigurationCondition condition, List classNames, List locales, String baseName) { - assert classNames.size() == locales.size() : "Each bundle should be represented by both classname and locale"; + public void addBundle(ConfigurationCondition condition, String baseName, String queriedLocale) { BundleConfiguration config = getOrCreateBundleConfig(condition, baseName); - for (int i = 0; i < classNames.size(); i++) { - String className = classNames.get(i); - String localeTag = locales.get(i); - if (!className.equals(PROPERTY_BUNDLE)) { - config.classNames.add(className); - } else { - config.locales.add(localeTag); - } - } + config.locales.add(queriedLocale); } private BundleConfiguration getOrCreateBundleConfig(ConfigurationCondition condition, String baseName) { diff --git a/substratevm/src/com.oracle.svm.configure/src/com/oracle/svm/configure/filters/HierarchyFilterNode.java b/substratevm/src/com.oracle.svm.configure/src/com/oracle/svm/configure/filters/HierarchyFilterNode.java index f9f447f3ef2d..7e94fcde26f0 100644 --- a/substratevm/src/com.oracle.svm.configure/src/com/oracle/svm/configure/filters/HierarchyFilterNode.java +++ b/substratevm/src/com.oracle.svm.configure/src/com/oracle/svm/configure/filters/HierarchyFilterNode.java @@ -68,6 +68,8 @@ public static HierarchyFilterNode createRoot() { public static HierarchyFilterNode createInclusiveRoot() { HierarchyFilterNode root = new HierarchyFilterNode(""); + /* Needed to ensure that the empty string is matched by the filter as well. */ + root.inclusion = Inclusion.Include; root.addOrGetChildren("**", ConfigurationFilter.Inclusion.Include); return root; } diff --git a/substratevm/src/com.oracle.svm.configure/src/com/oracle/svm/configure/trace/AccessAdvisor.java b/substratevm/src/com.oracle.svm.configure/src/com/oracle/svm/configure/trace/AccessAdvisor.java index 58603c34e2b8..5689984816fe 100644 --- a/substratevm/src/com.oracle.svm.configure/src/com/oracle/svm/configure/trace/AccessAdvisor.java +++ b/substratevm/src/com.oracle.svm.configure/src/com/oracle/svm/configure/trace/AccessAdvisor.java @@ -26,12 +26,12 @@ import java.util.regex.Pattern; -import jdk.graal.compiler.java.LambdaUtils; -import jdk.graal.compiler.phases.common.LazyValue; - import com.oracle.svm.configure.filters.ConfigurationFilter; import com.oracle.svm.configure.filters.HierarchyFilterNode; +import jdk.graal.compiler.java.LambdaUtils; +import jdk.graal.compiler.phases.common.LazyValue; + /** * Decides if a recorded access should be included in a configuration. Also advises the agent's * {@code AccessVerifier} classes which accesses to ignore when the agent is in restriction mode. @@ -66,11 +66,19 @@ public final class AccessAdvisor { internalCallerFilter.addOrGetChildren("com.sun.nio.sctp.**", ConfigurationFilter.Inclusion.Exclude); internalCallerFilter.addOrGetChildren("com.sun.nio.zipfs.**", ConfigurationFilter.Inclusion.Exclude); internalCallerFilter.addOrGetChildren("java.io.**", ConfigurationFilter.Inclusion.Exclude); + internalCallerFilter.addOrGetChildren("java.io.ObjectInputFilter$Config", ConfigurationFilter.Inclusion.Include); + internalCallerFilter.addOrGetChildren("java.lang.**", ConfigurationFilter.Inclusion.Exclude); + // ClassLoader.findSystemClass calls ClassLoader.loadClass + internalCallerFilter.addOrGetChildren("java.lang.ClassLoader", ConfigurationFilter.Inclusion.Include); // The agent should not filter calls from native libraries (JDK11). internalCallerFilter.addOrGetChildren("java.lang.ClassLoader$NativeLibrary", ConfigurationFilter.Inclusion.Include); + // Module has resource query wrappers + internalCallerFilter.addOrGetChildren("java.lang.Module", ConfigurationFilter.Inclusion.Include); internalCallerFilter.addOrGetChildren("java.math.**", ConfigurationFilter.Inclusion.Exclude); internalCallerFilter.addOrGetChildren("java.net.**", ConfigurationFilter.Inclusion.Exclude); + // URLConnection.lookupContentHandlerClassFor calls Class.forName + internalCallerFilter.addOrGetChildren("java.net.URLConnection", ConfigurationFilter.Inclusion.Include); internalCallerFilter.addOrGetChildren("java.nio.**", ConfigurationFilter.Inclusion.Exclude); internalCallerFilter.addOrGetChildren("java.text.**", ConfigurationFilter.Inclusion.Exclude); internalCallerFilter.addOrGetChildren("java.time.**", ConfigurationFilter.Inclusion.Exclude); @@ -78,13 +86,20 @@ public final class AccessAdvisor { internalCallerFilter.addOrGetChildren("java.util.concurrent.atomic.*", ConfigurationFilter.Inclusion.Include); // Atomic*FieldUpdater internalCallerFilter.addOrGetChildren("java.util.Collections", ConfigurationFilter.Inclusion.Include); // java.util.Collections.zeroLengthArray internalCallerFilter.addOrGetChildren("java.util.random.*", ConfigurationFilter.Inclusion.Include); // RandomGeneratorFactory$$Lambda - // Exception constructors + /* + * ForkJoinTask.getThrowableException calls Class.getConstructors and + * Constructor.newInstance + */ internalCallerFilter.addOrGetChildren("java.util.concurrent.ForkJoinTask", ConfigurationFilter.Inclusion.Include); + // LazyClassPathLookupIterator calls Class.forName + internalCallerFilter.addOrGetChildren("java.util.ServiceLoader$LazyClassPathLookupIterator", ConfigurationFilter.Inclusion.Include); internalCallerFilter.addOrGetChildren("javax.crypto.**", ConfigurationFilter.Inclusion.Exclude); internalCallerFilter.addOrGetChildren("javax.lang.model.**", ConfigurationFilter.Inclusion.Exclude); internalCallerFilter.addOrGetChildren("javax.net.**", ConfigurationFilter.Inclusion.Exclude); internalCallerFilter.addOrGetChildren("javax.tools.**", ConfigurationFilter.Inclusion.Exclude); internalCallerFilter.addOrGetChildren("jdk.internal.**", ConfigurationFilter.Inclusion.Exclude); + // BootLoader calls BuiltinClassLoader.getResource + internalCallerFilter.addOrGetChildren("jdk.internal.loader.BootLoader", ConfigurationFilter.Inclusion.Include); // The agent should not filter calls from native libraries (JDK17). internalCallerFilter.addOrGetChildren("jdk.internal.loader.NativeLibraries$NativeLibraryImpl", ConfigurationFilter.Inclusion.Include); internalCallerFilter.addOrGetChildren("jdk.jfr.**", ConfigurationFilter.Inclusion.Exclude); @@ -92,6 +107,8 @@ public final class AccessAdvisor { internalCallerFilter.addOrGetChildren("jdk.nio.**", ConfigurationFilter.Inclusion.Exclude); internalCallerFilter.addOrGetChildren("jdk.vm.**", ConfigurationFilter.Inclusion.Exclude); internalCallerFilter.addOrGetChildren("sun.invoke.**", ConfigurationFilter.Inclusion.Exclude); + // BytecodeDescriptor calls Class.forName + internalCallerFilter.addOrGetChildren("sun.invoke.util.BytecodeDescriptor", ConfigurationFilter.Inclusion.Include); internalCallerFilter.addOrGetChildren("sun.launcher.**", ConfigurationFilter.Inclusion.Exclude); internalCallerFilter.addOrGetChildren("sun.misc.**", ConfigurationFilter.Inclusion.Exclude); internalCallerFilter.addOrGetChildren("sun.net.**", ConfigurationFilter.Inclusion.Exclude); @@ -99,8 +116,12 @@ public final class AccessAdvisor { internalCallerFilter.addOrGetChildren("sun.net.www.protocol.http.*", ConfigurationFilter.Inclusion.Include); internalCallerFilter.addOrGetChildren("sun.nio.**", ConfigurationFilter.Inclusion.Exclude); internalCallerFilter.addOrGetChildren("sun.reflect.**", ConfigurationFilter.Inclusion.Exclude); + // ConstructorUtil has wrappers around reflection methods + internalCallerFilter.addOrGetChildren("sun.reflect.misc.ConstructorUtil", ConfigurationFilter.Inclusion.Include); internalCallerFilter.addOrGetChildren("sun.text.**", ConfigurationFilter.Inclusion.Exclude); internalCallerFilter.addOrGetChildren("sun.util.**", ConfigurationFilter.Inclusion.Exclude); + // Bundles calls Bundles.of + internalCallerFilter.addOrGetChildren("sun.util.resources.Bundles", ConfigurationFilter.Inclusion.Include); excludeInaccessiblePackages(internalCallerFilter); diff --git a/substratevm/src/com.oracle.svm.configure/src/com/oracle/svm/configure/trace/ReflectionProcessor.java b/substratevm/src/com.oracle.svm.configure/src/com/oracle/svm/configure/trace/ReflectionProcessor.java index 2790c4846396..9d8f66401581 100644 --- a/substratevm/src/com.oracle.svm.configure/src/com/oracle/svm/configure/trace/ReflectionProcessor.java +++ b/substratevm/src/com.oracle.svm.configure/src/com/oracle/svm/configure/trace/ReflectionProcessor.java @@ -76,7 +76,6 @@ public void processEntry(EconomicMap entry, ConfigurationSet configur resourceConfiguration.addResourcePattern(condition, (module == null ? "" : module + ":") + Pattern.quote(resource)); return; case "getResource": - case "getResourceAsStream": case "getSystemResource": case "getSystemResourceAsStream": case "getResources": @@ -88,7 +87,7 @@ public void processEntry(EconomicMap entry, ConfigurationSet configur } TypeConfiguration configuration = configurationSet.getReflectionConfiguration(); String callerClass = (String) entry.get("caller_class"); - boolean isLoadClass = function.equals("loadClass"); + boolean isLoadClass = function.equals("loadClass") || function.equals("findSystemClass"); if (isLoadClass || function.equals("forName") || function.equals("findClass")) { String name = singleElement(args); if (isLoadClass) { // different array syntax @@ -258,15 +257,13 @@ public void processEntry(EconomicMap entry, ConfigurationSet configur break; } + case "putBundleInCache": case "getBundleImpl": { - expectSize(args, 7); + expectSize(args, 5); String baseName = (String) args.get(2); - @SuppressWarnings("unchecked") - List classNames = (List) args.get(5); - @SuppressWarnings("unchecked") - List locales = (List) args.get(6); + String queriedLocale = (String) args.get(3); if (baseName != null) { - resourceConfiguration.addBundle(condition, classNames, locales, baseName); + resourceConfiguration.addBundle(condition, baseName, queriedLocale); } break; } diff --git a/substratevm/src/com.oracle.svm.configure/src/com/oracle/svm/configure/trace/SerializationProcessor.java b/substratevm/src/com.oracle.svm.configure/src/com/oracle/svm/configure/trace/SerializationProcessor.java index 98a5710775e9..78278bc025f5 100644 --- a/substratevm/src/com.oracle.svm.configure/src/com/oracle/svm/configure/trace/SerializationProcessor.java +++ b/substratevm/src/com.oracle.svm.configure/src/com/oracle/svm/configure/trace/SerializationProcessor.java @@ -55,7 +55,7 @@ void processEntry(EconomicMap entry, ConfigurationSet configurationSe List args = (List) entry.get("args"); SerializationConfiguration serializationConfiguration = configurationSet.getSerializationConfiguration(); - if ("ObjectStreamClass.".equals(function)) { + if ("ObjectStreamClass.".equals(function) || "ObjectInputStream.readClassDescriptor".equals(function)) { expectSize(args, 2); if (advisor.shouldIgnore(LazyValueUtils.lazyValue((String) args.get(0)), LazyValueUtils.lazyValue(null), false)) { diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/hub/ClassForNameSupport.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/hub/ClassForNameSupport.java index b87fdc9959c4..56fc69ef51e3 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/hub/ClassForNameSupport.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/hub/ClassForNameSupport.java @@ -35,6 +35,7 @@ import com.oracle.svm.core.reflect.MissingReflectionRegistrationUtils; import com.oracle.svm.core.util.ImageHeapMap; import com.oracle.svm.core.util.VMError; +import com.oracle.svm.util.ReflectionUtil; @AutomaticallyRegisteredImageSingleton public final class ClassForNameSupport { @@ -56,22 +57,41 @@ public static void registerClass(Class clazz) { } String name = clazz.getName(); Object currentValue = singleton().knownClasses.get(name); - VMError.guarantee(currentValue == null || currentValue == clazz || currentValue instanceof Throwable, - "Invalid Class.forName value for %s: %s", name, currentValue); /* - * If the class has already been seen as throwing an error, we don't overwrite this error + * If the class is already registered as negative, it means that it exists but is not + * accessible through the builder class loader, and it was already registered by name (as + * negative query) before this point. In that case, we update the map to contain the actual + * class. */ - singleton().knownClasses.putIfAbsent(name, clazz); + VMError.guarantee(currentValue == null || currentValue == clazz || currentValue instanceof Throwable || + (currentValue == NEGATIVE_QUERY && ReflectionUtil.lookupClass(true, name) == null), + "Invalid Class.forName value for %s: %s", name, currentValue); + + if (currentValue == NEGATIVE_QUERY) { + singleton().knownClasses.put(name, clazz); + } else { + /* + * If the class has already been seen as throwing an error, we don't overwrite this + * error + */ + singleton().knownClasses.putIfAbsent(name, clazz); + } } @Platforms(Platform.HOSTED_ONLY.class) public static void registerExceptionForClass(String className, Throwable t) { + Object currentValue = singleton().knownClasses.get(className); + VMError.guarantee(currentValue == null || currentValue.getClass() == t.getClass()); singleton().knownClasses.put(className, t); } @Platforms(Platform.HOSTED_ONLY.class) public static void registerNegativeQuery(String className) { - singleton().knownClasses.put(className, NEGATIVE_QUERY); + /* + * If the class is not accessible by the builder class loader, but was already registered + * through registerClass(Class), we don't overwrite the actual class or exception. + */ + singleton().knownClasses.putIfAbsent(className, NEGATIVE_QUERY); } public static Class forNameOrNull(String className, ClassLoader classLoader) { @@ -91,7 +111,8 @@ private static Class forName(String className, ClassLoader classLoader, boole return null; } Object result = singleton().knownClasses.get(className); - if (result == NEGATIVE_QUERY) { + if (result == NEGATIVE_QUERY || className.endsWith("[]")) { + /* Querying array classes with their "TypeName[]" name always throws */ result = new ClassNotFoundException(className); } if (result == null) { 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 d1651798ff2d..a46f1edc1e6b 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 @@ -25,6 +25,7 @@ package com.oracle.svm.core.hub; import static com.oracle.svm.core.MissingRegistrationUtils.throwMissingRegistrationErrors; +import static com.oracle.svm.core.annotate.TargetElement.CONSTRUCTOR_NAME; import static com.oracle.svm.core.reflect.ReflectionMetadataDecoder.NO_DATA; import static com.oracle.svm.core.reflect.target.ReflectionMetadataDecoderImpl.ALL_CLASSES_FLAG; import static com.oracle.svm.core.reflect.target.ReflectionMetadataDecoderImpl.ALL_CONSTRUCTORS_FLAG; @@ -1058,16 +1059,32 @@ private Method getMethod(String methodName, Class... parameterTypes) throws N return getReflectionFactory().copyMethod(method); } - private void checkMethod(String methodName, Class[] parameterTypes, Executable method, boolean publicOnly) throws NoSuchMethodException { + private void checkMethod(String methodName, Class[] parameterTypes, Method method, boolean publicOnly) throws NoSuchMethodException { + if (CONSTRUCTOR_NAME.equals(methodName)) { + throw new NoSuchMethodException(methodToString(methodName, parameterTypes)); + } + checkExecutable(methodName, parameterTypes, method, publicOnly); + } + + private void checkConstructor(Class[] parameterTypes, Constructor constructor, boolean publicOnly) throws NoSuchMethodException { + checkExecutable(CONSTRUCTOR_NAME, parameterTypes, constructor, publicOnly); + } + + private void checkExecutable(String methodName, Class[] parameterTypes, Executable method, boolean publicOnly) throws NoSuchMethodException { boolean throwMissingErrors = throwMissingRegistrationErrors(); Class clazz = DynamicHub.toClass(this); if (method == null) { - if (throwMissingErrors && !allElementsRegistered(publicOnly, ALL_DECLARED_METHODS_FLAG, ALL_METHODS_FLAG)) { + boolean isConstructor = methodName.equals(CONSTRUCTOR_NAME); + int allDeclaredFlag = isConstructor ? ALL_DECLARED_CONSTRUCTORS_FLAG : ALL_DECLARED_METHODS_FLAG; + int allPublicFlag = isConstructor ? ALL_CONSTRUCTORS_FLAG : ALL_METHODS_FLAG; + if (throwMissingErrors && !allElementsRegistered(publicOnly, allDeclaredFlag, allPublicFlag) && + !(isConstructor && isInterface())) { MissingReflectionRegistrationUtils.forMethod(clazz, methodName, parameterTypes); } /* * If getDeclaredMethods (or getMethods for a public method) is registered, we know for - * sure that the method does indeed not exist if we don't find it. + * sure that the method does indeed not exist if we don't find it. This is also the case + * when querying an interface constructor. */ throw new NoSuchMethodException(methodToString(methodName, parameterTypes)); } else { @@ -1232,7 +1249,7 @@ private Constructor getConstructor0(Class[] parameterTypes, int which) thr candidate = constructor; } } - checkMethod("", parameterTypes, candidate, which == Member.PUBLIC); + checkConstructor(parameterTypes, candidate, which == Member.PUBLIC); return candidate; } 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 6d22b6418f11..71a52c7ccff0 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 @@ -69,7 +69,7 @@ static ObjectStreamClass lookup(Class cl, boolean all) { } if (Serializable.class.isAssignableFrom(cl)) { - if (!DynamicHub.fromClass(cl).isRegisteredForSerialization()) { + if (!cl.isArray() && !DynamicHub.fromClass(cl).isRegisteredForSerialization()) { boolean isLambda = cl.getTypeName().contains(LambdaUtils.LAMBDA_CLASS_NAME_SUBSTRING); boolean isProxy = Proxy.isProxyClass(cl); if (isProxy || isLambda) { diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/Resources.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/Resources.java index 069e34e89dfe..60ac9ec96d0a 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/Resources.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/Resources.java @@ -259,10 +259,28 @@ public void registerIncludePattern(String module, String pattern) { assert MissingRegistrationUtils.throwMissingRegistrationErrors(); synchronized (includePatterns) { updateTimeStamp(); - includePatterns.put(new ModuleResourcePair(module, pattern), Boolean.TRUE); + includePatterns.put(new ModuleResourcePair(module, handleEscapedCharacters(pattern)), Boolean.TRUE); } } + @Platforms(Platform.HOSTED_ONLY.class)// + private static final String BEGIN_ESCAPED_SEQUENCE = "\\Q"; + + @Platforms(Platform.HOSTED_ONLY.class)// + private static final String END_ESCAPED_SEQUENCE = "\\E"; + + /* + * This handles generated include patterns which start and end with \Q and \E. The actual + * resource name is located inbetween those tags. + */ + @Platforms(Platform.HOSTED_ONLY.class) + private static String handleEscapedCharacters(String pattern) { + if (pattern.startsWith(BEGIN_ESCAPED_SEQUENCE) && pattern.endsWith(END_ESCAPED_SEQUENCE)) { + return pattern.substring(BEGIN_ESCAPED_SEQUENCE.length(), pattern.length() - END_ESCAPED_SEQUENCE.length()); + } + return pattern; + } + /** * Avoid pulling native file system by using {@link NativeImageResourcePath} implementation to * convert resourceName to canonical variant. diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/ResourcesHelper.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/ResourcesHelper.java index cccf351aad65..a4eb731f97ab 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/ResourcesHelper.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/ResourcesHelper.java @@ -36,6 +36,7 @@ import com.oracle.svm.core.util.VMError; import jdk.internal.loader.Resource; +import org.graalvm.nativeimage.ImageInfo; public class ResourcesHelper { @@ -106,8 +107,10 @@ public static URL nameToResourceURL(Module module, String resourceName) { return Resources.singleton().createURL(module, resourceName); } - public static InputStream nameToResourceInputStream(String resourceName) throws IOException { - URL url = nameToResourceURL(resourceName); + public static InputStream nameToResourceInputStream(String mn, String resourceName) throws IOException { + VMError.guarantee(ImageInfo.inImageRuntimeCode(), "ResourcesHelper code should only be used at runtime"); + Module module = ModuleLayer.boot().findModule(mn).orElse(null); + URL url = nameToResourceURL(module, resourceName); return url != null ? url.openStream() : null; } diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/ServiceCatalogSupport.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/ServiceCatalogSupport.java index 0a94b498a9c1..7bebc076e998 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/ServiceCatalogSupport.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/ServiceCatalogSupport.java @@ -24,6 +24,7 @@ */ package com.oracle.svm.core.jdk; +import java.lang.module.ModuleDescriptor; import java.util.List; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; @@ -79,5 +80,17 @@ public void enableServiceCatalogMapTransformer(Feature.BeforeAnalysisAccess acce }); return res; }); + access.registerFieldValueTransformer(ReflectionUtil.lookupField(ModuleDescriptor.Provides.class, "providers"), (receiver, original) -> { + VMError.guarantee(sealed, "Service provider detector must be registered before the analysis starts"); + List providers = (List) original; + String service = ((ModuleDescriptor.Provides) receiver).service(); + if (omittedServiceProviders.containsKey(service)) { + var omittedProviders = omittedServiceProviders.get(service); + providers = providers.stream() + .filter(p -> !omittedProviders.contains(p)) + .collect(Collectors.toList()); + } + return providers; + }); } } diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/Target_java_lang_Module.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/Target_java_lang_Module.java index 0b0794e28266..10996ed2d0e6 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/Target_java_lang_Module.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/Target_java_lang_Module.java @@ -24,19 +24,15 @@ */ package com.oracle.svm.core.jdk; -import java.io.ByteArrayInputStream; -import java.io.InputStream; import java.util.Arrays; import java.util.List; import java.util.Objects; import java.util.stream.Collectors; -import com.oracle.svm.core.SubstrateUtil; import com.oracle.svm.core.annotate.Alias; import com.oracle.svm.core.annotate.Substitute; import com.oracle.svm.core.annotate.TargetClass; import com.oracle.svm.core.annotate.TargetElement; -import com.oracle.svm.core.jdk.resources.ResourceStorageEntry; @SuppressWarnings("unused") @TargetClass(value = java.lang.Module.class) @@ -49,17 +45,6 @@ public final class Target_java_lang_Module { @TargetElement(onlyWith = JDK22OrLater.class) public native void ensureNativeAccess(Class owner, String methodName, Class currentClass); - @SuppressWarnings("static-method") - @Substitute - private InputStream getResourceAsStream(String resourceName) { - String resName = resourceName; - if (resName.startsWith("/")) { - resName = resName.substring(1); - } - Object res = Resources.singleton().get(SubstrateUtil.cast(this, Module.class), resName, true); - return res == null ? null : new ByteArrayInputStream(((ResourceStorageEntry) res).getData().get(0)); - } - @Substitute private static void defineModule0(Module module, boolean isOpen, String version, String location, Object[] pns) { if (Arrays.stream(pns).anyMatch(Objects::isNull)) { diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/Target_jdk_internal_loader_BuiltinClassLoader.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/Target_jdk_internal_loader_BuiltinClassLoader.java index 6d791b62bf9d..6b7bc68d4350 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/Target_jdk_internal_loader_BuiltinClassLoader.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/Target_jdk_internal_loader_BuiltinClassLoader.java @@ -69,7 +69,7 @@ public URL findResource(String mn, String name) { @Substitute public InputStream findResourceAsStream(String mn, String name) throws IOException { - return ResourcesHelper.nameToResourceInputStream(name); + return ResourcesHelper.nameToResourceInputStream(mn, name); } @Substitute diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/localization/LocalizationSupport.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/localization/LocalizationSupport.java index a036d9c5520a..8fa7d1ad7779 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/localization/LocalizationSupport.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/localization/LocalizationSupport.java @@ -28,6 +28,7 @@ import static com.oracle.svm.util.StringUtil.toDotSeparated; import static com.oracle.svm.util.StringUtil.toSlashSeparated; +import java.lang.reflect.Constructor; import java.nio.charset.Charset; import java.util.Collection; import java.util.Collections; @@ -56,6 +57,7 @@ import com.oracle.svm.core.util.VMError; import jdk.graal.compiler.debug.GraalError; +import sun.util.resources.Bundles; /** * Holder for localization information that is computed during image generation and used at run @@ -107,6 +109,22 @@ public Map getBundleContentOf(Object bundle) { @Platforms(Platform.HOSTED_ONLY.class) public void prepareBundle(String bundleName, ResourceBundle bundle, Function> findModule, Locale locale) { + /* + * Class-based bundle lookup happens on every query, but we don't need to register the + * constructor for a property resource bundle since the class lookup will fail. + */ + registerRequiredReflectionAndResourcesForBundle(bundleName, Set.of(locale)); + if (!(bundle instanceof PropertyResourceBundle)) { + RuntimeReflection.register(bundle.getClass()); + try { + Constructor nullaryConstructor = bundle.getClass().getDeclaredConstructor(); + RuntimeReflection.register(nullaryConstructor); + } catch (NoSuchMethodException e) { + RuntimeReflection.registerConstructorLookup(bundle.getClass()); + } + } + + /* Property-based bundle lookup happens only if class-based lookup fails */ if (bundle instanceof PropertyResourceBundle) { String[] bundleNameWithModule = SubstrateUtil.split(bundleName, ":", 2); String resourceName; @@ -131,12 +149,8 @@ public void prepareBundle(String bundleName, ResourceBundle bundle, Function ImageSingletons.lookup(RuntimeResourceSupport.class).addResource(m, finalResourceName)); } } - } else { - registerRequiredReflectionAndResourcesForBundle(bundleName, Set.of(locale)); - RuntimeReflection.register(bundle.getClass()); - RuntimeReflection.registerForReflectiveInstantiation(bundle.getClass()); - onBundlePrepared(bundle); } + onBundlePrepared(bundle); } private static String packageName(String bundleName) { @@ -183,6 +197,10 @@ private void registerRequiredReflectionAndResourcesForBundleAndLocale(String bas String bundleWithLocale = control.toBundleName(baseName, locale); RuntimeReflection.registerClassLookup(bundleWithLocale); Resources.singleton().registerNegativeQuery(bundleWithLocale.replace('.', '/') + ".properties"); + String otherBundleName = Bundles.toOtherBundleName(baseName, bundleWithLocale, locale); + if (!otherBundleName.equals(bundleWithLocale)) { + RuntimeReflection.registerClassLookup(otherBundleName); + } } } diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/localization/substitutions/Target_java_time_format_DateTimeTextProvider.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/localization/substitutions/Target_java_time_format_DateTimeTextProvider.java new file mode 100644 index 000000000000..66abd1c7c736 --- /dev/null +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/localization/substitutions/Target_java_time_format_DateTimeTextProvider.java @@ -0,0 +1,41 @@ +/* + * 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.jdk.localization.substitutions; + +import java.time.temporal.TemporalField; +import java.util.Locale; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import com.oracle.svm.core.annotate.Alias; +import com.oracle.svm.core.annotate.RecomputeFieldValue; +import com.oracle.svm.core.annotate.TargetClass; + +@TargetClass(className = "java.time.format.DateTimeTextProvider") +final class Target_java_time_format_DateTimeTextProvider { + @Alias @RecomputeFieldValue(kind = RecomputeFieldValue.Kind.FromAlias)// + private static ConcurrentMap, Object> CACHE = new ConcurrentHashMap<>(16, 0.75f, 2); +} diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/MissingResourceRegistrationUtils.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/MissingResourceRegistrationUtils.java index c4705f603a40..2b39b8f59159 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/MissingResourceRegistrationUtils.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/resources/MissingResourceRegistrationUtils.java @@ -24,6 +24,8 @@ */ package com.oracle.svm.core.jdk.resources; +import static com.oracle.svm.core.MissingRegistrationUtils.ERROR_EMPHASIS_INDENT; + import java.nio.file.Files; import java.nio.file.spi.FileSystemProvider; import java.util.Map; @@ -43,7 +45,14 @@ public static void missingResource(String resourcePath) { } private static String errorMessage(String resourcePath) { - return "The program tried to access the resource at path " + resourcePath + " without it being registered as reachable. Add it to the resource metadata to solve this problem. " + + /* Can't use multi-line strings as they pull in format and bloat "Hello, World!" */ + return "The program tried to access the resource at path " + + System.lineSeparator() + + System.lineSeparator() + + ERROR_EMPHASIS_INDENT + resourcePath + + System.lineSeparator() + + System.lineSeparator() + + " without it being registered as reachable. Add it to the resource metadata to solve this problem. " + "See https://www.graalvm.org/latest/reference-manual/native-image/metadata/#resources-and-resource-bundles for help"; } diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jni/functions/JNIFunctions.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jni/functions/JNIFunctions.java index e5e75aa9ae74..e6ef80ee0f37 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jni/functions/JNIFunctions.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jni/functions/JNIFunctions.java @@ -26,18 +26,14 @@ import static com.oracle.svm.core.heap.RestrictHeapAccess.Access.NO_ALLOCATION; +import java.lang.invoke.MethodType; import java.lang.reflect.Array; -import java.lang.reflect.Constructor; import java.lang.reflect.Executable; import java.lang.reflect.Field; -import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.nio.ByteBuffer; import java.util.Arrays; -import jdk.graal.compiler.core.common.SuppressFBWarnings; -import jdk.graal.compiler.nodes.java.ArrayLengthNode; -import jdk.graal.compiler.serviceprovider.JavaVersionUtil; import org.graalvm.nativeimage.ImageSingletons; import org.graalvm.nativeimage.IsolateThread; import org.graalvm.nativeimage.LogHandler; @@ -120,6 +116,9 @@ import com.oracle.svm.core.util.Utf8; import com.oracle.svm.core.util.VMError; +import jdk.graal.compiler.core.common.SuppressFBWarnings; +import jdk.graal.compiler.nodes.java.ArrayLengthNode; +import jdk.graal.compiler.serviceprovider.JavaVersionUtil; import jdk.internal.misc.Unsafe; import jdk.vm.ci.meta.MetaUtil; @@ -997,22 +996,15 @@ static JNIObjectHandle ToReflectedMethod(JNIEnvironment env, JNIObjectHandle cla JNIAccessibleMethodDescriptor descriptor = JNIReflectionDictionary.getMethodDescriptor(jniMethod); if (descriptor != null) { Class clazz = jniMethod.getDeclaringClass().getClassObject(); - if (descriptor.isConstructor()) { - for (Constructor ctor : clazz.getDeclaredConstructors()) { - if (descriptor.equals(JNIAccessibleMethodDescriptor.of(ctor))) { - result = ctor; - break; - } - } - } else { - for (Method method : clazz.getDeclaredMethods()) { - if (descriptor.getName().equals(method.getName())) { - if (descriptor.equals(JNIAccessibleMethodDescriptor.of(method))) { - result = method; - break; - } - } - } + Class[] parameter = MethodType.fromMethodDescriptorString(descriptor.getSignature(), JNIFunctions.class.getClassLoader()).parameterArray(); + try { + result = descriptor.isConstructor() ? clazz.getDeclaredConstructor(parameter) : clazz.getDeclaredMethod(descriptor.getName(), parameter); + } catch (NoSuchMethodException e) { + /* + * The method might have been registered for JNI access but not for reflection. When + * missing registration errors are not thrown, this results in a + * NoSuchMethodException, which means we have to return null. + */ } } return JNIObjectHandles.createLocal(result); diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/methodhandles/Target_java_lang_invoke_MethodHandleNatives.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/methodhandles/Target_java_lang_invoke_MethodHandleNatives.java index 8cdee0ec8a8c..a5f955c1663d 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/methodhandles/Target_java_lang_invoke_MethodHandleNatives.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/methodhandles/Target_java_lang_invoke_MethodHandleNatives.java @@ -39,6 +39,7 @@ import java.lang.reflect.Modifier; import jdk.graal.compiler.debug.GraalError; +import org.graalvm.nativeimage.MissingReflectionRegistrationError; import com.oracle.svm.core.StaticFieldsSupport; import com.oracle.svm.core.SubstrateUtil; @@ -237,16 +238,25 @@ static Method lookupMethod(Class declaringClazz, String name, Class[] para return lookupMethod(declaringClazz, name, parameterTypes, null); } - private static Method lookupMethod(Class declaringClazz, String name, Class[] parameterTypes, NoSuchMethodException originalException) throws NoSuchMethodException { + private static Method lookupMethod(Class declaringClazz, String name, Class[] parameterTypes, Throwable originalException) throws NoSuchMethodException { try { Method result = declaringClazz.getDeclaredMethod(name, parameterTypes); forceAccess(result); return result; - } catch (NoSuchMethodException e) { + } catch (NoSuchMethodException | MissingReflectionRegistrationError e) { + /* + * Getting a MissingReflectionRegistration error during lookup is not a problem if we + * find a matching method in a superclass, since if an overriding method existed a + * hiding method would have been registered. + */ Class superClass = declaringClazz.getSuperclass(); - NoSuchMethodException newOriginalException = originalException == null ? e : originalException; + Throwable newOriginalException = originalException == null ? e : originalException; if (superClass == null) { - throw newOriginalException; + if (newOriginalException instanceof NoSuchMethodException noSuchMethodException) { + throw noSuchMethodException; + } else { + throw (Error) newOriginalException; + } } else { return lookupMethod(superClass, name, parameterTypes, newOriginalException); } @@ -257,16 +267,20 @@ static Field lookupField(Class declaringClazz, String name) throws NoSuchFiel return lookupField(declaringClazz, name, null); } - private static Field lookupField(Class declaringClazz, String name, NoSuchFieldException originalException) throws NoSuchFieldException { + private static Field lookupField(Class declaringClazz, String name, Throwable originalException) throws NoSuchFieldException { try { Field result = declaringClazz.getDeclaredField(name); forceAccess(result); return result; - } catch (NoSuchFieldException e) { + } catch (NoSuchFieldException | MissingReflectionRegistrationError e) { Class superClass = declaringClazz.getSuperclass(); - NoSuchFieldException newOriginalException = originalException == null ? e : originalException; + Throwable newOriginalException = originalException == null ? e : originalException; if (superClass == null) { - throw newOriginalException; + if (newOriginalException instanceof NoSuchFieldException noSuchFieldException) { + throw noSuchFieldException; + } else { + throw (Error) newOriginalException; + } } else { return lookupField(superClass, name, newOriginalException); } @@ -325,6 +339,12 @@ public static Target_java_lang_invoke_MemberName resolve(Target_java_lang_invoke self.flags |= field.getModifiers(); } return self; + } catch (MissingReflectionRegistrationError e) { + if (speculativeResolve) { + return null; + } else { + throw e; + } } catch (NoSuchMethodException e) { if (speculativeResolve) { return null; diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/reflect/MissingReflectionRegistrationUtils.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/reflect/MissingReflectionRegistrationUtils.java index 8ca8222d8ccd..f3756c588c4e 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/reflect/MissingReflectionRegistrationUtils.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/reflect/MissingReflectionRegistrationUtils.java @@ -24,6 +24,8 @@ */ package com.oracle.svm.core.reflect; +import static com.oracle.svm.core.MissingRegistrationUtils.ERROR_EMPHASIS_INDENT; + import java.lang.reflect.Constructor; import java.lang.reflect.Executable; import java.lang.reflect.Field; @@ -103,7 +105,13 @@ private static String errorMessage(String failedAction, String elementDescriptor } private static String errorMessage(String failedAction, String elementDescriptor, String note, String helpLink) { - return "The program tried to reflectively " + failedAction + " " + elementDescriptor + + /* Can't use multi-line strings as they pull in format and bloat "Hello, World!" */ + return "The program tried to reflectively " + failedAction + + System.lineSeparator() + + System.lineSeparator() + + ERROR_EMPHASIS_INDENT + elementDescriptor + + System.lineSeparator() + + System.lineSeparator() + " without it being registered for runtime reflection. Add " + elementDescriptor + " to the " + helpLink + " metadata to solve this problem. " + (note != null ? "Note: " + note + " " : "") + "See https://www.graalvm.org/latest/reference-manual/native-image/metadata/#" + helpLink + " for help."; diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/reflect/target/ReflectionMetadataDecoderImpl.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/reflect/target/ReflectionMetadataDecoderImpl.java index a3da47c27ab9..fb0b1143841c 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/reflect/target/ReflectionMetadataDecoderImpl.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/reflect/target/ReflectionMetadataDecoderImpl.java @@ -34,7 +34,6 @@ import java.util.Arrays; import java.util.function.Function; -import jdk.graal.compiler.core.common.util.UnsafeArrayTypeReader; import org.graalvm.nativeimage.ImageSingletons; import org.graalvm.nativeimage.Platforms; import org.graalvm.nativeimage.impl.InternalPlatform; @@ -51,6 +50,8 @@ import com.oracle.svm.core.util.ByteArrayReader; import com.oracle.svm.core.util.VMError; +import jdk.graal.compiler.core.common.util.UnsafeArrayTypeReader; + /** * This class performs the parsing of reflection metadata at runtime. The encoding formats are * specified as comments above each parsing method. @@ -324,7 +325,13 @@ private static Object decodeField(UnsafeArrayTypeReader buf, Class declaringC if (inHeap) { Field field = (Field) decodeObject(buf); if (publicOnly && !Modifier.isPublic(field.getModifiers())) { - return null; + /* + * Generate negative copy of the field. Finding a non-public field when looking for + * a public one should not result in a missing registration exception. + */ + Target_java_lang_reflect_Field negativeField = new Target_java_lang_reflect_Field(); + negativeField.constructor(declaringClass, field.getName(), Object.class, field.getModifiers() | NEGATIVE_FLAG_MASK, false, -1, null, null); + field = SubstrateUtil.cast(negativeField, Field.class); } if (reflectOnly) { return complete ? field : null; @@ -363,7 +370,7 @@ private static Object decodeField(UnsafeArrayTypeReader buf, Class declaringC int offset = buf.getSVInt(); String deletedReason = decodeName(buf); if (publicOnly && !Modifier.isPublic(modifiers)) { - return null; + modifiers |= NEGATIVE_FLAG_MASK; } Target_java_lang_reflect_Field field = new Target_java_lang_reflect_Field(); @@ -483,7 +490,19 @@ private static Object decodeExecutable(UnsafeArrayTypeReader buf, Class decla if (inHeap) { Executable executable = (Executable) decodeObject(buf); if (publicOnly && !Modifier.isPublic(executable.getModifiers())) { - return null; + /* + * Generate negative copy of the executable. Finding a non-public method when + * looking for a public one should not result in a missing registration exception. + */ + if (isMethod) { + Target_java_lang_reflect_Method negativeMethod = new Target_java_lang_reflect_Method(); + negativeMethod.constructor(declaringClass, executable.getName(), executable.getParameterTypes(), Object.class, null, modifiers | NEGATIVE_FLAG_MASK, -1, null, null, null, null); + executable = SubstrateUtil.cast(negativeMethod, Executable.class); + } else { + Target_java_lang_reflect_Constructor negativeConstructor = new Target_java_lang_reflect_Constructor(); + negativeConstructor.constructor(declaringClass, executable.getParameterTypes(), null, modifiers | NEGATIVE_FLAG_MASK, -1, null, null, null); + executable = SubstrateUtil.cast(negativeConstructor, Executable.class); + } } if (reflectOnly) { return complete ? executable : null; @@ -545,7 +564,7 @@ private static Object decodeExecutable(UnsafeArrayTypeReader buf, Class decla byte[] reflectParameters = decodeByteArray(buf); Object accessor = decodeObject(buf); if (publicOnly && !Modifier.isPublic(modifiers)) { - return null; + modifiers |= NEGATIVE_FLAG_MASK; } Target_java_lang_reflect_Executable executable; diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/ServiceLoaderFeature.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/ServiceLoaderFeature.java index fdfe8521ad34..6c412c786731 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/ServiceLoaderFeature.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/ServiceLoaderFeature.java @@ -40,6 +40,7 @@ import com.oracle.svm.core.feature.AutomaticallyRegisteredFeature; import com.oracle.svm.core.feature.InternalFeature; +import com.oracle.svm.core.jdk.ServiceCatalogSupport; import com.oracle.svm.core.option.HostedOptionKey; import com.oracle.svm.core.option.LocatableMultiOptionValue; import com.oracle.svm.hosted.analysis.Inflation; @@ -133,14 +134,24 @@ public void afterRegistration(AfterRegistrationAccess access) { public void beforeAnalysis(BeforeAnalysisAccess access) { FeatureImpl.BeforeAnalysisAccessImpl accessImpl = (FeatureImpl.BeforeAnalysisAccessImpl) access; accessImpl.imageClassLoader.classLoaderSupport.serviceProvidersForEach((serviceName, providers) -> { - if (servicesToSkip.contains(serviceName)) { - return; - } Class serviceClass = access.findClassByName(serviceName); - if (serviceClass == null || serviceClass.isArray() || serviceClass.isPrimitive()) { - return; + boolean skipService = false; + /* If the service should not end up in the image, we remove all the providers with it */ + Collection providersToSkip = providers; + if (servicesToSkip.contains(serviceName)) { + skipService = true; + } else if (serviceClass == null || serviceClass.isArray() || serviceClass.isPrimitive()) { + skipService = true; + } else if (!accessImpl.getHostVM().platformSupported(serviceClass)) { + skipService = true; + } else { + providersToSkip = providers.stream().filter(serviceProvidersToSkip::contains).collect(Collectors.toList()); + if (!providersToSkip.isEmpty()) { + skipService = true; + } } - if (!accessImpl.getHostVM().platformSupported(serviceClass)) { + if (skipService) { + ServiceCatalogSupport.singleton().removeServicesFromServicesCatalog(serviceName, new HashSet<>(providersToSkip)); return; } access.registerReachabilityHandler(a -> handleServiceClassIsReachable(a, serviceClass, providers), serviceClass); diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/jdk/localization/LocalizationFeature.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/jdk/localization/LocalizationFeature.java index 89dc8f45fdcf..d21e21c7e5fb 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/jdk/localization/LocalizationFeature.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/jdk/localization/LocalizationFeature.java @@ -100,6 +100,7 @@ import sun.util.locale.LocaleObjectCache; import sun.util.locale.provider.LocaleProviderAdapter; import sun.util.locale.provider.ResourceBundleBasedAdapter; +import sun.util.resources.Bundles; import sun.util.resources.LocaleData; import sun.util.resources.ParallelListResourceBundle; @@ -135,6 +136,13 @@ @AutomaticallyRegisteredFeature public class LocalizationFeature implements InternalFeature { + /** + * Locales required by default in Java. + * + * @see Locale#getAvailableLocales() + */ + private static final Locale[] MINIMAL_LOCALES = new Locale[]{Locale.ROOT, Locale.ENGLISH, Locale.US}; + protected final boolean optimizedMode = Options.LocalizationOptimizedMode.getValue(); private final boolean substituteLoadLookup = Options.LocalizationSubstituteLoadLookup.getValue(); @@ -371,6 +379,8 @@ private static Set processLocalesOption() { if (Options.IncludeAllLocales.getValue()) { Collections.addAll(locales, Locale.getAvailableLocales()); /*- Fallthrough to also allow adding custom locales */ + } else { + Collections.addAll(locales, MINIMAL_LOCALES); } List invalid = new ArrayList<>(); for (String tag : Options.IncludeLocales.getValue().values()) { @@ -559,19 +569,43 @@ public void prepareBundle(String baseName) { prepareBundle(baseName, allLocales); } - @SuppressWarnings("deprecation") + private static final String[] RESOURCE_EXTENSION_PREFIXES = new String[]{ + "sun.text.resources.cldr", + "sun.util.resources.cldr", + "sun.text.resources", + "sun.util.resources" + }; + @Platforms(Platform.HOSTED_ONLY.class) public void prepareBundle(String baseName, Collection wantedLocales) { if (baseName.isEmpty()) { return; } + prepareBundleInternal(baseName, wantedLocales); + + String alternativeBundleName = null; + for (String resourceExtentionPrefix : RESOURCE_EXTENSION_PREFIXES) { + if (baseName.startsWith(resourceExtentionPrefix) && !baseName.startsWith(resourceExtentionPrefix + ".ext")) { + alternativeBundleName = baseName.replace(resourceExtentionPrefix, resourceExtentionPrefix + ".ext"); + break; + } + } + if (alternativeBundleName != null) { + prepareBundleInternal(alternativeBundleName, wantedLocales); + } + } + + private void prepareBundleInternal(String baseName, Collection wantedLocales) { boolean somethingFound = false; for (Locale locale : wantedLocales) { List resourceBundle; try { resourceBundle = ImageSingletons.lookup(ClassLoaderSupport.class).getResourceBundle(baseName, locale); } catch (MissingResourceException mre) { + for (Locale candidateLocale : support.control.getCandidateLocales(baseName, locale)) { + prepareNegativeBundle(baseName, candidateLocale); + } continue; } somethingFound |= !resourceBundle.isEmpty(); @@ -607,7 +641,7 @@ public void prepareBundle(String baseName, Collection wantedLocales) { trace(errorMessage); prepareNegativeBundle(baseName, Locale.ROOT); for (String language : wantedLocales.stream().map(Locale::getLanguage).collect(Collectors.toSet())) { - prepareNegativeBundle(baseName, new Locale(language)); + prepareNegativeBundle(baseName, Locale.of(language)); } for (Locale locale : wantedLocales) { if (!locale.getCountry().isEmpty()) { @@ -619,14 +653,13 @@ public void prepareBundle(String baseName, Collection wantedLocales) { @Platforms(Platform.HOSTED_ONLY.class) protected void prepareNegativeBundle(String baseName, Locale locale) { - String bundleName = baseName + (locale.toString().isEmpty() ? "" : "_" + locale); - Class clazz = findClassByName.apply(bundleName); - if (clazz != null) { - RuntimeReflection.register(clazz); - } else { - RuntimeReflection.registerClassLookup(bundleName); - } + String bundleName = support.control.toBundleName(baseName, locale); + RuntimeReflection.registerClassLookup(bundleName); Resources.singleton().registerNegativeQuery(support.getResultingPattern(baseName, locale) + ".properties"); + String otherBundleName = Bundles.toOtherBundleName(baseName, bundleName, locale); + if (!otherBundleName.equals(bundleName)) { + RuntimeReflection.registerClassLookup(otherBundleName); + } } @Platforms(Platform.HOSTED_ONLY.class) diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/reflect/ReflectionDataBuilder.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/reflect/ReflectionDataBuilder.java index 3c959bd9f521..5ae7c3402fb8 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/reflect/ReflectionDataBuilder.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/reflect/ReflectionDataBuilder.java @@ -827,10 +827,6 @@ private boolean includeAnnotation(AnnotationValue annotationValue) { private void registerTypesForAnnotation(AnnotationValue annotationValue) { registerTypes(annotationValue.getTypes()); - Class annotationType = annotationValue.getType(); - if (annotationType != null) { - RuntimeReflection.registerAllDeclaredMethods(annotationType); - } } @SuppressWarnings("cast") @@ -840,6 +836,7 @@ private void registerTypes(Collection> types) { analysisType.registerAsReachable("Is used by annotation of element registered for reflection."); if (type.isAnnotation()) { RuntimeProxyCreation.register(type); + RuntimeReflection.registerAllDeclaredMethods(type); } /* * Exception proxies are stored as-is in the image heap @@ -924,7 +921,15 @@ public Map, Set>> getReflectionInnerClasses() { } public int getEnabledReflectionQueries(Class clazz) { - return enabledQueriesFlags.getOrDefault(clazz, 0); + int enabledQueries = enabledQueriesFlags.getOrDefault(clazz, 0); + /* + * Primitives, arrays and object are registered by default since they provide reflective + * access to either no members or only Object methods. + */ + if (clazz == Object.class || clazz.isPrimitive() || clazz.isArray()) { + enabledQueries |= ALL_DECLARED_CONSTRUCTORS_FLAG | ALL_CONSTRUCTORS_FLAG | ALL_DECLARED_METHODS_FLAG | ALL_METHODS_FLAG | ALL_DECLARED_FIELDS_FLAG | ALL_FIELDS_FLAG; + } + return enabledQueries; } @Override diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/reflect/ReflectionFeature.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/reflect/ReflectionFeature.java index b0805cc60225..6c33e0d780e6 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/reflect/ReflectionFeature.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/reflect/ReflectionFeature.java @@ -31,13 +31,11 @@ import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.Arrays; +import java.util.List; import java.util.Map; import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; -import jdk.graal.compiler.api.replacements.SnippetReflectionProvider; -import jdk.graal.compiler.nodes.graphbuilderconf.GraphBuilderConfiguration.Plugins; -import jdk.graal.compiler.phases.util.Providers; import org.graalvm.nativeimage.AnnotationAccess; import org.graalvm.nativeimage.ImageSingletons; import org.graalvm.nativeimage.c.function.CFunctionPointer; @@ -59,6 +57,7 @@ import com.oracle.svm.core.feature.InternalFeature; import com.oracle.svm.core.fieldvaluetransformer.FieldValueTransformerWithAvailability; import com.oracle.svm.core.graal.meta.KnownOffsets; +import com.oracle.svm.core.hub.ClassForNameSupport; import com.oracle.svm.core.hub.DynamicHub; import com.oracle.svm.core.meta.MethodPointer; import com.oracle.svm.core.meta.SharedMethod; @@ -84,6 +83,9 @@ import com.oracle.svm.util.ModuleSupport; import com.oracle.svm.util.ReflectionUtil; +import jdk.graal.compiler.api.replacements.SnippetReflectionProvider; +import jdk.graal.compiler.nodes.graphbuilderconf.GraphBuilderConfiguration.Plugins; +import jdk.graal.compiler.phases.util.Providers; import jdk.internal.reflect.CallerSensitive; import jdk.internal.reflect.Reflection; import jdk.vm.ci.meta.JavaKind; @@ -104,6 +106,7 @@ public class ReflectionFeature implements InternalFeature, ReflectionSubstitutio */ private static final Method findCallerSensitiveAdapterMethod = ReflectionUtil.lookupMethod(ReflectionUtil.lookupClass(false, "jdk.internal.reflect.DirectMethodHandleAccessor"), "findCSMethodAdapter", Method.class); + private static final List> PRIMITIVE_CLASSES = List.of(void.class, boolean.class, byte.class, short.class, char.class, int.class, long.class, float.class, double.class); private AnnotationSubstitutionProcessor annotationSubstitutions; @@ -265,6 +268,11 @@ public void duringSetup(DuringSetupAccess a) { loader = access.getImageClassLoader(); annotationSubstitutions = ((Inflation) access.getBigBang()).getAnnotationSubstitutionProcessor(); + + /* Primitive classes cannot be accessed through Class.forName() */ + for (Class primitiveClass : PRIMITIVE_CLASSES) { + ClassForNameSupport.registerNegativeQuery(primitiveClass.getName()); + } } @Override @@ -282,7 +290,7 @@ public void beforeAnalysis(BeforeAnalysisAccess access) { access.registerFieldValueTransformer(ReflectionUtil.lookupField(SubstrateMethodAccessor.class, "vtableOffset"), new ComputeVTableOffset()); /* Make sure array classes don't need to be registered for reflection. */ - RuntimeReflection.register(Object[].class.getMethods()); + RuntimeReflection.register(Object.class.getDeclaredMethods()); } @Override 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 290f56e45fd1..367cea10ee1d 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 @@ -48,19 +48,6 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Stream; -import jdk.graal.compiler.debug.DebugContext; -import jdk.graal.compiler.graph.iterators.NodeIterable; -import jdk.graal.compiler.java.GraphBuilderPhase; -import jdk.graal.compiler.java.LambdaUtils; -import jdk.graal.compiler.nodes.ConstantNode; -import jdk.graal.compiler.nodes.StructuredGraph; -import jdk.graal.compiler.nodes.graphbuilderconf.GraphBuilderConfiguration; -import jdk.graal.compiler.nodes.graphbuilderconf.InvocationPlugins; -import jdk.graal.compiler.options.OptionValues; -import jdk.graal.compiler.phases.OptimisticOptimizations; -import jdk.graal.compiler.phases.tiers.HighTierContext; -import jdk.graal.compiler.printer.GraalDebugHandlersFactory; -import jdk.graal.compiler.replacements.MethodHandlePlugin; import org.graalvm.nativeimage.ImageSingletons; import org.graalvm.nativeimage.hosted.Feature; import org.graalvm.nativeimage.hosted.RuntimeReflection; @@ -94,6 +81,19 @@ import com.oracle.svm.util.LogUtils; import com.oracle.svm.util.ReflectionUtil; +import jdk.graal.compiler.debug.DebugContext; +import jdk.graal.compiler.graph.iterators.NodeIterable; +import jdk.graal.compiler.java.GraphBuilderPhase; +import jdk.graal.compiler.java.LambdaUtils; +import jdk.graal.compiler.nodes.ConstantNode; +import jdk.graal.compiler.nodes.StructuredGraph; +import jdk.graal.compiler.nodes.graphbuilderconf.GraphBuilderConfiguration; +import jdk.graal.compiler.nodes.graphbuilderconf.InvocationPlugins; +import jdk.graal.compiler.options.OptionValues; +import jdk.graal.compiler.phases.OptimisticOptimizations; +import jdk.graal.compiler.phases.tiers.HighTierContext; +import jdk.graal.compiler.printer.GraalDebugHandlersFactory; +import jdk.graal.compiler.replacements.MethodHandlePlugin; import jdk.internal.reflect.ReflectionFactory; import jdk.vm.ci.meta.Constant; import jdk.vm.ci.meta.JavaConstant; @@ -468,6 +468,8 @@ public void registerWithTargetConstructorClass(ConfigurationCondition condition, } Class serializationTargetClass = typeResolver.resolveType(targetClassName); + /* With invalid streams we have to register the class for lookup */ + ImageSingletons.lookup(RuntimeReflectionSupport.class).registerClassLookup(condition, targetClassName); if (serializationTargetClass == null) { return; } @@ -500,6 +502,11 @@ public void registerWithTargetConstructorClass(ConfigurationCondition condition, if (!Serializable.class.isAssignableFrom(serializationTargetClass)) { return; } + /* + * Making this class reachable as it will end up in the image heap without the analysis + * knowing. + */ + RuntimeReflection.register(java.io.ObjectOutputStream.class); if (denyRegistry.isAllowed(serializationTargetClass)) { if (customTargetConstructorClass != null) { diff --git a/substratevm/src/com.oracle.svm.jvmtiagentbase/src/com/oracle/svm/jvmtiagentbase/Support.java b/substratevm/src/com.oracle.svm.jvmtiagentbase/src/com/oracle/svm/jvmtiagentbase/Support.java index 0a327856f452..911d9989e4f3 100644 --- a/substratevm/src/com.oracle.svm.jvmtiagentbase/src/com/oracle/svm/jvmtiagentbase/Support.java +++ b/substratevm/src/com.oracle.svm.jvmtiagentbase/src/com/oracle/svm/jvmtiagentbase/Support.java @@ -367,29 +367,6 @@ public static JNIObjectHandle callStaticObjectMethodLLL(JNIEnvironment env, JNIO return jniFunctions().getCallStaticObjectMethodA().invoke(env, clazz, method, args); } - public static JNIObjectHandle callStaticObjectMethodLLLL(JNIEnvironment env, JNIObjectHandle clazz, JNIMethodId method, - JNIObjectHandle l0, JNIObjectHandle l1, JNIObjectHandle l2, JNIObjectHandle l3) { - - JNIValue args = StackValue.get(4, JNIValue.class); - args.addressOf(0).setObject(l0); - args.addressOf(1).setObject(l1); - args.addressOf(2).setObject(l2); - args.addressOf(3).setObject(l3); - return jniFunctions().getCallStaticObjectMethodA().invoke(env, clazz, method, args); - } - - public static JNIObjectHandle callStaticObjectMethodLLLLL(JNIEnvironment env, JNIObjectHandle clazz, JNIMethodId method, - JNIObjectHandle l0, JNIObjectHandle l1, JNIObjectHandle l2, JNIObjectHandle l3, JNIObjectHandle l4) { - - JNIValue args = StackValue.get(5, JNIValue.class); - args.addressOf(0).setObject(l0); - args.addressOf(1).setObject(l1); - args.addressOf(2).setObject(l2); - args.addressOf(3).setObject(l3); - args.addressOf(4).setObject(l4); - return jniFunctions().getCallStaticObjectMethodA().invoke(env, clazz, method, args); - } - public static void callStaticVoidMethodLL(JNIEnvironment env, JNIObjectHandle clazz, JNIMethodId method, JNIObjectHandle l0, JNIObjectHandle l1) { JNIValue args = StackValue.get(2, JNIValue.class); args.addressOf(0).setObject(l0); diff --git a/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/NativeImageResourceFileSystemProviderTest.java b/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/NativeImageResourceFileSystemProviderTest.java index 6e5ccc7fbe1c..b3a58cbbb627 100644 --- a/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/NativeImageResourceFileSystemProviderTest.java +++ b/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/NativeImageResourceFileSystemProviderTest.java @@ -259,6 +259,7 @@ public void readingFileByteChannel() { /** * Writing into file using {@link java.nio.channels.ByteChannel}. */ + @SuppressWarnings("CallToPrintStackTrace") @Test public void writingFileByteChannel() { Path resourceDirectory = fileSystem.getPath(RESOURCE_DIR); @@ -280,7 +281,8 @@ public void writingFileByteChannel() { try (SeekableByteChannel channel = Files.newByteChannel(resourceFile1, StandardOpenOption.READ, StandardOpenOption.WRITE)) { writeInChannelAndCheck(channel); } catch (IOException ioException) { - Assert.fail("Exception occurs during writing into file!"); + ioException.printStackTrace(); + Assert.fail("Exception occurred while writing into the file: " + resourceFile1); } }